diff --git a/plugins/auth-backend/migrations/20250701120000_oidc_client_registration.js b/plugins/auth-backend/migrations/20250701120000_oidc_client_registration.js index bf8ee521f0..e175c922c1 100644 --- a/plugins/auth-backend/migrations/20250701120000_oidc_client_registration.js +++ b/plugins/auth-backend/migrations/20250701120000_oidc_client_registration.js @@ -43,11 +43,6 @@ exports.up = async function up(knex) { .notNullable() .comment('The name of the client, should be human readable'); - table - .timestamp('expires_at', { useTz: false, precision: 0 }) - .nullable() - .comment('Client registration expiration timestamp'); - table .text('response_types') .notNullable() @@ -110,7 +105,7 @@ exports.up = async function up(knex) { .comment('Authorization session status'); table - .timestamp('expires_at', { useTz: false, precision: 0 }) + .timestamp('expires_at', { useTz: true, precision: 0 }) .notNullable() .comment('Session expiration timestamp'); @@ -119,32 +114,6 @@ exports.up = async function up(knex) { table.index(['status', 'expires_at']); }); - await knex.schema.createTable('oidc_consent_requests', table => { - table.comment('User consent requests for OAuth authorization'); - - table - .string('id') - .primary() - .notNullable() - .comment('Unique consent request identifier'); - - table - .string('session_id') - .notNullable() - .comment('Authorization session identifier'); - - table - .timestamp('expires_at', { useTz: false, precision: 0 }) - .notNullable() - .comment('Consent request expiration timestamp'); - - table - .foreign('session_id') - .references('id') - .inTable('oauth_authorization_sessions') - .onDelete('CASCADE'); - }); - await knex.schema.createTable('oidc_authorization_codes', table => { table.comment('OAuth authorization codes for code exchange flow'); @@ -160,7 +129,7 @@ exports.up = async function up(knex) { .comment('Authorization session identifier'); table - .timestamp('expires_at', { useTz: false, precision: 0 }) + .timestamp('expires_at', { useTz: true, precision: 0 }) .notNullable() .comment('Authorization code expiration timestamp'); @@ -175,41 +144,13 @@ exports.up = async function up(knex) { .inTable('oauth_authorization_sessions') .onDelete('CASCADE'); }); - - await knex.schema.createTable('oidc_access_tokens', table => { - table.comment('OAuth access tokens for API access'); - - table - .string('token_id') - .primary() - .notNullable() - .comment('Unique access token identifier'); - - table - .string('session_id') - .notNullable() - .comment('Authorization session identifier'); - - table - .timestamp('expires_at', { useTz: false, precision: 0 }) - .notNullable() - .comment('Access token expiration timestamp'); - - table - .foreign('session_id') - .references('id') - .inTable('oauth_authorization_sessions') - .onDelete('CASCADE'); - }); }; /** * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { - await knex.schema.dropTable('oidc_access_tokens'); await knex.schema.dropTable('oidc_authorization_codes'); - await knex.schema.dropTable('oidc_consent_requests'); await knex.schema.dropTable('oauth_authorization_sessions'); await knex.schema.dropTable('oidc_clients'); }; diff --git a/plugins/auth-backend/src/database/OidcDatabase.test.ts b/plugins/auth-backend/src/database/OidcDatabase.test.ts index f9c2b83511..82064361a5 100644 --- a/plugins/auth-backend/src/database/OidcDatabase.test.ts +++ b/plugins/auth-backend/src/database/OidcDatabase.test.ts @@ -172,111 +172,6 @@ describe('Oidc Database', () => { }); }); - 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); @@ -392,42 +287,5 @@ describe('Oidc Database', () => { }); }); }); - - describe('Access Tokens', () => { - it('should create and return an access token', 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 accessToken = await oidc.createAccessToken({ - tokenId: 'test-token', - sessionId: session.id, - expiresAt: '2025-01-01T00:00:00Z', - }); - - expect(accessToken).toEqual( - expect.objectContaining({ - tokenId: 'test-token', - sessionId: session.id, - expiresAt: '2025-01-01T00:00:00Z', - }), - ); - }); - }); }); }); diff --git a/plugins/auth-backend/src/database/OidcDatabase.ts b/plugins/auth-backend/src/database/OidcDatabase.ts index 0c75290f12..17dd4f2c06 100644 --- a/plugins/auth-backend/src/database/OidcDatabase.ts +++ b/plugins/auth-backend/src/database/OidcDatabase.ts @@ -20,7 +20,6 @@ type OidcClientRow = { client_id: string; client_secret: string; client_name: string; - expires_at: string | null; response_types: string; grant_types: string; redirect_uris: string; @@ -43,12 +42,6 @@ type OAuthAuthorizationSessionRow = { expires_at: string; }; -type OidcConsentRequestRow = { - id: string; - session_id: string; - expires_at: string; -}; - type OidcAuthorizationCodeRow = { code: string; session_id: string; @@ -56,12 +49,6 @@ type OidcAuthorizationCodeRow = { used: boolean; }; -type OidcAccessTokenRow = { - token_id: string; - session_id: string; - expires_at: string; -}; - export type Client = { clientId: string; clientName: string; @@ -70,7 +57,6 @@ export type Client = { responseTypes: string[]; grantTypes: string[]; scope?: string; - expiresAt?: string; metadata?: Record; }; @@ -125,7 +111,6 @@ export class OidcDatabase { client_id: client.clientId, client_secret: client.clientSecret, client_name: client.clientName, - expires_at: client.expiresAt, response_types: JSON.stringify(client.responseTypes), grant_types: JSON.stringify(client.grantTypes), redirect_uris: JSON.stringify(client.redirectUris), @@ -192,30 +177,6 @@ export class OidcDatabase { 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', @@ -230,12 +191,6 @@ export class OidcDatabase { 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, ) { @@ -284,16 +239,6 @@ export class OidcDatabase { return this.rowToAuthorizationCode(updated) as AuthorizationCode; } - async createAccessToken(accessToken: AccessToken) { - await this.db('oidc_access_tokens').insert({ - token_id: accessToken.tokenId, - session_id: accessToken.sessionId, - expires_at: accessToken.expiresAt, - }); - - return accessToken; - } - private rowToClient(row: Partial): Partial { return { clientId: row.client_id, @@ -307,7 +252,6 @@ export class OidcDatabase { : undefined, grantTypes: row.grant_types ? JSON.parse(row.grant_types) : undefined, scope: row.scope ?? undefined, - expiresAt: row.expires_at ?? undefined, metadata: row.metadata ? JSON.parse(row.metadata) : undefined, }; } @@ -350,16 +294,6 @@ export class OidcDatabase { }; } - private rowToConsentRequest( - row: Partial, - ): Partial { - return { - id: row.id, - sessionId: row.session_id, - expiresAt: row.expires_at, - }; - } - private authorizationCodeToRow( authorizationCode: Partial, ): Partial { diff --git a/plugins/auth-backend/src/service/OidcRouter.test.ts b/plugins/auth-backend/src/service/OidcRouter.test.ts index 150065ede5..579c96470f 100644 --- a/plugins/auth-backend/src/service/OidcRouter.test.ts +++ b/plugins/auth-backend/src/service/OidcRouter.test.ts @@ -24,6 +24,7 @@ import { startTestBackend, TestDatabases, TestDatabaseId, + mockCredentials, } from '@backstage/backend-test-utils'; import request from 'supertest'; import crypto from 'crypto'; @@ -35,6 +36,8 @@ import { OidcService } from '../service/OidcService'; import { TokenIssuer } from '../identity/types'; describe('OidcRouter', () => { + const MOCK_USER_TOKEN = 'mock-user-token'; + const MOCK_USER_ENTITY_REF = 'user:default/test-user'; const databases = TestDatabases.create(); async function createRouter(databaseId: TestDatabaseId) { @@ -82,6 +85,9 @@ describe('OidcRouter', () => { logger: mockServices.logger.mock(), userInfo: userInfoDatabase, oidc: oidcDatabase, + httpAuth: mockServices.httpAuth({ + defaultCredentials: mockCredentials.user(), + }), }); return { @@ -209,7 +215,7 @@ describe('OidcRouter', () => { }); }); - describe('consent flow', () => { + describe('auth flow', () => { it('should register a client', async () => { const { router } = await createRouter(databaseId); @@ -251,7 +257,7 @@ describe('OidcRouter', () => { }); }); - it('should create a consent request via authorization endpoint', async () => { + it('should create an authorization session via authorization endpoint', async () => { const { mocks: { service }, router, @@ -297,11 +303,11 @@ describe('OidcRouter', () => { .expect(302); expect(response.header.location).toMatch( - /^http:\/\/localhost:3000\/auth\/consent\/[a-f0-9-]+$/, + /^http:\/\/localhost:3000\/auth\/sessions\/[a-f0-9-]+$/, ); }); - it('should get consent request details', async () => { + it('should get auth session details', async () => { const { mocks: { service }, router, @@ -315,7 +321,7 @@ describe('OidcRouter', () => { scope: 'openid', }); - const consentRequest = await service.createConsentRequest({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', @@ -344,11 +350,11 @@ describe('OidcRouter', () => { }); const response = await request(server) - .get(`/api/auth/v1/consent/${consentRequest.consentRequestId}`) + .get(`/api/auth/v1/sessions/${authSession.id}`) .expect(200); expect(response.body).toEqual({ - id: consentRequest.consentRequestId, + id: authSession.id, clientName: 'Test Client', scope: 'openid', redirectUri: 'https://example.com/callback', @@ -369,7 +375,7 @@ describe('OidcRouter', () => { scope: 'openid', }); - const consentRequest = await service.createConsentRequest({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', @@ -397,21 +403,11 @@ describe('OidcRouter', () => { ], }); - auth.authenticate.mockResolvedValueOnce({ - principal: { - type: 'user', - userEntityRef: 'user:default/test-user', - }, - $$type: '@backstage/BackstageCredentials', - }); - auth.isPrincipal.mockReturnValueOnce(true); const response = await request(server) - .post( - `/api/auth/v1/consent/${consentRequest.consentRequestId}/approve`, - ) - .set('Authorization', 'Bearer test-token') + .post(`/api/auth/v1/sessions/${authSession.id}/approve`) + .set('Authorization', `Bearer ${MOCK_USER_TOKEN}`) .expect(200); expect(response.body).toEqual({ @@ -421,7 +417,7 @@ describe('OidcRouter', () => { }); }); - it('should reject consent request', async () => { + it('should reject auth session', async () => { const { mocks: { service }, router, @@ -435,7 +431,7 @@ describe('OidcRouter', () => { scope: 'openid', }); - const consentRequest = await service.createConsentRequest({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', @@ -464,9 +460,7 @@ describe('OidcRouter', () => { }); const response = await request(server) - .post( - `/api/auth/v1/consent/${consentRequest.consentRequestId}/reject`, - ) + .post(`/api/auth/v1/sessions/${authSession.id}/reject`) .expect(200); expect(response.body).toEqual({ @@ -505,7 +499,7 @@ describe('OidcRouter', () => { scope: 'openid', }); - const consentRequest = await service.createConsentRequest({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', @@ -534,10 +528,8 @@ describe('OidcRouter', () => { }); const approvalResponse = await request(server) - .post( - `/api/auth/v1/consent/${consentRequest.consentRequestId}/approve`, - ) - .set('Authorization', 'Bearer test-token') + .post(`/api/auth/v1/sessions/${authSession.id}/approve`) + .set('Authorization', `Bearer ${MOCK_USER_TOKEN}`) .expect(200); const redirectUrl = new URL(approvalResponse.body.redirectUrl); @@ -566,7 +558,7 @@ describe('OidcRouter', () => { expect(tokenIssuer.issueToken).toHaveBeenCalledWith({ claims: { - sub: 'user:default/test-user', + sub: MOCK_USER_ENTITY_REF, }, }); }); @@ -602,7 +594,7 @@ describe('OidcRouter', () => { 'test-code-verifier-123456789012345678901234567890123456789012345'; const codeChallenge = codeVerifier; - const consentRequest = await service.createConsentRequest({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', @@ -633,10 +625,8 @@ describe('OidcRouter', () => { }); const approvalResponse = await request(server) - .post( - `/api/auth/v1/consent/${consentRequest.consentRequestId}/approve`, - ) - .set('Authorization', 'Bearer test-token') + .post(`/api/auth/v1/sessions/${authSession.id}/approve`) + .set('Authorization', `Bearer ${MOCK_USER_TOKEN}`) .expect(200); const redirectUrl = new URL(approvalResponse.body.redirectUrl); @@ -666,7 +656,7 @@ describe('OidcRouter', () => { expect(tokenIssuer.issueToken).toHaveBeenCalledWith({ claims: { - sub: 'user:default/test-user-pkce', + sub: MOCK_USER_ENTITY_REF, }, }); }); @@ -694,7 +684,7 @@ describe('OidcRouter', () => { scope: 'openid', }); - const consentRequest = await service.createConsentRequest({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', @@ -722,10 +712,8 @@ describe('OidcRouter', () => { }); const approvalResponse = await request(server) - .post( - `/api/auth/v1/consent/${consentRequest.consentRequestId}/approve`, - ) - .set('Authorization', 'Bearer test-token') + .post(`/api/auth/v1/sessions/${authSession.id}/approve`) + .set('Authorization', `Bearer ${MOCK_USER_TOKEN}`) .expect(200); const redirectUrl = new URL(approvalResponse.body.redirectUrl); @@ -833,7 +821,7 @@ describe('OidcRouter', () => { .update(codeVerifier) .digest('base64url'); - const consentRequest = await service.createConsentRequest({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', @@ -864,10 +852,8 @@ describe('OidcRouter', () => { }); const approvalResponse = await request(server) - .post( - `/api/auth/v1/consent/${consentRequest.consentRequestId}/approve`, - ) - .set('Authorization', 'Bearer test-token') + .post(`/api/auth/v1/sessions/${authSession.id}/approve`) + .set('Authorization', `Bearer ${MOCK_USER_TOKEN}`) .expect(200); const redirectUrl = new URL(approvalResponse.body.redirectUrl); diff --git a/plugins/auth-backend/src/service/OidcRouter.ts b/plugins/auth-backend/src/service/OidcRouter.ts index c7bb42e1f5..c07ffd5adb 100644 --- a/plugins/auth-backend/src/service/OidcRouter.ts +++ b/plugins/auth-backend/src/service/OidcRouter.ts @@ -16,7 +16,11 @@ import Router from 'express-promise-router'; import { OidcService } from './OidcService'; import { AuthenticationError, isError } from '@backstage/errors'; -import { AuthService, LoggerService } from '@backstage/backend-plugin-api'; +import { + AuthService, + HttpAuthService, + LoggerService, +} from '@backstage/backend-plugin-api'; import { TokenIssuer } from '../identity/types'; import { UserInfoDatabase } from '../database/UserInfoDatabase'; import { OidcDatabase } from '../database/OidcDatabase'; @@ -28,6 +32,7 @@ export class OidcRouter { private readonly logger: LoggerService, private readonly auth: AuthService, private readonly appUrl: string, + private readonly httpAuth: HttpAuthService, ) {} static create(options: { @@ -38,12 +43,14 @@ export class OidcRouter { logger: LoggerService; userInfo: UserInfoDatabase; oidc: OidcDatabase; + httpAuth: HttpAuthService; }) { return new OidcRouter( OidcService.create(options), options.logger, options.auth, options.appUrl, + options.httpAuth, ); } @@ -70,7 +77,7 @@ export class OidcRouter { // Authorization endpoint // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest // Handles the initial authorization request from the client, validates parameters, - // and redirects to the consent page for user approval + // and redirects to the Authorization Session page for user approval router.get('/v1/authorize', async (req, res) => { // todo(blam): maybe add zod types for validating input const { @@ -94,7 +101,7 @@ export class OidcRouter { } try { - const result = await this.oidc.createConsentRequest({ + const result = await this.oidc.createAuthorizationSession({ clientId: clientId as string, redirectUri: redirectUri as string, responseType: responseType as string, @@ -107,12 +114,13 @@ export class OidcRouter { // todo(blam): maybe this URL could be overridable by config if // the plugin is mounted somewhere else? - const consentUrl = new URL( - `/auth/consent/${result.consentRequestId}`, + // support slashes in baseUrl? + const authSessionRedirectUrl = new URL( + `/auth/sessions/${result.id}`, this.appUrl, ); - return res.redirect(consentUrl.toString()); + return res.redirect(authSessionRedirectUrl.toString()); } catch (error) { const errorParams = new URLSearchParams(); errorParams.append( @@ -133,77 +141,69 @@ export class OidcRouter { } }); - // Consent request details endpoint - // Returns consent request details for the frontend consent page - router.get('/v1/consent/:consentId', async (req, res) => { - const { consentId } = req.params; + // Authorization Session request details endpoint + // Returns Authorization Session request details for the frontned + router.get('/v1/sessions/:sessionId', async (req, res) => { + const { sessionId } = req.params; - if (!consentId) { + if (!sessionId) { return res.status(400).json({ error: 'invalid_request', - error_description: 'Missing consent ID', + error_description: 'Missing Authorization Session ID', }); } try { - const consentRequest = await this.oidc.getConsentRequest({ - consentRequestId: consentId, + const session = await this.oidc.getAuthorizationSession({ + sessionId, }); return res.json({ - id: consentRequest.id, - clientName: consentRequest.clientName, - scope: consentRequest.scope, - redirectUri: consentRequest.redirectUri, + id: session.id, + clientName: session.clientName, + scope: session.scope, + redirectUri: session.redirectUri, }); } catch (error) { this.logger.error( - `Failed to get consent request: ${ + `Failed to get authorization session: ${ isError(error) ? error.message : 'Unknown error' }`, error, ); return res.status(404).json({ error: 'not_found', - error_description: 'Consent request not found or expired', + error_description: 'Authorization session not found or expired', }); } }); - // Consent approval endpoint - // Handles user approval of consent requests and generates authorization codes - router.post('/v1/consent/:consentId/approve', async (req, res) => { - const { consentId } = req.params; + // Authorization Session approval endpoint + // Handles user approval of Authorization Session requests and generates authorization codes + router.post('/v1/sessions/:sessionId/approve', async (req, res) => { + const { sessionId } = req.params; - if (!consentId) { + if (!sessionId) { return res.status(400).json({ error: 'invalid_request', - error_description: 'Missing consent ID', + error_description: 'Missing authorization session ID', }); } try { - const authHeader = req.headers.authorization; - if (!authHeader?.startsWith('Bearer ')) { - return res.status(401).json({ - error: 'unauthorized', - error_description: 'Bearer token required', - }); - } + const httpCredentials = await this.httpAuth.credentials(req); - const token = authHeader.substring(7); - const credentials = await this.auth.authenticate(token); - if (!this.auth.isPrincipal(credentials, 'user')) { + if (!this.auth.isPrincipal(httpCredentials, 'user')) { return res.status(401).json({ error: 'unauthorized', error_description: 'Authentication required', }); } - const userEntityRef = credentials.principal.userEntityRef; + const userEntityRef = httpCredentials.principal.userEntityRef; - const result = await this.oidc.approveConsentRequest({ - consentRequestId: consentId, + const result = await this.oidc.approveAuthorizationSession({ + sessionId, userEntityRef, }); @@ -211,8 +211,9 @@ export class OidcRouter { redirectUrl: result.redirectUrl, }); } catch (error) { + console.log(error); this.logger.error( - `Failed to approve consent: ${ + `Failed to approve authorization session: ${ isError(error) ? error.message : 'Unknown error' }`, error, @@ -224,33 +225,33 @@ export class OidcRouter { } }); - // Consent rejection endpoint - // Handles user rejection of consent requests and redirects with error - router.post('/v1/consent/:consentId/reject', async (req, res) => { - const { consentId } = req.params; + // Authorization Session rejection endpoint + // Handles user rejection of Authorization Session requests and redirects with error + router.post('/v1/sessions/:sessionId/reject', async (req, res) => { + const { sessionId } = req.params; - if (!consentId) { + if (!sessionId) { return res.status(400).json({ error: 'invalid_request', - error_description: 'Missing consent ID', + error_description: 'Missing authorization session ID', }); } try { - const consentRequest = await this.oidc.getConsentRequest({ - consentRequestId: consentId, + const session = await this.oidc.getAuthorizationSession({ + sessionId, }); - await this.oidc.deleteConsentRequest({ consentRequestId: consentId }); + await this.oidc.rejectAuthorizationSession({ sessionId }); const errorParams = new URLSearchParams(); errorParams.append('error', 'access_denied'); errorParams.append('error_description', 'User denied the request'); - if (consentRequest.state) { - errorParams.append('state', consentRequest.state); + if (session.state) { + errorParams.append('state', session.state); } - const redirectUrl = new URL(consentRequest.redirectUri); + const redirectUrl = new URL(session.redirectUri); redirectUrl.search = errorParams.toString(); return res.json({ @@ -258,7 +259,10 @@ export class OidcRouter { }); } catch (error) { const description = isError(error) ? error.message : 'Unknown error'; - this.logger.error(`Failed to reject consent: ${description}`, error); + this.logger.error( + `Failed to reject authorization session: ${description}`, + error, + ); return res.status(400).json({ error: 'invalid_request', diff --git a/plugins/auth-backend/src/service/OidcService.test.ts b/plugins/auth-backend/src/service/OidcService.test.ts index fa4c0a12ac..5067cde458 100644 --- a/plugins/auth-backend/src/service/OidcService.test.ts +++ b/plugins/auth-backend/src/service/OidcService.test.ts @@ -233,8 +233,8 @@ describe('OidcService', () => { }); }); - describe('createConsentRequest', () => { - it('should create a consent request for valid client', async () => { + describe('createAuthorizationSession', () => { + it('should create a authorization session for valid client', async () => { const { service } = await createOidcService(databaseId); const client = await service.registerClient({ @@ -242,7 +242,7 @@ describe('OidcService', () => { redirectUris: ['https://example.com/callback'], }); - const consent = await service.createConsentRequest({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', @@ -250,8 +250,8 @@ describe('OidcService', () => { state: 'test-state', }); - expect(consent).toEqual({ - consentRequestId: expect.any(String), + expect(authSession).toEqual({ + id: expect.any(String), clientName: 'Test Client', scope: 'openid', redirectUri: 'https://example.com/callback', @@ -262,7 +262,7 @@ describe('OidcService', () => { const { service } = await createOidcService(databaseId); await expect( - service.createConsentRequest({ + service.createAuthorizationSession({ clientId: 'invalid-client', redirectUri: 'https://example.com/callback', responseType: 'code', @@ -279,7 +279,7 @@ describe('OidcService', () => { }); await expect( - service.createConsentRequest({ + service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://invalid.com/callback', responseType: 'code', @@ -296,7 +296,7 @@ describe('OidcService', () => { }); await expect( - service.createConsentRequest({ + service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'token', @@ -312,7 +312,7 @@ describe('OidcService', () => { redirectUris: ['https://example.com/callback'], }); - const consent = await service.createConsentRequest({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', @@ -320,7 +320,7 @@ describe('OidcService', () => { codeChallengeMethod: 'S256', }); - expect(consent.consentRequestId).toBeDefined(); + expect(authSession.id).toBeDefined(); }); it('should throw error for invalid PKCE method', async () => { @@ -332,7 +332,7 @@ describe('OidcService', () => { }); await expect( - service.createConsentRequest({ + service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', @@ -343,8 +343,8 @@ describe('OidcService', () => { }); }); - describe('approveConsentRequest', () => { - it('should approve a valid consent request', async () => { + describe('approveAuthorizationSession', () => { + it('should approve a valid authorization session', async () => { const { service } = await createOidcService(databaseId); const client = await service.registerClient({ @@ -352,15 +352,15 @@ describe('OidcService', () => { redirectUris: ['https://example.com/callback'], }); - const consent = await service.createConsentRequest({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', state: 'test-state', }); - const result = await service.approveConsentRequest({ - consentRequestId: consent.consentRequestId, + const result = await service.approveAuthorizationSession({ + sessionId: authSession.id, userEntityRef: 'user:default/test', }); @@ -369,20 +369,20 @@ describe('OidcService', () => { ); }); - it('should throw error for invalid consent request', async () => { + it('should throw error for invalid authorization session', async () => { const { service } = await createOidcService(databaseId); await expect( - service.approveConsentRequest({ - consentRequestId: 'invalid-consent', + service.approveAuthorizationSession({ + sessionId: 'invalid-session', userEntityRef: 'user:default/test', }), - ).rejects.toThrow('Invalid consent request'); + ).rejects.toThrow('Invalid authorization session'); }); }); - describe('getConsentRequest', () => { - it('should return consent request details', async () => { + describe('getAuthorizationSession', () => { + it('should return authorization session details', async () => { const { service } = await createOidcService(databaseId); const client = await service.registerClient({ @@ -390,7 +390,7 @@ describe('OidcService', () => { redirectUris: ['https://example.com/callback'], }); - const consent = await service.createConsentRequest({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', @@ -398,13 +398,13 @@ describe('OidcService', () => { state: 'test-state', }); - const details = await service.getConsentRequest({ - consentRequestId: consent.consentRequestId, + const details = await service.getAuthorizationSession({ + sessionId: authSession.id, }); expect(details).toEqual( expect.objectContaining({ - id: consent.consentRequestId, + id: authSession.id, clientId: client.clientId, clientName: 'Test Client', redirectUri: 'https://example.com/callback', @@ -416,8 +416,8 @@ describe('OidcService', () => { }); }); - describe('deleteConsentRequest', () => { - it('should delete a consent request', async () => { + describe('rejectAuthorizationSession', () => { + it('should delete a authorization session', async () => { const { service } = await createOidcService(databaseId); const client = await service.registerClient({ @@ -425,31 +425,35 @@ describe('OidcService', () => { redirectUris: ['https://example.com/callback'], }); - const consent = await service.createConsentRequest({ + const authSession = await service.createAuthorizationSession({ clientId: client.clientId, redirectUri: 'https://example.com/callback', responseType: 'code', }); - await service.deleteConsentRequest({ - consentRequestId: consent.consentRequestId, + await service.rejectAuthorizationSession({ + sessionId: authSession.id, }); await expect( - service.getConsentRequest({ - consentRequestId: consent.consentRequestId, + service.getAuthorizationSession({ + sessionId: authSession.id, }), - ).rejects.toThrow('Invalid consent request'); + ).resolves.toEqual( + expect.objectContaining({ + status: 'rejected', + }), + ); }); - it('should handle deleting non-existent consent request', async () => { + it('should throw error for invalid authorization session', async () => { const { service } = await createOidcService(databaseId); await expect( - service.deleteConsentRequest({ - consentRequestId: 'non-existent', + service.rejectAuthorizationSession({ + sessionId: 'invalid-session', }), - ).resolves.not.toThrow(); + ).rejects.toThrow('Invalid authorization session'); }); }); diff --git a/plugins/auth-backend/src/service/OidcService.ts b/plugins/auth-backend/src/service/OidcService.ts index 4214f53a8c..e89dfeac6f 100644 --- a/plugins/auth-backend/src/service/OidcService.ts +++ b/plugins/auth-backend/src/service/OidcService.ts @@ -126,7 +126,7 @@ export class OidcService { }); } - public async createConsentRequest(opts: { + public async createAuthorizationSession(opts: { clientId: string; redirectUri: string; responseType: string; @@ -185,43 +185,24 @@ export class OidcService { expiresAt: sessionExpiresAt, }); - const consentRequestId = crypto.randomUUID(); - const consentExpiresAt = DateTime.now().plus({ minutes: 30 }).toISO(); - - await this.oidc.createConsentRequest({ - id: consentRequestId, - sessionId, - expiresAt: consentExpiresAt, - }); - return { - consentRequestId, + id: sessionId, clientName: client.clientName, scope, redirectUri, }; } - public async approveConsentRequest(opts: { - consentRequestId: string; + public async approveAuthorizationSession(opts: { + sessionId: string; userEntityRef: string; }) { - const { consentRequestId, userEntityRef } = opts; - - const consentRequest = await this.oidc.getConsentRequest({ - id: consentRequestId, - }); - if (!consentRequest) { - throw new InputError('Invalid consent request'); - } - - if (DateTime.fromISO(consentRequest.expiresAt) < DateTime.now()) { - throw new InputError('Consent request expired'); - } + const { sessionId, userEntityRef } = opts; const session = await this.oidc.getAuthorizationSession({ - id: consentRequest.sessionId, + id: sessionId, }); + if (!session) { throw new InputError('Invalid authorization session'); } @@ -245,9 +226,8 @@ export class OidcService { expiresAt: codeExpiresAt, }); - await this.oidc.deleteConsentRequest({ id: consentRequestId }); - const redirectUrl = new URL(session.redirectUri); + redirectUrl.searchParams.append('code', authorizationCode); if (session.state) { redirectUrl.searchParams.append('state', session.state); @@ -258,33 +238,26 @@ export class OidcService { }; } - public async getConsentRequest(opts: { consentRequestId: string }) { - const consentRequest = await this.oidc.getConsentRequest({ - id: opts.consentRequestId, - }); - if (!consentRequest) { - throw new InputError('Invalid consent request'); - } - - if (DateTime.fromISO(consentRequest.expiresAt) < DateTime.now()) { - throw new InputError('Consent request expired'); - } - + public async getAuthorizationSession(opts: { sessionId: string }) { const session = await this.oidc.getAuthorizationSession({ - id: consentRequest.sessionId, + id: opts.sessionId, }); if (!session) { throw new InputError('Invalid authorization session'); } + if (DateTime.fromISO(session.expiresAt) < DateTime.now()) { + throw new InputError('Authorization session expired'); + } + const client = await this.oidc.getClient({ clientId: session.clientId }); if (!client) { throw new InputError('Invalid client_id'); } return { - id: consentRequest.id, + id: session.id, clientId: session.clientId, clientName: client.clientName, redirectUri: session.redirectUri, @@ -294,24 +267,28 @@ export class OidcService { codeChallenge: session.codeChallenge, codeChallengeMethod: session.codeChallengeMethod, nonce: session.nonce, - expiresAt: consentRequest.expiresAt, + expiresAt: session.expiresAt, + status: session.status, }; } - public async deleteConsentRequest(opts: { consentRequestId: string }) { - const consentRequest = await this.oidc.getConsentRequest({ - id: opts.consentRequestId, + public async rejectAuthorizationSession(opts: { sessionId: string }) { + const session = await this.oidc.getAuthorizationSession({ + id: opts.sessionId, }); - if (!consentRequest) { - return; + + if (!session) { + throw new InputError('Invalid authorization session'); + } + + if (DateTime.fromISO(session.expiresAt) < DateTime.now()) { + throw new InputError('Authorization session expired'); } await this.oidc.updateAuthorizationSession({ - id: consentRequest.sessionId, + id: session.id, status: 'rejected', }); - - await this.oidc.deleteConsentRequest({ id: opts.consentRequestId }); } public async authorize(opts: { @@ -487,15 +464,6 @@ export class OidcService { used: true, }); - const accessTokenId = crypto.randomUUID(); - const expiresAt = DateTime.now().plus({ hours: 1 }).toISO(); - - await this.oidc.createAccessToken({ - tokenId: accessTokenId, - sessionId: session.id, - expiresAt, - }); - const { token } = await this.tokenIssuer.issueToken({ claims: { sub: session.userEntityRef, diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 28f218c749..1f5fea7cb5 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -21,6 +21,7 @@ import { AuthService, DatabaseService, DiscoveryService, + HttpAuthService, LoggerService, RootConfigService, } from '@backstage/backend-plugin-api'; @@ -52,6 +53,7 @@ interface RouterOptions { providerFactories?: ProviderFactories; catalog: CatalogService; ownershipResolver?: AuthOwnershipResolver; + httpAuth: HttpAuthService; } export async function createRouter( @@ -64,6 +66,7 @@ export async function createRouter( database: db, tokenFactoryAlgorithm, providerFactories = {}, + httpAuth, } = options; const router = Router(); @@ -158,6 +161,7 @@ export async function createRouter( userInfo, oidc, logger, + httpAuth, }); router.use(oidcRouter.getRouter());