diff --git a/plugins/auth-backend/src/database/OidcDatabase.test.ts b/plugins/auth-backend/src/database/OidcDatabase.test.ts index 65f9e9824c..f9c2b83511 100644 --- a/plugins/auth-backend/src/database/OidcDatabase.test.ts +++ b/plugins/auth-backend/src/database/OidcDatabase.test.ts @@ -64,7 +64,6 @@ describe('Oidc Database', () => { scope: undefined, expiresAt: undefined, metadata: undefined, - createdAt: expect.any(String), }); }); @@ -94,11 +93,195 @@ describe('Oidc Database', () => { }); }); + describe('Authorization Sessions', () => { + it('should create and return an authorization session', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const client = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, + userEntityRef: 'user:default/blam', + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + codeChallenge: 'test-challenge', + codeChallengeMethod: 'S256', + nonce: 'test-nonce', + expiresAt: '2025-01-01T00:00:00Z', + }); + + expect(session).toEqual( + expect.objectContaining({ + id: 'test-session', + clientId: client.clientId, + userEntityRef: 'user:default/blam', + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + codeChallenge: 'test-challenge', + codeChallengeMethod: 'S256', + nonce: 'test-nonce', + expiresAt: '2025-01-01T00:00:00Z', + status: 'pending', + }), + ); + }); + + it('should allow updating the authorization session', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const client = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + expiresAt: '2025-01-01T00:00:00Z', + }); + + await expect( + oidc.updateAuthorizationSession({ + id: 'test-session', + userEntityRef: 'user:default/blam', + status: 'approved', + }), + ).resolves.toEqual({ + ...session, + userEntityRef: 'user:default/blam', + status: 'approved', + }); + }); + }); + + describe('Consent Requests', () => { + it('should create and return a consent request', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const client = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + expiresAt: '2025-01-01T00:00:00Z', + }); + + const consentRequest = await oidc.createConsentRequest({ + id: 'test-consent', + sessionId: session.id, + expiresAt: '2025-01-01T00:00:00Z', + }); + + await expect( + oidc.getConsentRequest({ id: 'test-consent' }), + ).resolves.toEqual(consentRequest); + }); + + it('should return consent request with session data', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const client = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + scope: 'openid', + state: 'test-state', + expiresAt: '2025-01-01T00:00:00Z', + }); + + const consentRequest = await oidc.createConsentRequest({ + id: 'test-consent', + sessionId: session.id, + expiresAt: '2025-01-01T00:00:00Z', + }); + + const consentFromDb = await oidc.getConsentRequest({ + id: 'test-consent', + }); + const sessionFromDb = await oidc.getAuthorizationSession({ + id: consentFromDb!.sessionId, + }); + + expect(consentFromDb).toEqual(consentRequest); + expect(sessionFromDb).toEqual(session); + }); + + it('should delete consent request', async () => { + const { oidc } = await createOidcDatabase(databaseId); + + const client = await oidc.createClient({ + clientId: 'test-client', + clientName: 'Test Client', + clientSecret: 'test-secret', + redirectUris: ['https://example.com/callback'], + responseTypes: ['code'], + grantTypes: ['authorization_code'], + }); + + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + expiresAt: '2025-01-01T00:00:00Z', + }); + + await oidc.createConsentRequest({ + id: 'test-consent', + sessionId: session.id, + expiresAt: '2025-01-01T00:00:00Z', + }); + + await oidc.deleteConsentRequest({ id: 'test-consent' }); + + await expect( + oidc.getConsentRequest({ id: 'test-consent' }), + ).resolves.toBeNull(); + }); + }); + describe('Authorization Codes', () => { it('should create and return an authorization code', async () => { const { oidc } = await createOidcDatabase(databaseId); - const mockClient = await oidc.createClient({ + const client = await oidc.createClient({ clientId: 'test-client', clientName: 'Test Client', clientSecret: 'test-secret', @@ -107,35 +290,33 @@ describe('Oidc Database', () => { grantTypes: ['authorization_code'], }); - const authorizationCode = await oidc.createAuthorizationCode({ - code: 'test-code', - clientId: mockClient.clientId, - userEntityRef: 'user:default/blam', + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, redirectUri: 'https://example.com/callback', - scope: undefined, - codeChallenge: 'test-challenge', - codeChallengeMethod: 'S256', - nonce: 'test-nonce', - expiresAt: '2025-01-01', + responseType: 'code', + expiresAt: '2025-01-01T00:00:00Z', }); - await expect( - oidc.getAuthorizationCode({ code: 'test-code' }), - ).resolves.toEqual(authorizationCode); + const authCode = await oidc.createAuthorizationCode({ + code: 'test-code', + sessionId: session.id, + expiresAt: '2025-01-01T00:00:00Z', + }); + + expect(authCode).toEqual( + expect.objectContaining({ + code: 'test-code', + sessionId: session.id, + expiresAt: '2025-01-01T00:00:00Z', + }), + ); }); - it('should return null if the authorization code does not exist', async () => { + it('should return authorization code with session data', async () => { const { oidc } = await createOidcDatabase(databaseId); - await expect( - oidc.getAuthorizationCode({ code: 'test-code' }), - ).resolves.toBeNull(); - }); - - it('should return the authorization code when created', async () => { - const { oidc } = await createOidcDatabase(databaseId); - - const mockClient = await oidc.createClient({ + const client = await oidc.createClient({ clientId: 'test-client', clientName: 'Test Client', clientSecret: 'test-secret', @@ -144,27 +325,40 @@ describe('Oidc Database', () => { grantTypes: ['authorization_code'], }); - const authorizationCode = await oidc.createAuthorizationCode({ - code: 'test-code', - clientId: mockClient.clientId, + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, userEntityRef: 'user:default/blam', redirectUri: 'https://example.com/callback', - scope: undefined, + responseType: 'code', + scope: 'openid', codeChallenge: 'test-challenge', codeChallengeMethod: 'S256', nonce: 'test-nonce', - expiresAt: '2025-01-01', + expiresAt: '2025-01-01T00:00:00Z', }); - await expect( - oidc.getAuthorizationCode({ code: 'test-code' }), - ).resolves.toEqual(authorizationCode); + const authCode = await oidc.createAuthorizationCode({ + code: 'test-code', + sessionId: session.id, + expiresAt: '2025-01-01T00:00:00Z', + }); + + const authCodeFromDb = await oidc.getAuthorizationCode({ + code: 'test-code', + }); + const sessionFromDb = await oidc.getAuthorizationSession({ + id: authCodeFromDb!.sessionId, + }); + + expect(authCodeFromDb).toEqual(authCode); + expect(sessionFromDb).toEqual(session); }); it('should allow updating the authorization code', async () => { const { oidc } = await createOidcDatabase(databaseId); - const mockClient = await oidc.createClient({ + const client = await oidc.createClient({ clientId: 'test-client', clientName: 'Test Client', clientSecret: 'test-secret', @@ -173,24 +367,27 @@ describe('Oidc Database', () => { grantTypes: ['authorization_code'], }); - const authorizationCode = await oidc.createAuthorizationCode({ - code: 'test-code', - clientId: mockClient.clientId, - userEntityRef: 'user:default/blam', + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, redirectUri: 'https://example.com/callback', - codeChallenge: 'test-challenge', - codeChallengeMethod: 'S256', - nonce: 'test-nonce', - expiresAt: '2025-01-01', + responseType: 'code', + expiresAt: '2025-01-01T00:00:00Z', }); - await expect( - oidc.updateAuthorizationCode({ - code: 'test-code', - used: true, - }), - ).resolves.toEqual({ - ...authorizationCode, + const authCode = await oidc.createAuthorizationCode({ + code: 'test-code', + sessionId: session.id, + expiresAt: '2025-01-01T00:00:00Z', + }); + + const updatedAuthCode = await oidc.updateAuthorizationCode({ + code: 'test-code', + used: true, + }); + + expect(updatedAuthCode).toEqual({ + ...authCode, used: true, }); }); @@ -200,7 +397,7 @@ describe('Oidc Database', () => { it('should create and return an access token', async () => { const { oidc } = await createOidcDatabase(databaseId); - const mockClient = await oidc.createClient({ + const client = await oidc.createClient({ clientId: 'test-client', clientName: 'Test Client', clientSecret: 'test-secret', @@ -209,81 +406,27 @@ describe('Oidc Database', () => { grantTypes: ['authorization_code'], }); - const accessToken = await oidc.createAccessToken({ - tokenId: 'test-token', - clientId: mockClient.clientId, - userEntityRef: 'user:default/blam', - expiresAt: '2025-01-01', - revoked: false, - }); - - await expect( - oidc.getAccessToken({ tokenId: 'test-token' }), - ).resolves.toEqual(accessToken); - }); - - it('should return null if the access token does not exist', async () => { - const { oidc } = await createOidcDatabase(databaseId); - - await expect( - oidc.getAccessToken({ tokenId: 'test-token' }), - ).resolves.toBeNull(); - }); - - it('should return the access token when created', async () => { - const { oidc } = await createOidcDatabase(databaseId); - - const mockClient = await oidc.createClient({ - clientId: 'test-client', - clientName: 'Test Client', - clientSecret: 'test-secret', - redirectUris: ['https://example.com/callback'], - responseTypes: ['code'], - grantTypes: ['authorization_code'], + const session = await oidc.createAuthorizationSession({ + id: 'test-session', + clientId: client.clientId, + redirectUri: 'https://example.com/callback', + responseType: 'code', + expiresAt: '2025-01-01T00:00:00Z', }); const accessToken = await oidc.createAccessToken({ tokenId: 'test-token', - clientId: mockClient.clientId, - userEntityRef: 'user:default/blam', - expiresAt: '2025-01-01', - revoked: false, + sessionId: session.id, + expiresAt: '2025-01-01T00:00:00Z', }); - await expect( - oidc.getAccessToken({ tokenId: 'test-token' }), - ).resolves.toEqual(accessToken); - }); - - it('should allow updating the access token', async () => { - const { oidc } = await createOidcDatabase(databaseId); - - const mockClient = await oidc.createClient({ - clientId: 'test-client', - clientName: 'Test Client', - clientSecret: 'test-secret', - redirectUris: ['https://example.com/callback'], - responseTypes: ['code'], - grantTypes: ['authorization_code'], - }); - - const accessToken = await oidc.createAccessToken({ - tokenId: 'test-token', - clientId: mockClient.clientId, - userEntityRef: 'user:default/blam', - expiresAt: '2025-01-01', - revoked: false, - }); - - await expect( - oidc.updateAccessToken({ + expect(accessToken).toEqual( + expect.objectContaining({ tokenId: 'test-token', - revoked: true, + sessionId: session.id, + expiresAt: '2025-01-01T00:00:00Z', }), - ).resolves.toEqual({ - ...accessToken, - revoked: true, - }); + ); }); }); }); diff --git a/plugins/auth-backend/src/database/OidcDatabase.ts b/plugins/auth-backend/src/database/OidcDatabase.ts index 12554fba57..0c75290f12 100644 --- a/plugins/auth-backend/src/database/OidcDatabase.ts +++ b/plugins/auth-backend/src/database/OidcDatabase.ts @@ -16,13 +16,10 @@ import { Knex } from 'knex'; import { AuthDatabase } from './AuthDatabase'; -import { DateTime } from 'luxon'; - type OidcClientRow = { client_id: string; client_secret: string; client_name: string; - created_at: string; expires_at: string | null; response_types: string; grant_types: string; @@ -31,31 +28,41 @@ type OidcClientRow = { metadata: string | null; }; -type OidcAuthorizationCodeRow = { - code: string; +type OAuthAuthorizationSessionRow = { + id: string; client_id: string; - user_entity_ref: string; + user_entity_ref: string | null; redirect_uri: string; scope: string | null; + state: string | null; + response_type: string; code_challenge: string | null; code_challenge_method: string | null; nonce: string | null; - created_at: string; + status: 'pending' | 'approved' | 'rejected' | 'expired'; expires_at: string; - used?: boolean; +}; + +type OidcConsentRequestRow = { + id: string; + session_id: string; + expires_at: string; +}; + +type OidcAuthorizationCodeRow = { + code: string; + session_id: string; + expires_at: string; + used: boolean; }; type OidcAccessTokenRow = { token_id: string; - client_id: string; - user_entity_ref: string; - scope: string | null; - created_at: string; + session_id: string; expires_at: string; - revoked?: boolean; }; -type Client = { +export type Client = { clientId: string; clientName: string; clientSecret: string; @@ -65,31 +72,40 @@ type Client = { scope?: string; expiresAt?: string; metadata?: Record; - createdAt: string; }; -type AuthorizationCode = { - code: string; +export type AuthorizationSession = { + id: string; clientId: string; - userEntityRef: string; + userEntityRef?: string; redirectUri: string; scope?: string; + state?: string; + responseType: string; codeChallenge?: string; codeChallengeMethod?: string; nonce?: string; - createdAt: string; + status: 'pending' | 'approved' | 'rejected' | 'expired'; + expiresAt: string; +}; + +export type ConsentRequest = { + id: string; + sessionId: string; + expiresAt: string; +}; + +export type AuthorizationCode = { + code: string; + sessionId: string; expiresAt: string; used: boolean; }; -type AccessToken = { +export type AccessToken = { tokenId: string; - clientId: string; - userEntityRef: string; - scope?: string; - createdAt: string; + sessionId: string; expiresAt: string; - revoked?: boolean; }; /** @@ -104,14 +120,11 @@ export class OidcDatabase { return new OidcDatabase(client); } - async createClient(client: Omit) { - const now = DateTime.now().toString(); - + async createClient(client: Client) { await this.db('oidc_clients').insert({ client_id: client.clientId, client_secret: client.clientSecret, client_name: client.clientName, - created_at: now, expires_at: client.expiresAt, response_types: JSON.stringify(client.responseTypes), grant_types: JSON.stringify(client.grantTypes), @@ -120,10 +133,7 @@ export class OidcDatabase { metadata: JSON.stringify(client.metadata), }); - return { - ...client, - createdAt: now, - }; + return client; } async getClient({ clientId }: { clientId: string }) { @@ -138,44 +148,122 @@ export class OidcDatabase { return this.rowToClient(client) as Client; } - async createAuthorizationCode( - authorizationCode: Omit, + async createAuthorizationSession( + session: Omit, ) { - const now = DateTime.now().toString(); + await this.db( + 'oauth_authorization_sessions', + ).insert({ + id: session.id, + client_id: session.clientId, + user_entity_ref: session.userEntityRef, + redirect_uri: session.redirectUri, + scope: session.scope, + state: session.state, + response_type: session.responseType, + code_challenge: session.codeChallenge, + code_challenge_method: session.codeChallengeMethod, + nonce: session.nonce, + status: 'pending', + expires_at: session.expiresAt, + }); + return { + ...session, + status: 'pending', + }; + } + + async updateAuthorizationSession( + session: Partial & { id: string }, + ) { + const row = this.authorizationSessionToRow(session); + const updatedFields = Object.fromEntries( + Object.entries(row).filter(([_, value]) => value !== undefined), + ); + + const [updated] = await this.db( + 'oauth_authorization_sessions', + ) + .where('id', session.id) + .update(updatedFields) + .returning('*'); + + return this.rowToAuthorizationSession(updated) as AuthorizationSession; + } + + async createConsentRequest(consentRequest: ConsentRequest) { + await this.db('oidc_consent_requests').insert({ + id: consentRequest.id, + session_id: consentRequest.sessionId, + expires_at: consentRequest.expiresAt, + }); + + return consentRequest; + } + + async getConsentRequest({ id }: { id: string }) { + const consentRequest = await this.db( + 'oidc_consent_requests', + ) + .where('id', id) + .first(); + + if (!consentRequest) { + return null; + } + + return this.rowToConsentRequest(consentRequest) as ConsentRequest; + } + + async getAuthorizationSession({ id }: { id: string }) { + const session = await this.db( + 'oauth_authorization_sessions', + ) + .where('id', id) + .first(); + + if (!session) { + return null; + } + + return this.rowToAuthorizationSession(session) as AuthorizationSession; + } + + async deleteConsentRequest({ id }: { id: string }) { + await this.db('oidc_consent_requests') + .where('id', id) + .delete(); + } + + async createAuthorizationCode( + authorizationCode: Omit, + ) { await this.db('oidc_authorization_codes').insert({ code: authorizationCode.code, - client_id: authorizationCode.clientId, - user_entity_ref: authorizationCode.userEntityRef, - redirect_uri: authorizationCode.redirectUri, - scope: authorizationCode.scope, - code_challenge: authorizationCode.codeChallenge, - code_challenge_method: authorizationCode.codeChallengeMethod, - nonce: authorizationCode.nonce, + session_id: authorizationCode.sessionId, expires_at: authorizationCode.expiresAt, - created_at: now, used: false, }); return { ...authorizationCode, - createdAt: now, used: false, }; } async getAuthorizationCode({ code }: { code: string }) { - const authorizationCode = await this.db( + const authCode = await this.db( 'oidc_authorization_codes', ) .where('code', code) .first(); - if (!authorizationCode) { + if (!authCode) { return null; } - return this.rowToAuthorizationCode(authorizationCode) as AuthorizationCode; + return this.rowToAuthorizationCode(authCode) as AuthorizationCode; } async updateAuthorizationCode( @@ -196,50 +284,14 @@ export class OidcDatabase { return this.rowToAuthorizationCode(updated) as AuthorizationCode; } - async createAccessToken(accessToken: Omit) { - const now = DateTime.now().toString(); - + async createAccessToken(accessToken: AccessToken) { await this.db('oidc_access_tokens').insert({ token_id: accessToken.tokenId, - client_id: accessToken.clientId, - user_entity_ref: accessToken.userEntityRef, - scope: accessToken.scope, - created_at: now, + session_id: accessToken.sessionId, expires_at: accessToken.expiresAt, - revoked: accessToken.revoked ?? false, }); - return { - ...accessToken, - createdAt: now, - }; - } - - async getAccessToken({ tokenId }: { tokenId: string }) { - const accessToken = await this.db('oidc_access_tokens') - .where('token_id', tokenId) - .first(); - - if (!accessToken) { - return null; - } - - return this.rowToAccessToken(accessToken) as AccessToken; - } - - async updateAccessToken( - accessToken: Partial & { tokenId: string }, - ) { - const row = this.accessTokenToRow(accessToken); - const updatedFields = Object.fromEntries( - Object.entries(row).filter(([_, value]) => value !== undefined), - ); - const [updated] = await this.db('oidc_access_tokens') - .where('token_id', accessToken.tokenId) - .update(updatedFields) - .returning('*'); - - return this.rowToAccessToken(updated) as AccessToken; + return accessToken; } private rowToClient(row: Partial): Partial { @@ -257,7 +309,54 @@ export class OidcDatabase { scope: row.scope ?? undefined, expiresAt: row.expires_at ?? undefined, metadata: row.metadata ? JSON.parse(row.metadata) : undefined, - createdAt: row.created_at, + }; + } + + private authorizationSessionToRow( + session: Partial, + ): Partial { + return { + id: session.id, + client_id: session.clientId, + user_entity_ref: session.userEntityRef, + redirect_uri: session.redirectUri, + scope: session.scope, + state: session.state, + response_type: session.responseType, + code_challenge: session.codeChallenge, + code_challenge_method: session.codeChallengeMethod, + nonce: session.nonce, + status: session.status, + expires_at: session.expiresAt, + }; + } + + private rowToAuthorizationSession( + row: Partial, + ): Partial { + return { + id: row.id, + clientId: row.client_id, + userEntityRef: row.user_entity_ref ?? undefined, + redirectUri: row.redirect_uri, + scope: row.scope ?? undefined, + state: row.state ?? undefined, + responseType: row.response_type, + codeChallenge: row.code_challenge ?? undefined, + codeChallengeMethod: row.code_challenge_method ?? undefined, + nonce: row.nonce ?? undefined, + status: row.status, + expiresAt: row.expires_at, + }; + } + + private rowToConsentRequest( + row: Partial, + ): Partial { + return { + id: row.id, + sessionId: row.session_id, + expiresAt: row.expires_at, }; } @@ -266,14 +365,7 @@ export class OidcDatabase { ): Partial { return { code: authorizationCode.code, - client_id: authorizationCode.clientId, - user_entity_ref: authorizationCode.userEntityRef, - redirect_uri: authorizationCode.redirectUri, - scope: authorizationCode.scope, - code_challenge: authorizationCode.codeChallenge, - code_challenge_method: authorizationCode.codeChallengeMethod, - nonce: authorizationCode.nonce, - created_at: authorizationCode.createdAt, + session_id: authorizationCode.sessionId, expires_at: authorizationCode.expiresAt, used: authorizationCode.used, }; @@ -284,44 +376,9 @@ export class OidcDatabase { ): Partial { return { code: row.code, - clientId: row.client_id, - userEntityRef: row.user_entity_ref, - redirectUri: row.redirect_uri, - scope: row.scope ?? undefined, - codeChallenge: row.code_challenge ?? undefined, - codeChallengeMethod: row.code_challenge_method ?? undefined, - nonce: row.nonce ?? undefined, - createdAt: row.created_at, + sessionId: row.session_id, expiresAt: row.expires_at, used: Boolean(row.used), }; } - - private accessTokenToRow( - accessToken: Partial, - ): Partial { - return { - token_id: accessToken.tokenId, - client_id: accessToken.clientId, - user_entity_ref: accessToken.userEntityRef, - scope: accessToken.scope, - created_at: accessToken.createdAt, - expires_at: accessToken.expiresAt, - revoked: accessToken.revoked, - }; - } - - private rowToAccessToken( - row: Partial, - ): Partial { - return { - tokenId: row.token_id, - clientId: row.client_id, - userEntityRef: row.user_entity_ref, - scope: row.scope ?? undefined, - createdAt: row.created_at, - expiresAt: row.expires_at, - revoked: Boolean(row.revoked), - }; - } }