Skip to content

Commit

Permalink
start moving thing to backend
Browse files Browse the repository at this point in the history
  • Loading branch information
bazumo authored and N2D4 committed Oct 29, 2024
1 parent 747b77d commit a0673a2
Show file tree
Hide file tree
Showing 4 changed files with 286 additions and 28 deletions.
3 changes: 3 additions & 0 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,12 @@
"jose": "^5.2.2",
"next": "^14.2.5",
"nodemailer": "^6.9.10",
"oidc-provider": "^8.5.1",
"openid-client": "^5.6.4",
"oslo": "^1.2.1",
"pg": "^8.11.3",
"posthog-node": "^4.1.0",
"quick-lru": "^7.0.0",
"react": "^18.2",
"semver": "^7.6.3",
"server-only": "^0.0.1",
Expand All @@ -71,6 +73,7 @@
"@simplewebauthn/types": "^11.0.0",
"@types/node": "^20.8.10",
"@types/nodemailer": "^6.4.14",
"@types/oidc-provider": "^8.5.1",
"@types/react": "^18.2.66",
"@types/semver": "^7.5.8",
"concurrently": "^8.2.2",
Expand Down
40 changes: 40 additions & 0 deletions apps/backend/src/app/api/v1/idp/[...route]/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { interaction_route, oidc } from "@/idp/idp";
import { IncomingMessage, ServerResponse } from "http";
import { NextApiRequest } from "next";
import { NextResponse } from "next/server";
import { NextRequest } from "next/server";


export async function GET(req: NextRequest) {

console.log(req);

let body = "";

const next_response = new NextResponse();
const res = {
...next_response,
removeHeader: (key: string) => {
console.log('removeHeader', key);
next_response.headers.delete(key);
},
setHeader: (key: string, value: string) => {
console.log('setHeader', key, value);
next_response.headers.set(key, value);
},
end: (chunk: any, encoding: any, callback: any) => {
console.log('end', chunk);
body += chunk;
},
};


const x = await oidc.callback()(req as unknown as IncomingMessage, res as unknown as ServerResponse);

const finalResponse = new NextResponse(body);
next_response.headers.forEach((value, key) => {
finalResponse.headers.set(key, value);
});

return finalResponse;
}
246 changes: 226 additions & 20 deletions apps/backend/src/idp/idp.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,238 @@
import { getEnvVariable } from '@stackframe/stack-shared/dist/utils/env';
import Provider from 'oidc-provider';

interface ProviderSettings {
redirectUris: string[],
clientId: string,
clientSecret: string,
import { Configuration } from 'oidc-provider';

import QuickLRU from 'quick-lru';

interface StorageValue {
consumed?: number,
uid: string,
grantId?: string,
userCode?: string,
[key: string]: any, // Add index signature to allow arbitrary string keys
}

function getProvider(providerSettings: ProviderSettings) {
let storage = new QuickLRU<string, StorageValue | string[] | string>({ maxSize: 1000 });

function grantKeyFor(id: string): string {
return `grant:${id}`;
}

const provider = new Provider(getEnvVariable('STACK_BASE_URL'), {
clients: [
{
client_id: providerSettings.clientId,
client_secret: providerSettings.clientSecret,
redirect_uris: providerSettings.redirectUris,
},
],
});
function sessionUidKeyFor(id: string): string {
return `sessionUid:${id}`;
}

function userCodeKeyFor(userCode: string): string {
return `userCode:${userCode}`;
}

const grantable = new Set([
'AccessToken',
'AuthorizationCode',
'RefreshToken',
'DeviceCode',
'BackchannelAuthenticationRequest',
]);

class MemoryAdapter {
private model: string;

constructor(model: string) {
console.log('MemoryAdapter constructor', model);
this.model = model;
}

key(id: string): string {
return `${this.model}:${id}`;
}

async destroy(id: string): Promise<void> {
console.log('MemoryAdapter destroy', id);
const key = this.key(id);
storage.delete(key);
}

async consume(id: string): Promise<void> {
console.log('MemoryAdapter consume', id);
const value = storage.get(this.key(id)) as StorageValue;
value.consumed = Math.floor(Date.now() / 1000);
}

return provider;
async find(id: string): Promise<StorageValue | undefined> {
console.log('MemoryAdapter find', id);
return storage.get(this.key(id)) as StorageValue | undefined;
}

async findByUid(uid: string): Promise<StorageValue | undefined> {
console.log('MemoryAdapter findByUid', uid);
const id = storage.get(sessionUidKeyFor(uid)) as string;
return await this.find(id);
}

async findByUserCode(userCode: string): Promise<StorageValue | undefined> {
console.log('MemoryAdapter findByUserCode', userCode);
const id = storage.get(userCodeKeyFor(userCode)) as string;
return await this.find(id);
}

async upsert(id: string, payload: StorageValue, expiresIn: number): Promise<void> {
const key = this.key(id);
console.log('MemoryAdapter upsert', id, payload, expiresIn);
if (this.model === 'Session') {
storage.set(sessionUidKeyFor(payload.uid), id, { maxAge: expiresIn * 1000 });
}

const { grantId, userCode } = payload;
if (grantable.has(this.model) && grantId) {
const grantKey = grantKeyFor(grantId);
const grant = storage.get(grantKey) as string[];
if (!grant) {
storage.set(grantKey, [key]);
} else {
grant.push(key);
}
}

if (userCode) {
storage.set(userCodeKeyFor(userCode), id, { maxAge: expiresIn * 1000 });
}

storage.set(key, payload, { maxAge: expiresIn * 1000 });
}


async revokeByGrantId(grantId: string): Promise<void> {
const grantKey = grantKeyFor(grantId);
const grant = storage.get(grantKey) as string[];
if (grant) {
grant.forEach((token: string) => storage.delete(token));
storage.delete(grantKey);
}
}
}

const idp = getProvider({
redirectUris: ['http://localhost:8102/api/v1/auth/oauth/callback/github'],
clientId: 'github',
clientSecret: 'MOCK-SERVER-SECRET',
const port = Number.parseInt(process.env.PORT || "8117");

const mockedProviders = [
"stack-auth",
];

const configuration: Configuration = {
adapter: MemoryAdapter,
clients: mockedProviders.map((providerId) => ({
client_id: "client-id",
client_secret: "test-client-secret",
redirect_uris: [
`http://localhost:8116/api/auth/callback/stack-auth`,
],
})),
ttl: {
// we make sessions short so it asks us for our login again after a minute, instead of automatically logging us in with the already-logged-in session
Session: 60,
},
features: {
devInteractions: {
enabled: false,
},
},

async loadExistingGrant(ctx: any) {
const grantId = ctx.oidc.result?.consent?.grantId
|| ctx.oidc.session!.grantIdFor(ctx.oidc.client!.clientId);

if (grantId) {
// keep grant expiry aligned with session expiry
// to prevent consent prompt being requested when grant expires
const grant = await ctx.oidc.provider.Grant.find(grantId);

// this aligns the Grant ttl with that of the current session
// if the same Grant is used for multiple sessions, or is set
// to never expire, you probably do not want this in your code
if (ctx.oidc.account && grant.exp < ctx.oidc.session!.exp) {
grant.exp = ctx.oidc.session!.exp;

await grant.save();
}

if (true /*HACK*/) {
const grant = new ctx.oidc.provider.Grant({
clientId: ctx.oidc.client!.clientId,
accountId: ctx.oidc.session!.accountId,
});

grant.addOIDCScope('openid email profile');
grant.addOIDCClaims(['first_name']);
grant.addResourceScope('urn:example:resource-indicator', 'api:read api:write');
await grant.save();
return grant;
}
}
},

interactions: {
// THIS IS WHERE WE REDIRECT TO PRIMARY AUTH SERVER
// TODO: do we need to sanitize the parameter?
url: (ctx, interaction) => `/interaction/${encodeURIComponent(interaction.uid)}`,

},


async findAccount(ctx, id) {
return {
accountId: id,
claims: async (use, scope, claims, rejected) => {
return {
sub: id,
email: "[email protected]",
email_verified: true,
name: "Test User"
};
}
};
},
};

// TODO: add stateful session management


const oidc = new Provider(`http://localhost:${port}`, configuration);


oidc.use(async (ctx, next) => {
console.log('oidc.use', ctx.path);
if (ctx.method === 'GET' && /^\/interaction\/[^/]+\/login$/.test(ctx.path)) {
console.log('oidc.use login', ctx.path);
ctx.body = 'OIDC Mock Server';
const uid = ctx.path.split('/')[2];


const grant = new oidc.Grant({
accountId: "lmao_id",
clientId: "client-id",
});

const grantId = await grant.save();


const result = {
login: {
accountId: "lmao_id",
},
consent: {
grantId,
},
};

return await oidc.interactionFinished(ctx.req, ctx.res, result);
}
if (ctx.method === 'GET' && /^\/interaction\/[^/]+$/.test(ctx.path)) {
console.log('oidc.use interaction', ctx.path);
const uid = ctx.path.split('/')[2];
return ctx.redirect(`http://localhost:8103/handler/sign-in?after_auth_return_to=${encodeURIComponent(`http://localhost:8117/interaction/${encodeURIComponent(uid)}/login`)}`);
}
await next();
});


export { oidc };

Loading

0 comments on commit a0673a2

Please sign in to comment.