diff --git a/plugins/auth-backend/src/database/OidcDatabase.test.ts b/plugins/auth-backend/src/database/OidcDatabase.test.ts index bf00159b49..65f9e9824c 100644 --- a/plugins/auth-backend/src/database/OidcDatabase.test.ts +++ b/plugins/auth-backend/src/database/OidcDatabase.test.ts @@ -41,7 +41,7 @@ describe('Oidc Database', () => { } describe.each(databases.eachSupportedId())('%p', databaseId => { - describe('Client', () => { + describe('Clients', () => { it('should create and return a client', async () => { const { oidc } = await createOidcDatabase(databaseId); @@ -94,7 +94,7 @@ describe('Oidc Database', () => { }); }); - describe('Authorization Code', () => { + describe('Authorization Codes', () => { it('should create and return an authorization code', async () => { const { oidc } = await createOidcDatabase(databaseId); @@ -195,5 +195,96 @@ describe('Oidc Database', () => { }); }); }); + + describe('Access Tokens', () => { + it('should create and return an 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.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 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 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({ + tokenId: 'test-token', + revoked: true, + }), + ).resolves.toEqual({ + ...accessToken, + revoked: true, + }); + }); + }); }); }); diff --git a/plugins/auth-backend/src/database/OidcDatabase.ts b/plugins/auth-backend/src/database/OidcDatabase.ts index ae3cfc42a6..cbaf101ed9 100644 --- a/plugins/auth-backend/src/database/OidcDatabase.ts +++ b/plugins/auth-backend/src/database/OidcDatabase.ts @@ -45,6 +45,16 @@ type OidcAuthorizationCodeRow = { used?: boolean; }; +type OidcAccessTokenRow = { + token_id: string; + client_id: string; + user_entity_ref: string; + scope: string | null; + created_at: string; + expires_at: string; + revoked?: boolean; +}; + type Client = { clientId: string; clientName: string; @@ -72,6 +82,23 @@ type AuthorizationCode = { used: boolean; }; +type AccessToken = { + tokenId: string; + clientId: string; + userEntityRef: string; + scope?: string; + createdAt: string; + expiresAt: string; + revoked?: boolean; +}; + +/** + * This class is an implementation for the Database operations that power the OIDC sign-in flow. + * + * This class provides database operations for OpenID Connect (OIDC) authentication flows. + * It manages OIDC clients, authorization codes, and access tokens in the database, as well as the consent requests + * for the frontend plugin to accept. + */ export class OidcDatabase { private constructor(private readonly db: Knex) {} @@ -161,15 +188,61 @@ export class OidcDatabase { const updatedFields = Object.fromEntries( Object.entries(row).filter(([_, value]) => value !== undefined), ); - console.log(updatedFields); - const updated = await this.db( + + const [updated] = await this.db( 'oidc_authorization_codes', ) .where('code', authorizationCode.code) .update(updatedFields) .returning('*'); - return this.rowToAuthorizationCode(updated[0]) as AuthorizationCode; + return this.rowToAuthorizationCode(updated) as AuthorizationCode; + } + + async createAccessToken(accessToken: Omit) { + const now = DateTime.now().toString(); + + 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, + 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; } private rowToClient(row: Partial): Partial { @@ -226,4 +299,32 @@ export class OidcDatabase { 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), + }; + } }