Compare commits

...

2 Commits

Author SHA1 Message Date
jkunz 525a72b73b v1.19.0
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-20 08:40:40 +00:00
jkunz d913dfaeb1 feat(oidc): persist hashed OIDC tokens, authorization codes, and user consent in smartdata storage 2026-04-20 08:40:40 +00:00
12 changed files with 458 additions and 144 deletions
+9
View File
@@ -1,5 +1,14 @@
# Changelog # 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) ## 2026-04-20 - 1.18.0 - feat(reception)
persist email action tokens and registration sessions for authentication and signup flows persist email action tokens and registration sessions for authentication and signup flows
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@idp.global/idp.global", "name": "@idp.global/idp.global",
"version": "1.18.0", "version": "1.19.0",
"description": "An identity provider software managing user authentications, registrations, and sessions.", "description": "An identity provider software managing user authentications, registrations, and sessions.",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts", "typings": "dist_ts/index.d.ts",
+76
View File
@@ -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<void> }).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<void> }).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<void> }).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();
+1 -1
View File
@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@idp.global/idp.global', name: '@idp.global/idp.global',
version: '1.18.0', version: '1.19.0',
description: 'An identity provider software managing user authentications, registrations, and sessions.' description: 'An identity provider software managing user authentications, registrations, and sessions.'
} }
+34
View File
@@ -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);
}
}
@@ -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();
}
}
+154 -82
View File
@@ -1,6 +1,10 @@
import * as plugins from '../plugins.js'; import * as plugins from '../plugins.js';
import type { Reception } from './classes.reception.js'; import type { Reception } from './classes.reception.js';
import type { App } from './classes.app.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 * OidcManager handles OpenID Connect (OIDC) server functionality
@@ -12,25 +16,31 @@ export class OidcManager {
return this.receptionRef.db.smartdataDb; return this.receptionRef.db.smartdataDb;
} }
// In-memory store for authorization codes (short-lived, 10 min TTL) public COidcAuthorizationCode = plugins.smartdata.setDefaultManagerForDoc(
private authorizationCodes = new Map<string, plugins.idpInterfaces.data.IAuthorizationCode>(); this,
OidcAuthorizationCode
);
// In-memory store for access tokens (for validation) public COidcAccessToken = plugins.smartdata.setDefaultManagerForDoc(this, OidcAccessToken);
private accessTokens = new Map<string, plugins.idpInterfaces.data.IOidcAccessToken>();
// In-memory store for refresh tokens public COidcRefreshToken = plugins.smartdata.setDefaultManagerForDoc(this, OidcRefreshToken);
private refreshTokens = new Map<string, plugins.idpInterfaces.data.IOidcRefreshToken>();
// In-memory store for user consents (should be persisted later) public COidcUserConsent = plugins.smartdata.setDefaultManagerForDoc(this, OidcUserConsent);
private userConsents = new Map<string, plugins.idpInterfaces.data.IUserConsent>();
private cleanupInterval: ReturnType<typeof setInterval> | null = null;
constructor(receptionRefArg: Reception) { constructor(receptionRefArg: Reception) {
this.receptionRef = receptionRefArg; this.receptionRef = receptionRefArg;
// Start cleanup task for expired codes/tokens
this.startCleanupTask(); this.startCleanupTask();
} }
public async stop() {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
}
/** /**
* Get the OIDC Discovery Document * Get the OIDC Discovery Document
*/ */
@@ -174,9 +184,11 @@ export class OidcManager {
codeChallenge?: string, codeChallenge?: string,
nonce?: string nonce?: string
): Promise<string> { ): Promise<string> {
const code = plugins.smartunique.shortId(32); const code = this.createOpaqueToken();
const authCode: plugins.idpInterfaces.data.IAuthorizationCode = { const authCode = new OidcAuthorizationCode();
code, authCode.id = plugins.smartunique.shortId(12);
authCode.data = {
codeHash: OidcAuthorizationCode.hashCode(code),
clientId, clientId,
userId, userId,
scopes, scopes,
@@ -184,11 +196,13 @@ export class OidcManager {
codeChallenge, codeChallenge,
codeChallengeMethod: codeChallenge ? 'S256' : undefined, codeChallengeMethod: codeChallenge ? 'S256' : undefined,
nonce, nonce,
expiresAt: Date.now() + 10 * 60 * 1000, // 10 minutes expiresAt: Date.now() + 10 * 60 * 1000,
issuedAt: Date.now(),
used: false, used: false,
}; };
this.authorizationCodes.set(code, authCode); await authCode.save();
await this.upsertUserConsent(userId, clientId, scopes);
return code; return code;
} }
@@ -261,50 +275,48 @@ export class OidcManager {
} }
// Find and validate authorization code // Find and validate authorization code
const authCode = this.authorizationCodes.get(code); const authCode = await this.getAuthorizationCodeByCode(code);
if (!authCode) { if (!authCode) {
return this.tokenErrorResponse('invalid_grant', 'Invalid authorization code'); return this.tokenErrorResponse('invalid_grant', 'Invalid authorization code');
} }
if (authCode.used) { if (authCode.data.used) {
// Code reuse attack - revoke all tokens for this code
this.authorizationCodes.delete(code);
return this.tokenErrorResponse('invalid_grant', 'Authorization code already used'); return this.tokenErrorResponse('invalid_grant', 'Authorization code already used');
} }
if (authCode.expiresAt < Date.now()) { if (authCode.isExpired()) {
this.authorizationCodes.delete(code); await authCode.delete();
return this.tokenErrorResponse('invalid_grant', 'Authorization code expired'); 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'); 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'); return this.tokenErrorResponse('invalid_grant', 'Redirect URI mismatch');
} }
// Verify PKCE if code challenge was used // Verify PKCE if code challenge was used
if (authCode.codeChallenge) { if (authCode.data.codeChallenge) {
if (!codeVerifier) { if (!codeVerifier) {
return this.tokenErrorResponse('invalid_grant', 'Code verifier required'); return this.tokenErrorResponse('invalid_grant', 'Code verifier required');
} }
const expectedChallenge = this.generateS256Challenge(codeVerifier); const expectedChallenge = this.generateS256Challenge(codeVerifier);
if (expectedChallenge !== authCode.codeChallenge) { if (expectedChallenge !== authCode.data.codeChallenge) {
return this.tokenErrorResponse('invalid_grant', 'Invalid code verifier'); return this.tokenErrorResponse('invalid_grant', 'Invalid code verifier');
} }
} }
// Mark code as used // Mark code as used
authCode.used = true; await authCode.markUsed();
// Generate tokens // Generate tokens
const tokens = await this.generateTokens( const tokens = await this.generateTokens(
authCode.userId, authCode.data.userId,
app.data.oauthCredentials.clientId, app.data.oauthCredentials.clientId,
authCode.scopes, authCode.data.scopes,
authCode.nonce authCode.data.nonce
); );
return new Response(JSON.stringify(tokens), { return new Response(JSON.stringify(tokens), {
@@ -330,31 +342,30 @@ export class OidcManager {
return this.tokenErrorResponse('invalid_request', 'Missing refresh_token'); return this.tokenErrorResponse('invalid_request', 'Missing refresh_token');
} }
const tokenHash = await plugins.smarthash.sha256FromString(refreshToken); const storedToken = await this.getRefreshTokenByToken(refreshToken);
const storedToken = this.refreshTokens.get(tokenHash);
if (!storedToken) { if (!storedToken) {
return this.tokenErrorResponse('invalid_grant', 'Invalid refresh token'); 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'); return this.tokenErrorResponse('invalid_grant', 'Refresh token has been revoked');
} }
if (storedToken.expiresAt < Date.now()) { if (storedToken.isExpired()) {
this.refreshTokens.delete(tokenHash); await storedToken.delete();
return this.tokenErrorResponse('invalid_grant', 'Refresh token expired'); 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'); return this.tokenErrorResponse('invalid_grant', 'Client ID mismatch');
} }
// Generate new tokens (without new refresh token by default) // Generate new tokens (without new refresh token by default)
const tokens = await this.generateTokens( const tokens = await this.generateTokens(
storedToken.userId, storedToken.data.userId,
storedToken.clientId, storedToken.data.clientId,
storedToken.scopes, storedToken.data.scopes,
undefined, undefined,
false // Don't generate new refresh token false // Don't generate new refresh token
); );
@@ -384,18 +395,18 @@ export class OidcManager {
const refreshTokenLifetime = 30 * 24 * 3600; // 30 days const refreshTokenLifetime = 30 * 24 * 3600; // 30 days
// Generate access token // Generate access token
const accessToken = plugins.smartunique.shortId(32); const accessToken = this.createOpaqueToken();
const accessTokenHash = await plugins.smarthash.sha256FromString(accessToken); const accessTokenData = new OidcAccessToken();
const accessTokenData: plugins.idpInterfaces.data.IOidcAccessToken = { accessTokenData.id = plugins.smartunique.shortId(12);
id: plugins.smartunique.shortId(8), accessTokenData.data = {
tokenHash: accessTokenHash, tokenHash: OidcAccessToken.hashToken(accessToken),
clientId, clientId,
userId, userId,
scopes, scopes,
expiresAt: now + accessTokenLifetime * 1000, expiresAt: now + accessTokenLifetime * 1000,
issuedAt: now, issuedAt: now,
}; };
this.accessTokens.set(accessTokenHash, accessTokenData); await accessTokenData.save();
// Generate ID token (JWT) // Generate ID token (JWT)
const idToken = await this.generateIdToken(userId, clientId, scopes, nonce); const idToken = await this.generateIdToken(userId, clientId, scopes, nonce);
@@ -410,11 +421,11 @@ export class OidcManager {
// Generate refresh token if requested // Generate refresh token if requested
if (includeRefreshToken) { if (includeRefreshToken) {
const refreshToken = plugins.smartunique.shortId(48); const refreshToken = this.createOpaqueToken(48);
const refreshTokenHash = await plugins.smarthash.sha256FromString(refreshToken); const refreshTokenData = new OidcRefreshToken();
const refreshTokenData: plugins.idpInterfaces.data.IOidcRefreshToken = { refreshTokenData.id = plugins.smartunique.shortId(12);
id: plugins.smartunique.shortId(8), refreshTokenData.data = {
tokenHash: refreshTokenHash, tokenHash: OidcRefreshToken.hashToken(refreshToken),
clientId, clientId,
userId, userId,
scopes, scopes,
@@ -422,7 +433,7 @@ export class OidcManager {
issuedAt: now, issuedAt: now,
revoked: false, revoked: false,
}; };
this.refreshTokens.set(refreshTokenHash, refreshTokenData); await refreshTokenData.save();
response.refresh_token = refreshToken; response.refresh_token = refreshToken;
} }
@@ -482,8 +493,7 @@ export class OidcManager {
} }
const accessToken = authHeader.substring(7); const accessToken = authHeader.substring(7);
const tokenHash = await plugins.smarthash.sha256FromString(accessToken); const tokenData = await this.getAccessTokenByToken(accessToken);
const tokenData = this.accessTokens.get(tokenHash);
if (!tokenData) { if (!tokenData) {
return new Response(JSON.stringify({ error: 'invalid_token' }), { return new Response(JSON.stringify({ error: 'invalid_token' }), {
@@ -495,8 +505,8 @@ export class OidcManager {
}); });
} }
if (tokenData.expiresAt < Date.now()) { if (tokenData.isExpired()) {
this.accessTokens.delete(tokenHash); await tokenData.delete();
return new Response(JSON.stringify({ error: 'invalid_token', error_description: 'Token expired' }), { return new Response(JSON.stringify({ error: 'invalid_token', error_description: 'Token expired' }), {
status: 401, status: 401,
headers: { headers: {
@@ -507,7 +517,7 @@ export class OidcManager {
} }
// Get user claims based on token scopes // 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), { return new Response(JSON.stringify(userInfo), {
status: 200, status: 200,
@@ -583,21 +593,20 @@ export class OidcManager {
return new Response(null, { status: 200 }); // Spec says always return 200 return new Response(null, { status: 200 }); // Spec says always return 200
} }
const tokenHash = await plugins.smarthash.sha256FromString(token);
// Try to revoke as refresh token // Try to revoke as refresh token
if (!tokenTypeHint || tokenTypeHint === 'refresh_token') { if (!tokenTypeHint || tokenTypeHint === 'refresh_token') {
const refreshToken = this.refreshTokens.get(tokenHash); const refreshToken = await this.getRefreshTokenByToken(token);
if (refreshToken) { if (refreshToken) {
refreshToken.revoked = true; await refreshToken.revoke();
return new Response(null, { status: 200 }); return new Response(null, { status: 200 });
} }
} }
// Try to revoke as access token // Try to revoke as access token
if (!tokenTypeHint || tokenTypeHint === 'access_token') { if (!tokenTypeHint || tokenTypeHint === 'access_token') {
if (this.accessTokens.has(tokenHash)) { const accessToken = await this.getAccessTokenByToken(token);
this.accessTokens.delete(tokenHash); if (accessToken) {
await accessToken.delete();
return new Response(null, { status: 200 }); return new Response(null, { status: 200 });
} }
} }
@@ -616,6 +625,53 @@ export class OidcManager {
return apps[0] || null; 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 * Generate S256 PKCE challenge from verifier
*/ */
@@ -655,29 +711,45 @@ export class OidcManager {
* Start cleanup task for expired tokens/codes * Start cleanup task for expired tokens/codes
*/ */
private startCleanupTask(): void { private startCleanupTask(): void {
setInterval(() => { this.cleanupInterval = setInterval(() => {
const now = Date.now(); void this.cleanupExpiredOidcState();
}, 60 * 1000);
}
// Clean up expired authorization codes private async cleanupExpiredOidcState() {
for (const [code, data] of this.authorizationCodes) { const now = Date.now();
if (data.expiresAt < now) {
this.authorizationCodes.delete(code);
}
}
// Clean up expired access tokens const expiredAuthorizationCodes = await this.COidcAuthorizationCode.getInstances({
for (const [hash, data] of this.accessTokens) { data: {
if (data.expiresAt < now) { expiresAt: {
this.accessTokens.delete(hash); $lt: now,
} } as any,
} },
});
for (const authCode of expiredAuthorizationCodes) {
await authCode.delete();
}
// Clean up expired refresh tokens const expiredAccessTokens = await this.COidcAccessToken.getInstances({
for (const [hash, data] of this.refreshTokens) { data: {
if (data.expiresAt < now) { expiresAt: {
this.refreshTokens.delete(hash); $lt: now,
} } as any,
} },
}, 60 * 1000); // Run every minute });
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();
}
} }
} }
+40
View File
@@ -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();
}
}
+30
View File
@@ -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();
}
}
+1
View File
@@ -78,6 +78,7 @@ export class Reception {
*/ */
public async stop() { public async stop() {
await this.housekeeping.stop(); await this.housekeeping.stop();
await this.oidcManager.stop();
console.log('stopped serviceserver!'); console.log('stopped serviceserver!');
await this.db.stop(); await this.db.stop();
} }
+67 -59
View File
@@ -11,86 +11,94 @@ export type TOidcScope = 'openid' | 'profile' | 'email' | 'organizations' | 'rol
* Authorization code for OAuth 2.0 authorization code flow * Authorization code for OAuth 2.0 authorization code flow
*/ */
export interface IAuthorizationCode { export interface IAuthorizationCode {
/** The authorization code string */ id: string;
code: string; data: {
/** OAuth client ID */ /** Hashed authorization code string */
clientId: string; codeHash: string;
/** User ID who authorized */ /** OAuth client ID */
userId: string; clientId: string;
/** Scopes granted */ /** User ID who authorized */
scopes: TOidcScope[]; userId: string;
/** Redirect URI used in authorization request */ /** Scopes granted */
redirectUri: string; scopes: TOidcScope[];
/** PKCE code challenge (S256 hashed) */ /** Redirect URI used in authorization request */
codeChallenge?: string; redirectUri: string;
/** PKCE code challenge method */ /** PKCE code challenge (S256 hashed) */
codeChallengeMethod?: 'S256'; codeChallenge?: string;
/** Nonce from authorization request (for ID token) */ /** PKCE code challenge method */
nonce?: string; codeChallengeMethod?: 'S256';
/** Expiration timestamp (10 minutes from creation) */ /** Nonce from authorization request (for ID token) */
expiresAt: number; nonce?: string;
/** Whether the code has been used (single-use) */ /** Expiration timestamp (10 minutes from creation) */
used: boolean; expiresAt: number;
/** Creation timestamp */
issuedAt: number;
/** Whether the code has been used (single-use) */
used: boolean;
};
} }
/** /**
* OIDC Access Token (opaque or JWT) * OIDC Access Token (opaque or JWT)
*/ */
export interface IOidcAccessToken { export interface IOidcAccessToken {
/** Token identifier */
id: string; id: string;
/** The access token string (or hash for storage) */ data: {
tokenHash: string; /** The access token string hash for storage */
/** OAuth client ID */ tokenHash: string;
clientId: string; /** OAuth client ID */
/** User ID */ clientId: string;
userId: string; /** User ID */
/** Granted scopes */ userId: string;
scopes: TOidcScope[]; /** Granted scopes */
/** Expiration timestamp */ scopes: TOidcScope[];
expiresAt: number; /** Expiration timestamp */
/** Creation timestamp */ expiresAt: number;
issuedAt: number; /** Creation timestamp */
issuedAt: number;
};
} }
/** /**
* OIDC Refresh Token * OIDC Refresh Token
*/ */
export interface IOidcRefreshToken { export interface IOidcRefreshToken {
/** Token identifier */
id: string; id: string;
/** The refresh token string (or hash for storage) */ data: {
tokenHash: string; /** The refresh token string hash for storage */
/** OAuth client ID */ tokenHash: string;
clientId: string; /** OAuth client ID */
/** User ID */ clientId: string;
userId: string; /** User ID */
/** Granted scopes */ userId: string;
scopes: TOidcScope[]; /** Granted scopes */
/** Expiration timestamp */ scopes: TOidcScope[];
expiresAt: number; /** Expiration timestamp */
/** Creation timestamp */ expiresAt: number;
issuedAt: number; /** Creation timestamp */
/** Whether the token has been revoked */ issuedAt: number;
revoked: boolean; /** Whether the token has been revoked */
revoked: boolean;
};
} }
/** /**
* User consent record for an OAuth client * User consent record for an OAuth client
*/ */
export interface IUserConsent { export interface IUserConsent {
/** Unique identifier */
id: string; id: string;
/** User who gave consent */ data: {
userId: string; /** User who gave consent */
/** OAuth client ID */ userId: string;
clientId: string; /** OAuth client ID */
/** Scopes the user consented to */ clientId: string;
scopes: TOidcScope[]; /** Scopes the user consented to */
/** When consent was granted */ scopes: TOidcScope[];
grantedAt: number; /** When consent was granted */
/** When consent was last updated */ grantedAt: number;
updatedAt: number; /** When consent was last updated */
updatedAt: number;
};
} }
/** /**
+1 -1
View File
@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@idp.global/idp.global', name: '@idp.global/idp.global',
version: '1.18.0', version: '1.19.0',
description: 'An identity provider software managing user authentications, registrations, and sessions.' description: 'An identity provider software managing user authentications, registrations, and sessions.'
} }