diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c03f1d7e2..656d59201 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -278,6 +278,9 @@ importers: '@types/express': specifier: ^4.17.21 version: 4.17.21 + '@types/formidable': + specifier: ^3.4.5 + version: 3.4.5 '@types/http-errors': specifier: ^2.0.4 version: 2.0.4 @@ -2203,6 +2206,12 @@ packages: '@types/serve-static': 1.15.5 dev: true + /@types/formidable@3.4.5: + resolution: {integrity: sha512-s7YPsNVfnsng5L8sKnG/Gbb2tiwwJTY1conOkJzTMRvJAlLFW1nEua+ADsJQu8N1c0oTHx9+d5nqg10WuT9gHQ==} + dependencies: + '@types/node': 20.11.30 + dev: true + /@types/fs-extra@9.0.13: resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} dependencies: diff --git a/src/node/security/OAuth2Provider.ts b/src/node/security/OAuth2Provider.ts index 1ecbd07ee..fd50c9e87 100644 --- a/src/node/security/OAuth2Provider.ts +++ b/src/node/security/OAuth2Provider.ts @@ -1,7 +1,13 @@ import {ArgsExpressType} from "../types/ArgsExpressType"; -import Provider, {Account, Configuration} from 'oidc-provider'; +import Provider, {Account, Configuration, InteractionResults} from 'oidc-provider'; import {generateKeyPair, exportJWK} from 'jose' - +import MemoryAdapter from "./OIDCAdapter"; +import path from "path"; +const settings = require('../utils/Settings'); +import {IncomingForm} from 'formidable' +import {Request, Response} from 'express'; +import {format} from 'url' +import {ParsedUrlQuery} from "node:querystring"; const configuration: Configuration = { // refer to the documentation for other available configuration clients: [ { @@ -9,7 +15,7 @@ const configuration: Configuration = { client_secret: 'a_different_secret', grant_types: ['authorization_code'], response_types: ['code'], - redirect_uris: ['http://localhost:3001/cb'] + redirect_uris: ['http://localhost:3001/cb', 'https://oauth.pstmn.io/v1/callback'] }, { client_id: 'app', @@ -20,18 +26,33 @@ const configuration: Configuration = { } ], scopes: ['openid', 'profile', 'email'], - //adapter: MemoryAdapter, - /*findAccount: async (ctx, id) => { - console.log(ctx, id) + findAccount: async (ctx, id) => { + console.log("Finding account", id) return { accountId: id, claims: () => ({ sub: id, }) - } satisfies Account - },*/ + } as Account + }, + + ttl:{ + AccessToken: 1 * 60 * 60, // 1 hour in seconds + AuthorizationCode: 10 * 60, // 10 minutes in seconds + ClientCredentials: 1 * 60 * 60, // 1 hour in seconds + IdToken: 1 * 60 * 60, // 1 hour in seconds + RefreshToken: 1 * 24 * 60 * 60, // 1 day in seconds + }, + cookies: { + keys: ['oidc'], + }, + features:{ + devInteractions: {enabled: false}, + }, + adapter: MemoryAdapter }; + export const expressCreateServer = async (hookName: string, args: ArgsExpressType, cb: Function) => { const {privateKey} = await generateKeyPair('RS256'); const privateKeyJWK = await exportJWK(privateKey); @@ -43,9 +64,154 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp }, }); + + args.app.post('/interaction/:uid', async (req, res, next) => { + const formid = new IncomingForm(); + try { + const {login, password} = (await formid.parse(req))[0] + const {prompt, jti, session, params, grantId} = await oidc.interactionDetails(req, res); + + console.log("Session is", session) + + switch (prompt.name) { + case 'login': { + const users = settings.users as { + [username: string]: { + password: string; + admin: boolean; + } + } + const usersArray1 = Object.keys(users).map((username) => ({ + username, + ...users[username] + })); + const account = usersArray1.find((user) => user.username === login as unknown as string && user.password === password as unknown as string); + if (!account) { + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({error: "Invalid login"})); + } + + if (account) { + await oidc.interactionFinished(req, res, { + login: {accountId: account.username} + }, {mergeWithLastSubmission: false}); + } + break; + } + case 'consent': { + let grant; + if (grantId) { + // we'll be modifying existing grant in existing session + grant = await oidc.Grant.find(grantId); + } else { + // we're establishing a new grant + grant = new oidc.Grant({ + accountId: session!.accountId, + clientId: params.client_id as string, + }); + } + + if (prompt.details.missingOIDCScope) { + grant!.addOIDCScope(prompt.details.missingOIDCScope.join(' ')); + } + if (prompt.details.missingOIDCClaims) { + grant!.addOIDCClaims(prompt.details.missingOIDCClaims as string[]); + } + if (prompt.details.missingResourceScopes) { + for (const [indicator, scope] of Object.entries(prompt.details.missingResourceScopes)) { + grant!.addResourceScope(indicator, scope.join(' ')); + } + } + const result = {consent: {grantId: await grant!.save()}}; + await oidc.interactionFinished(req, res, result, { + mergeWithLastSubmission: true, + }); + break; + } + } + await next(); + } catch (err) { + console.log(err) + return next(err); + } + }) + + + args.app.get('/interaction/:uid', async (req: Request, res: Response, next) => { + try { + const { + uid, prompt, params, session, + } = await oidc.interactionDetails(req, res); + + console.log("Params are", params) + params["state"] = uid + + console.log("Prompt is", prompt) + switch (prompt.name) { + case 'login': { + res.redirect(format({ + pathname: '/views/login', + query: params as ParsedUrlQuery + })) + break + } + case 'consent': { + console.log("Consent") + res.redirect(format({ + pathname: '/views/consent', + query: params as ParsedUrlQuery + })) + break + } + default: + return res.sendFile(path.join(settings.root,'src','static', 'oidc','login.html')); + } + } catch (err) { + return next(err); + } + }); + + + args.app.get('/views/login', async (req, res) => { + res.sendFile(path.join(settings.root,'src','static', 'oidc','login.html')); + }) + + args.app.get('/views/consent', async (req, res) => { + res.sendFile(path.join(settings.root,'src','static', 'oidc','consent.html')); + }) + + args.app.get('/interaction/:uid/confirm', async (req, res) => { + const {uid, prompt, params} = await oidc.interactionDetails(req, res); + console.log('interaction', uid, prompt, params); + res.render('interaction', { + uid, + prompt, + params, + title: 'Authorize', + client: await oidc.Client.find(params.client_id!), + }); + }) + + args.app.get('/interaction/:uid', async (req, res) => { + return res.sendFile(path.join(settings.root,'src','static', 'oidc','login.html')); + }) + oidc.on('authorization.error', (ctx, error) => { console.log('authorization.error', error); }) + + oidc.on('server_error', (ctx, error) => { + console.log('server_error', error); + }) + oidc.on('grant.error', (ctx, error) => { + console.log('grant.error', error); + }) + oidc.on('introspection.error', (ctx, error) => { + console.log('introspection.error', error); + }) + oidc.on('revocation.error', (ctx, error) => { + console.log('revocation.error', error); + }) args.app.use("/oidc", oidc.callback()); - cb(); + //cb(); } diff --git a/src/node/security/OIDCAdapter.ts b/src/node/security/OIDCAdapter.ts index 29310d300..d71d98112 100644 --- a/src/node/security/OIDCAdapter.ts +++ b/src/node/security/OIDCAdapter.ts @@ -1,10 +1,12 @@ import {LRUCache} from 'lru-cache'; -import {Adapter, AdapterPayload} from "oidc-provider"; +import type {Adapter, AdapterPayload} from "oidc-provider"; const options = { max: 500, - + sizeCalculation: (item, key) => { + return 1 + }, // for use with tracking overall storage size maxSize: 5000, @@ -41,6 +43,7 @@ class MemoryAdapter implements Adapter{ } destroy(id:string) { + console.log("destroy", id); const key = this.key(id); const found = storage.get(key) as AdapterPayload; @@ -58,51 +61,63 @@ class MemoryAdapter implements Adapter{ } consume(id: string) { + console.log("consume", id); (storage.get(this.key(id)) as AdapterPayload)!.consumed = epochTime(); return Promise.resolve(); } find(id: string): Promise { - return Promise.resolve(storage.get(this.key(id)) as AdapterPayload); + const foundSession = storage.get(this.key(id)) as AdapterPayload; + console.log("find", id, foundSession); + if (storage.has(this.key(id))){ + return Promise.resolve(storage.get(this.key(id)) as AdapterPayload); + } + return Promise.resolve(undefined) } findByUserCode(userCode: string) { + console.log("findByUserCode", userCode); const id = storage.get(userCodeKeyFor(userCode)) as string; return this.find(id); } - upsert(id: string, payload: any, expiresIn: number) { - console.log('upsert'); + upsert(id: string, payload: { + iat: number; + exp: number; + uid: string; + kind: string; + jti: string; + accountId: string; + loginTs: number; + }, expiresIn: number) { + console.log("upsert", payload); const key = this.key(id); - const { grantId, userCode } = payload; - if (grantId) { - const grantKey = grantKeyFor(grantId); - const grant = storage.get(grantKey) as unknown as string[]; - if (!grant) { - storage.set(grantKey, [key]); - } else { - grant.push(key); - } - } - - if (userCode) { - storage.set(userCodeKeyFor(userCode), id, {ttl:expiresIn * 1000}); - } - storage.set(key, payload, {ttl: expiresIn * 1000}); return Promise.resolve(); } findByUid(uid: string): Promise { - console.log('findByUid', uid); + console.log("findByUid", uid); + for(const [_, value] of storage.entries()){ + if(typeof value ==="object" && "uid" in value && value.uid === uid){ + console.log("found", value); + return Promise.resolve(value); + } + } + console.log("not found"); return Promise.resolve(undefined); } revokeByGrantId(grantId: string): Promise { - console.log('findByUid', grantId); - return Promise.resolve(undefined); + const grantKey = grantKeyFor(grantId); + const grant = storage.get(grantKey) as string[]; + if (grant) { + grant.forEach((token) => storage.delete(token)); + storage.delete(grantKey); + } + return Promise.resolve(); } } diff --git a/src/package.json b/src/package.json index da3f32ff5..68825c1b3 100644 --- a/src/package.json +++ b/src/package.json @@ -84,6 +84,7 @@ "@playwright/test": "^1.42.1", "@types/async": "^3.2.24", "@types/express": "^4.17.21", + "@types/formidable": "^3.4.5", "@types/http-errors": "^2.0.4", "@types/jsdom": "^21.1.6", "@types/mocha": "^10.0.6", diff --git a/src/static/oidc/consent.html b/src/static/oidc/consent.html new file mode 100644 index 000000000..086740e56 --- /dev/null +++ b/src/static/oidc/consent.html @@ -0,0 +1,18 @@ + + + + + Consent + + + +
+ + +
+ diff --git a/src/static/oidc/login.html b/src/static/oidc/login.html new file mode 100644 index 000000000..edc26fce8 --- /dev/null +++ b/src/static/oidc/login.html @@ -0,0 +1,53 @@ + + + + + Title + + + +
+ + + + +
+
+ +