chore: cleanup and simplify

Signed-off-by: benjdlambert <ben@blam.sh>

Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
benjdlambert
2025-07-07 12:21:57 +02:00
parent 0d320ca888
commit bf372ab53f
8 changed files with 166 additions and 467 deletions
@@ -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',
}),
);
});
});
});
});
@@ -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<string, unknown>;
};
@@ -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<OidcConsentRequestRow>('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<OidcConsentRequestRow>(
'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<OAuthAuthorizationSessionRow>(
'oauth_authorization_sessions',
@@ -230,12 +191,6 @@ export class OidcDatabase {
return this.rowToAuthorizationSession(session) as AuthorizationSession;
}
async deleteConsentRequest({ id }: { id: string }) {
await this.db<OidcConsentRequestRow>('oidc_consent_requests')
.where('id', id)
.delete();
}
async createAuthorizationCode(
authorizationCode: Omit<AuthorizationCode, 'used'>,
) {
@@ -284,16 +239,6 @@ export class OidcDatabase {
return this.rowToAuthorizationCode(updated) as AuthorizationCode;
}
async createAccessToken(accessToken: AccessToken) {
await this.db<OidcAccessTokenRow>('oidc_access_tokens').insert({
token_id: accessToken.tokenId,
session_id: accessToken.sessionId,
expires_at: accessToken.expiresAt,
});
return accessToken;
}
private rowToClient(row: Partial<OidcClientRow>): Partial<Client> {
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<OidcConsentRequestRow>,
): Partial<ConsentRequest> {
return {
id: row.id,
sessionId: row.session_id,
expiresAt: row.expires_at,
};
}
private authorizationCodeToRow(
authorizationCode: Partial<AuthorizationCode>,
): Partial<OidcAuthorizationCodeRow> {
@@ -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);
+57 -53
View File
@@ -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',
@@ -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');
});
});
+28 -60
View File
@@ -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,
@@ -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());