diff --git a/changelog.md b/changelog.md index 1eb08f0..16cf9ea 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2026-04-20 - 1.19.0 - feat(oidc) +persist hashed OIDC tokens, authorization codes, and user consent in smartdata storage + +- replace in-memory OIDC authorization code, access token, refresh token, and consent stores with SmartData document classes +- store authorization codes and tokens as hashes instead of persisting plaintext values, with helpers for matching, expiration, and revocation +- persist and merge user consent scopes when issuing authorization codes +- add cleanup lifecycle management for expired OIDC state and stop the cleanup task when reception shuts down +- add tests covering hashed code/token matching, authorization code usage, refresh token revocation, and consent scope merging + ## 2026-04-20 - 1.18.0 - feat(reception) persist email action tokens and registration sessions for authentication and signup flows diff --git a/test/test.oidc.node.ts b/test/test.oidc.node.ts new file mode 100644 index 0000000..78ee1fb --- /dev/null +++ b/test/test.oidc.node.ts @@ -0,0 +1,76 @@ +import { tap, expect } from '@git.zone/tstest/tapbundle'; + +import { OidcAccessToken } from '../ts/reception/classes.oidcaccesstoken.js'; +import { OidcAuthorizationCode } from '../ts/reception/classes.oidcauthorizationcode.js'; +import { OidcRefreshToken } from '../ts/reception/classes.oidcrefreshtoken.js'; +import { OidcUserConsent } from '../ts/reception/classes.oidcuserconsent.js'; + +tap.test('stores authorization codes as hashes and marks them used', async () => { + const authCode = new OidcAuthorizationCode(); + authCode.id = 'oidc-auth-code'; + authCode.data.codeHash = OidcAuthorizationCode.hashCode('plain-auth-code'); + + let saveCount = 0; + (authCode as OidcAuthorizationCode & { save: () => Promise }).save = async () => { + saveCount++; + }; + + expect(authCode.matchesCode('plain-auth-code')).toBeTrue(); + expect(authCode.matchesCode('wrong-code')).toBeFalse(); + + await authCode.markUsed(); + expect(authCode.data.used).toBeTrue(); + expect(saveCount).toEqual(1); +}); + +tap.test('stores access tokens without plaintext persistence', async () => { + const accessToken = new OidcAccessToken(); + accessToken.id = 'oidc-access-token'; + accessToken.data.tokenHash = OidcAccessToken.hashToken('plain-access-token'); + accessToken.data.expiresAt = Date.now() + 60_000; + + expect(accessToken.matchesToken('plain-access-token')).toBeTrue(); + expect(accessToken.matchesToken('different-access-token')).toBeFalse(); + expect(accessToken.isExpired()).toBeFalse(); +}); + +tap.test('revokes persisted refresh tokens', async () => { + const refreshToken = new OidcRefreshToken(); + refreshToken.id = 'oidc-refresh-token'; + refreshToken.data.tokenHash = OidcRefreshToken.hashToken('plain-refresh-token'); + refreshToken.data.expiresAt = Date.now() + 60_000; + + let saveCount = 0; + (refreshToken as OidcRefreshToken & { save: () => Promise }).save = async () => { + saveCount++; + }; + + expect(refreshToken.matchesToken('plain-refresh-token')).toBeTrue(); + expect(refreshToken.data.revoked).toBeFalse(); + + await refreshToken.revoke(); + expect(refreshToken.data.revoked).toBeTrue(); + expect(saveCount).toEqual(1); +}); + +tap.test('merges user consent scopes without duplicates', async () => { + const consent = new OidcUserConsent(); + consent.id = 'oidc-consent'; + consent.data.userId = 'user-1'; + consent.data.clientId = 'client-1'; + consent.data.scopes = ['openid']; + + let saveCount = 0; + (consent as OidcUserConsent & { save: () => Promise }).save = async () => { + saveCount++; + }; + + await consent.grantScopes(['openid', 'email', 'profile']); + + expect(consent.data.scopes.sort()).toEqual(['email', 'openid', 'profile']); + expect(consent.data.grantedAt).toBeGreaterThan(0); + expect(consent.data.updatedAt).toBeGreaterThan(0); + expect(saveCount).toEqual(1); +}); + +export default tap.start(); diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index e763823..887e073 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@idp.global/idp.global', - version: '1.18.0', + version: '1.19.0', description: 'An identity provider software managing user authentications, registrations, and sessions.' } diff --git a/ts/reception/classes.oidcaccesstoken.ts b/ts/reception/classes.oidcaccesstoken.ts new file mode 100644 index 0000000..14ea964 --- /dev/null +++ b/ts/reception/classes.oidcaccesstoken.ts @@ -0,0 +1,34 @@ +import * as plugins from '../plugins.js'; +import type { OidcManager } from './classes.oidcmanager.js'; + +@plugins.smartdata.Manager() +export class OidcAccessToken extends plugins.smartdata.SmartDataDbDoc< + OidcAccessToken, + plugins.idpInterfaces.data.IOidcAccessToken, + OidcManager +> { + public static hashToken(tokenArg: string) { + return plugins.smarthash.sha256FromStringSync(tokenArg); + } + + @plugins.smartdata.unI() + public id: string; + + @plugins.smartdata.svDb() + public data: plugins.idpInterfaces.data.IOidcAccessToken['data'] = { + tokenHash: '', + clientId: '', + userId: '', + scopes: [], + expiresAt: 0, + issuedAt: 0, + }; + + public isExpired() { + return this.data.expiresAt < Date.now(); + } + + public matchesToken(tokenArg: string) { + return this.data.tokenHash === OidcAccessToken.hashToken(tokenArg); + } +} diff --git a/ts/reception/classes.oidcauthorizationcode.ts b/ts/reception/classes.oidcauthorizationcode.ts new file mode 100644 index 0000000..9cfda87 --- /dev/null +++ b/ts/reception/classes.oidcauthorizationcode.ts @@ -0,0 +1,44 @@ +import * as plugins from '../plugins.js'; +import type { OidcManager } from './classes.oidcmanager.js'; + +@plugins.smartdata.Manager() +export class OidcAuthorizationCode extends plugins.smartdata.SmartDataDbDoc< + OidcAuthorizationCode, + plugins.idpInterfaces.data.IAuthorizationCode, + OidcManager +> { + public static hashCode(codeArg: string) { + return plugins.smarthash.sha256FromStringSync(codeArg); + } + + @plugins.smartdata.unI() + public id: string; + + @plugins.smartdata.svDb() + public data: plugins.idpInterfaces.data.IAuthorizationCode['data'] = { + codeHash: '', + clientId: '', + userId: '', + scopes: [], + redirectUri: '', + codeChallenge: undefined, + codeChallengeMethod: undefined, + nonce: undefined, + expiresAt: 0, + issuedAt: 0, + used: false, + }; + + public isExpired() { + return this.data.expiresAt < Date.now(); + } + + public matchesCode(codeArg: string) { + return this.data.codeHash === OidcAuthorizationCode.hashCode(codeArg); + } + + public async markUsed() { + this.data.used = true; + await this.save(); + } +} diff --git a/ts/reception/classes.oidcmanager.ts b/ts/reception/classes.oidcmanager.ts index 43000f5..60b9cf3 100644 --- a/ts/reception/classes.oidcmanager.ts +++ b/ts/reception/classes.oidcmanager.ts @@ -1,6 +1,10 @@ import * as plugins from '../plugins.js'; import type { Reception } from './classes.reception.js'; import type { App } from './classes.app.js'; +import { OidcAccessToken } from './classes.oidcaccesstoken.js'; +import { OidcAuthorizationCode } from './classes.oidcauthorizationcode.js'; +import { OidcRefreshToken } from './classes.oidcrefreshtoken.js'; +import { OidcUserConsent } from './classes.oidcuserconsent.js'; /** * OidcManager handles OpenID Connect (OIDC) server functionality @@ -12,25 +16,31 @@ export class OidcManager { return this.receptionRef.db.smartdataDb; } - // In-memory store for authorization codes (short-lived, 10 min TTL) - private authorizationCodes = new Map(); + public COidcAuthorizationCode = plugins.smartdata.setDefaultManagerForDoc( + this, + OidcAuthorizationCode + ); - // In-memory store for access tokens (for validation) - private accessTokens = new Map(); + public COidcAccessToken = plugins.smartdata.setDefaultManagerForDoc(this, OidcAccessToken); - // In-memory store for refresh tokens - private refreshTokens = new Map(); + public COidcRefreshToken = plugins.smartdata.setDefaultManagerForDoc(this, OidcRefreshToken); - // In-memory store for user consents (should be persisted later) - private userConsents = new Map(); + public COidcUserConsent = plugins.smartdata.setDefaultManagerForDoc(this, OidcUserConsent); + + private cleanupInterval: ReturnType | null = null; constructor(receptionRefArg: Reception) { this.receptionRef = receptionRefArg; - - // Start cleanup task for expired codes/tokens this.startCleanupTask(); } + public async stop() { + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval); + this.cleanupInterval = null; + } + } + /** * Get the OIDC Discovery Document */ @@ -174,9 +184,11 @@ export class OidcManager { codeChallenge?: string, nonce?: string ): Promise { - const code = plugins.smartunique.shortId(32); - const authCode: plugins.idpInterfaces.data.IAuthorizationCode = { - code, + const code = this.createOpaqueToken(); + const authCode = new OidcAuthorizationCode(); + authCode.id = plugins.smartunique.shortId(12); + authCode.data = { + codeHash: OidcAuthorizationCode.hashCode(code), clientId, userId, scopes, @@ -184,11 +196,13 @@ export class OidcManager { codeChallenge, codeChallengeMethod: codeChallenge ? 'S256' : undefined, nonce, - expiresAt: Date.now() + 10 * 60 * 1000, // 10 minutes + expiresAt: Date.now() + 10 * 60 * 1000, + issuedAt: Date.now(), used: false, }; - this.authorizationCodes.set(code, authCode); + await authCode.save(); + await this.upsertUserConsent(userId, clientId, scopes); return code; } @@ -261,50 +275,48 @@ export class OidcManager { } // Find and validate authorization code - const authCode = this.authorizationCodes.get(code); + const authCode = await this.getAuthorizationCodeByCode(code); if (!authCode) { return this.tokenErrorResponse('invalid_grant', 'Invalid authorization code'); } - if (authCode.used) { - // Code reuse attack - revoke all tokens for this code - this.authorizationCodes.delete(code); + if (authCode.data.used) { return this.tokenErrorResponse('invalid_grant', 'Authorization code already used'); } - if (authCode.expiresAt < Date.now()) { - this.authorizationCodes.delete(code); + if (authCode.isExpired()) { + await authCode.delete(); return this.tokenErrorResponse('invalid_grant', 'Authorization code expired'); } - if (authCode.clientId !== app.data.oauthCredentials.clientId) { + if (authCode.data.clientId !== app.data.oauthCredentials.clientId) { return this.tokenErrorResponse('invalid_grant', 'Client ID mismatch'); } - if (authCode.redirectUri !== redirectUri) { + if (authCode.data.redirectUri !== redirectUri) { return this.tokenErrorResponse('invalid_grant', 'Redirect URI mismatch'); } // Verify PKCE if code challenge was used - if (authCode.codeChallenge) { + if (authCode.data.codeChallenge) { if (!codeVerifier) { return this.tokenErrorResponse('invalid_grant', 'Code verifier required'); } const expectedChallenge = this.generateS256Challenge(codeVerifier); - if (expectedChallenge !== authCode.codeChallenge) { + if (expectedChallenge !== authCode.data.codeChallenge) { return this.tokenErrorResponse('invalid_grant', 'Invalid code verifier'); } } // Mark code as used - authCode.used = true; + await authCode.markUsed(); // Generate tokens const tokens = await this.generateTokens( - authCode.userId, + authCode.data.userId, app.data.oauthCredentials.clientId, - authCode.scopes, - authCode.nonce + authCode.data.scopes, + authCode.data.nonce ); return new Response(JSON.stringify(tokens), { @@ -330,31 +342,30 @@ export class OidcManager { return this.tokenErrorResponse('invalid_request', 'Missing refresh_token'); } - const tokenHash = await plugins.smarthash.sha256FromString(refreshToken); - const storedToken = this.refreshTokens.get(tokenHash); + const storedToken = await this.getRefreshTokenByToken(refreshToken); if (!storedToken) { return this.tokenErrorResponse('invalid_grant', 'Invalid refresh token'); } - if (storedToken.revoked) { + if (storedToken.data.revoked) { return this.tokenErrorResponse('invalid_grant', 'Refresh token has been revoked'); } - if (storedToken.expiresAt < Date.now()) { - this.refreshTokens.delete(tokenHash); + if (storedToken.isExpired()) { + await storedToken.delete(); return this.tokenErrorResponse('invalid_grant', 'Refresh token expired'); } - if (storedToken.clientId !== app.data.oauthCredentials.clientId) { + if (storedToken.data.clientId !== app.data.oauthCredentials.clientId) { return this.tokenErrorResponse('invalid_grant', 'Client ID mismatch'); } // Generate new tokens (without new refresh token by default) const tokens = await this.generateTokens( - storedToken.userId, - storedToken.clientId, - storedToken.scopes, + storedToken.data.userId, + storedToken.data.clientId, + storedToken.data.scopes, undefined, false // Don't generate new refresh token ); @@ -384,18 +395,18 @@ export class OidcManager { const refreshTokenLifetime = 30 * 24 * 3600; // 30 days // Generate access token - const accessToken = plugins.smartunique.shortId(32); - const accessTokenHash = await plugins.smarthash.sha256FromString(accessToken); - const accessTokenData: plugins.idpInterfaces.data.IOidcAccessToken = { - id: plugins.smartunique.shortId(8), - tokenHash: accessTokenHash, + const accessToken = this.createOpaqueToken(); + const accessTokenData = new OidcAccessToken(); + accessTokenData.id = plugins.smartunique.shortId(12); + accessTokenData.data = { + tokenHash: OidcAccessToken.hashToken(accessToken), clientId, userId, scopes, expiresAt: now + accessTokenLifetime * 1000, issuedAt: now, }; - this.accessTokens.set(accessTokenHash, accessTokenData); + await accessTokenData.save(); // Generate ID token (JWT) const idToken = await this.generateIdToken(userId, clientId, scopes, nonce); @@ -410,11 +421,11 @@ export class OidcManager { // Generate refresh token if requested if (includeRefreshToken) { - const refreshToken = plugins.smartunique.shortId(48); - const refreshTokenHash = await plugins.smarthash.sha256FromString(refreshToken); - const refreshTokenData: plugins.idpInterfaces.data.IOidcRefreshToken = { - id: plugins.smartunique.shortId(8), - tokenHash: refreshTokenHash, + const refreshToken = this.createOpaqueToken(48); + const refreshTokenData = new OidcRefreshToken(); + refreshTokenData.id = plugins.smartunique.shortId(12); + refreshTokenData.data = { + tokenHash: OidcRefreshToken.hashToken(refreshToken), clientId, userId, scopes, @@ -422,7 +433,7 @@ export class OidcManager { issuedAt: now, revoked: false, }; - this.refreshTokens.set(refreshTokenHash, refreshTokenData); + await refreshTokenData.save(); response.refresh_token = refreshToken; } @@ -482,8 +493,7 @@ export class OidcManager { } const accessToken = authHeader.substring(7); - const tokenHash = await plugins.smarthash.sha256FromString(accessToken); - const tokenData = this.accessTokens.get(tokenHash); + const tokenData = await this.getAccessTokenByToken(accessToken); if (!tokenData) { return new Response(JSON.stringify({ error: 'invalid_token' }), { @@ -495,8 +505,8 @@ export class OidcManager { }); } - if (tokenData.expiresAt < Date.now()) { - this.accessTokens.delete(tokenHash); + if (tokenData.isExpired()) { + await tokenData.delete(); return new Response(JSON.stringify({ error: 'invalid_token', error_description: 'Token expired' }), { status: 401, headers: { @@ -507,7 +517,7 @@ export class OidcManager { } // Get user claims based on token scopes - const userInfo = await this.getUserClaims(tokenData.userId, tokenData.scopes); + const userInfo = await this.getUserClaims(tokenData.data.userId, tokenData.data.scopes); return new Response(JSON.stringify(userInfo), { status: 200, @@ -583,21 +593,20 @@ export class OidcManager { return new Response(null, { status: 200 }); // Spec says always return 200 } - const tokenHash = await plugins.smarthash.sha256FromString(token); - // Try to revoke as refresh token if (!tokenTypeHint || tokenTypeHint === 'refresh_token') { - const refreshToken = this.refreshTokens.get(tokenHash); + const refreshToken = await this.getRefreshTokenByToken(token); if (refreshToken) { - refreshToken.revoked = true; + await refreshToken.revoke(); return new Response(null, { status: 200 }); } } // Try to revoke as access token if (!tokenTypeHint || tokenTypeHint === 'access_token') { - if (this.accessTokens.has(tokenHash)) { - this.accessTokens.delete(tokenHash); + const accessToken = await this.getAccessTokenByToken(token); + if (accessToken) { + await accessToken.delete(); return new Response(null, { status: 200 }); } } @@ -616,6 +625,53 @@ export class OidcManager { return apps[0] || null; } + private createOpaqueToken(byteLength = 32): string { + return plugins.crypto.randomBytes(byteLength).toString('base64url'); + } + + private async getAuthorizationCodeByCode(codeArg: string) { + return this.COidcAuthorizationCode.getInstance({ + 'data.codeHash': OidcAuthorizationCode.hashCode(codeArg), + }); + } + + private async getAccessTokenByToken(tokenArg: string) { + return this.COidcAccessToken.getInstance({ + 'data.tokenHash': OidcAccessToken.hashToken(tokenArg), + }); + } + + private async getRefreshTokenByToken(tokenArg: string) { + return this.COidcRefreshToken.getInstance({ + 'data.tokenHash': OidcRefreshToken.hashToken(tokenArg), + }); + } + + public async getUserConsent(userIdArg: string, clientIdArg: string) { + return this.COidcUserConsent.getInstance({ + 'data.userId': userIdArg, + 'data.clientId': clientIdArg, + }); + } + + public async upsertUserConsent( + userIdArg: string, + clientIdArg: string, + scopesArg: plugins.idpInterfaces.data.TOidcScope[] + ) { + let userConsent = await this.getUserConsent(userIdArg, clientIdArg); + + if (!userConsent) { + userConsent = new OidcUserConsent(); + userConsent.id = plugins.smartunique.shortId(12); + userConsent.data.userId = userIdArg; + userConsent.data.clientId = clientIdArg; + } + + await userConsent.grantScopes(scopesArg); + return userConsent; + } + /** * Generate S256 PKCE challenge from verifier */ @@ -655,29 +711,45 @@ export class OidcManager { * Start cleanup task for expired tokens/codes */ private startCleanupTask(): void { - setInterval(() => { - const now = Date.now(); + this.cleanupInterval = setInterval(() => { + void this.cleanupExpiredOidcState(); + }, 60 * 1000); + } - // Clean up expired authorization codes - for (const [code, data] of this.authorizationCodes) { - if (data.expiresAt < now) { - this.authorizationCodes.delete(code); - } - } + private async cleanupExpiredOidcState() { + const now = Date.now(); - // Clean up expired access tokens - for (const [hash, data] of this.accessTokens) { - if (data.expiresAt < now) { - this.accessTokens.delete(hash); - } - } + const expiredAuthorizationCodes = await this.COidcAuthorizationCode.getInstances({ + data: { + expiresAt: { + $lt: now, + } as any, + }, + }); + for (const authCode of expiredAuthorizationCodes) { + await authCode.delete(); + } - // Clean up expired refresh tokens - for (const [hash, data] of this.refreshTokens) { - if (data.expiresAt < now) { - this.refreshTokens.delete(hash); - } - } - }, 60 * 1000); // Run every minute + const expiredAccessTokens = await this.COidcAccessToken.getInstances({ + data: { + expiresAt: { + $lt: now, + } as any, + }, + }); + for (const accessToken of expiredAccessTokens) { + await accessToken.delete(); + } + + const expiredRefreshTokens = await this.COidcRefreshToken.getInstances({ + data: { + expiresAt: { + $lt: now, + } as any, + }, + }); + for (const refreshToken of expiredRefreshTokens) { + await refreshToken.delete(); + } } } diff --git a/ts/reception/classes.oidcrefreshtoken.ts b/ts/reception/classes.oidcrefreshtoken.ts new file mode 100644 index 0000000..f69d3a3 --- /dev/null +++ b/ts/reception/classes.oidcrefreshtoken.ts @@ -0,0 +1,40 @@ +import * as plugins from '../plugins.js'; +import type { OidcManager } from './classes.oidcmanager.js'; + +@plugins.smartdata.Manager() +export class OidcRefreshToken extends plugins.smartdata.SmartDataDbDoc< + OidcRefreshToken, + plugins.idpInterfaces.data.IOidcRefreshToken, + OidcManager +> { + public static hashToken(tokenArg: string) { + return plugins.smarthash.sha256FromStringSync(tokenArg); + } + + @plugins.smartdata.unI() + public id: string; + + @plugins.smartdata.svDb() + public data: plugins.idpInterfaces.data.IOidcRefreshToken['data'] = { + tokenHash: '', + clientId: '', + userId: '', + scopes: [], + expiresAt: 0, + issuedAt: 0, + revoked: false, + }; + + public isExpired() { + return this.data.expiresAt < Date.now(); + } + + public matchesToken(tokenArg: string) { + return this.data.tokenHash === OidcRefreshToken.hashToken(tokenArg); + } + + public async revoke() { + this.data.revoked = true; + await this.save(); + } +} diff --git a/ts/reception/classes.oidcuserconsent.ts b/ts/reception/classes.oidcuserconsent.ts new file mode 100644 index 0000000..a231e3c --- /dev/null +++ b/ts/reception/classes.oidcuserconsent.ts @@ -0,0 +1,30 @@ +import * as plugins from '../plugins.js'; +import type { OidcManager } from './classes.oidcmanager.js'; + +@plugins.smartdata.Manager() +export class OidcUserConsent extends plugins.smartdata.SmartDataDbDoc< + OidcUserConsent, + plugins.idpInterfaces.data.IUserConsent, + OidcManager +> { + @plugins.smartdata.unI() + public id: string; + + @plugins.smartdata.svDb() + public data: plugins.idpInterfaces.data.IUserConsent['data'] = { + userId: '', + clientId: '', + scopes: [], + grantedAt: 0, + updatedAt: 0, + }; + + public async grantScopes(scopesArg: plugins.idpInterfaces.data.TOidcScope[]) { + this.data.scopes = [...new Set([...this.data.scopes, ...scopesArg])]; + if (!this.data.grantedAt) { + this.data.grantedAt = Date.now(); + } + this.data.updatedAt = Date.now(); + await this.save(); + } +} diff --git a/ts/reception/classes.reception.ts b/ts/reception/classes.reception.ts index 271807c..1d19e28 100644 --- a/ts/reception/classes.reception.ts +++ b/ts/reception/classes.reception.ts @@ -78,6 +78,7 @@ export class Reception { */ public async stop() { await this.housekeeping.stop(); + await this.oidcManager.stop(); console.log('stopped serviceserver!'); await this.db.stop(); } diff --git a/ts_interfaces/data/loint-reception.oidc.ts b/ts_interfaces/data/loint-reception.oidc.ts index e9bd0ad..40cd1c3 100644 --- a/ts_interfaces/data/loint-reception.oidc.ts +++ b/ts_interfaces/data/loint-reception.oidc.ts @@ -11,86 +11,94 @@ export type TOidcScope = 'openid' | 'profile' | 'email' | 'organizations' | 'rol * Authorization code for OAuth 2.0 authorization code flow */ export interface IAuthorizationCode { - /** The authorization code string */ - code: string; - /** OAuth client ID */ - clientId: string; - /** User ID who authorized */ - userId: string; - /** Scopes granted */ - scopes: TOidcScope[]; - /** Redirect URI used in authorization request */ - redirectUri: string; - /** PKCE code challenge (S256 hashed) */ - codeChallenge?: string; - /** PKCE code challenge method */ - codeChallengeMethod?: 'S256'; - /** Nonce from authorization request (for ID token) */ - nonce?: string; - /** Expiration timestamp (10 minutes from creation) */ - expiresAt: number; - /** Whether the code has been used (single-use) */ - used: boolean; + id: string; + data: { + /** Hashed authorization code string */ + codeHash: string; + /** OAuth client ID */ + clientId: string; + /** User ID who authorized */ + userId: string; + /** Scopes granted */ + scopes: TOidcScope[]; + /** Redirect URI used in authorization request */ + redirectUri: string; + /** PKCE code challenge (S256 hashed) */ + codeChallenge?: string; + /** PKCE code challenge method */ + codeChallengeMethod?: 'S256'; + /** Nonce from authorization request (for ID token) */ + nonce?: string; + /** Expiration timestamp (10 minutes from creation) */ + expiresAt: number; + /** Creation timestamp */ + issuedAt: number; + /** Whether the code has been used (single-use) */ + used: boolean; + }; } /** * OIDC Access Token (opaque or JWT) */ export interface IOidcAccessToken { - /** Token identifier */ id: string; - /** The access token string (or hash for storage) */ - tokenHash: string; - /** OAuth client ID */ - clientId: string; - /** User ID */ - userId: string; - /** Granted scopes */ - scopes: TOidcScope[]; - /** Expiration timestamp */ - expiresAt: number; - /** Creation timestamp */ - issuedAt: number; + data: { + /** The access token string hash for storage */ + tokenHash: string; + /** OAuth client ID */ + clientId: string; + /** User ID */ + userId: string; + /** Granted scopes */ + scopes: TOidcScope[]; + /** Expiration timestamp */ + expiresAt: number; + /** Creation timestamp */ + issuedAt: number; + }; } /** * OIDC Refresh Token */ export interface IOidcRefreshToken { - /** Token identifier */ id: string; - /** The refresh token string (or hash for storage) */ - tokenHash: string; - /** OAuth client ID */ - clientId: string; - /** User ID */ - userId: string; - /** Granted scopes */ - scopes: TOidcScope[]; - /** Expiration timestamp */ - expiresAt: number; - /** Creation timestamp */ - issuedAt: number; - /** Whether the token has been revoked */ - revoked: boolean; + data: { + /** The refresh token string hash for storage */ + tokenHash: string; + /** OAuth client ID */ + clientId: string; + /** User ID */ + userId: string; + /** Granted scopes */ + scopes: TOidcScope[]; + /** Expiration timestamp */ + expiresAt: number; + /** Creation timestamp */ + issuedAt: number; + /** Whether the token has been revoked */ + revoked: boolean; + }; } /** * User consent record for an OAuth client */ export interface IUserConsent { - /** Unique identifier */ id: string; - /** User who gave consent */ - userId: string; - /** OAuth client ID */ - clientId: string; - /** Scopes the user consented to */ - scopes: TOidcScope[]; - /** When consent was granted */ - grantedAt: number; - /** When consent was last updated */ - updatedAt: number; + data: { + /** User who gave consent */ + userId: string; + /** OAuth client ID */ + clientId: string; + /** Scopes the user consented to */ + scopes: TOidcScope[]; + /** When consent was granted */ + grantedAt: number; + /** When consent was last updated */ + updatedAt: number; + }; } /** diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index e763823..887e073 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@idp.global/idp.global', - version: '1.18.0', + version: '1.19.0', description: 'An identity provider software managing user authentications, registrations, and sessions.' }