chore: cleanup and simplify
Signed-off-by: benjdlambert <ben@blam.sh> Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user