+5
-4
@@ -21,10 +21,11 @@ import {
|
||||
import { mockServices, startTestBackend } from '@backstage/backend-test-utils';
|
||||
import Router from 'express-promise-router';
|
||||
import request from 'supertest';
|
||||
import { OidcService } from './OidcService';
|
||||
import { OidcRouter } from './OidcRouter';
|
||||
import { UserInfoDatabase } from '../database/UserInfoDatabase';
|
||||
import { OidcService } from './OidcService';
|
||||
|
||||
describe('OidcService', () => {
|
||||
describe('OidcRouter', () => {
|
||||
describe('/v1/userinfo', () => {
|
||||
it('should return user info for full tokens', async () => {
|
||||
const auth = mockServices.auth.mock();
|
||||
@@ -48,7 +49,7 @@ describe('OidcService', () => {
|
||||
const router = Router();
|
||||
|
||||
router.use(
|
||||
OidcService.create({
|
||||
OidcRouter.create({
|
||||
auth,
|
||||
tokenIssuer: {} as any,
|
||||
baseUrl: 'http://localhost:7000',
|
||||
@@ -110,7 +111,7 @@ describe('OidcService', () => {
|
||||
const router = Router();
|
||||
|
||||
router.use(
|
||||
OidcService.create({
|
||||
OidcRouter.create({
|
||||
auth,
|
||||
tokenIssuer: {} as any,
|
||||
baseUrl: 'http://localhost:7000',
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import Router from 'express-promise-router';
|
||||
import { OidcService } from './OidcService';
|
||||
import { AuthenticationError } from '@backstage/errors';
|
||||
import { AuthService } from '@backstage/backend-plugin-api';
|
||||
import { TokenIssuer } from '../identity/types';
|
||||
import { UserInfoDatabase } from '../database/UserInfoDatabase';
|
||||
|
||||
export class OidcRouter {
|
||||
private constructor(private readonly oidc: OidcService) {}
|
||||
|
||||
static create(options: {
|
||||
auth: AuthService;
|
||||
tokenIssuer: TokenIssuer;
|
||||
baseUrl: string;
|
||||
userInfo: UserInfoDatabase;
|
||||
}) {
|
||||
return new OidcRouter(OidcService.create(options));
|
||||
}
|
||||
|
||||
public getRouter() {
|
||||
const router = Router();
|
||||
|
||||
router.get('/.well-known/openid-configuration', (_req, res) => {
|
||||
res.json(this.oidc.getConfiguration());
|
||||
});
|
||||
|
||||
router.get('/.well-known/jwks.json', async (_req, res) => {
|
||||
const { keys } = await this.oidc.listPublicKeys();
|
||||
res.json({ keys });
|
||||
});
|
||||
|
||||
router.get('/v1/token', (_req, res) => {
|
||||
res.status(501).send('Not Implemented');
|
||||
});
|
||||
|
||||
// This endpoint doesn't use the regular HttpAuthoidc, since the contract
|
||||
// is specifically for the header to be communicated in the Authorization
|
||||
// header, regardless of token type
|
||||
router.get('/v1/userinfo', async (req, res) => {
|
||||
const matches = req.headers.authorization?.match(/^Bearer[ ]+(\S+)$/i);
|
||||
const token = matches?.[1];
|
||||
if (!token) {
|
||||
throw new AuthenticationError('No token provided');
|
||||
}
|
||||
|
||||
const userInfo = await this.oidc.getUserInfo({ token });
|
||||
|
||||
if (!userInfo) {
|
||||
res.status(404).send('User info not found');
|
||||
return;
|
||||
}
|
||||
|
||||
res.json(userInfo);
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,7 @@
|
||||
import { AuthService } from '@backstage/backend-plugin-api';
|
||||
import { TokenIssuer } from '../identity/types';
|
||||
import { UserInfoDatabase } from '../database/UserInfoDatabase';
|
||||
import Router from 'express-promise-router';
|
||||
import { AuthenticationError, InputError } from '@backstage/errors';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { decodeJwt } from 'jose';
|
||||
|
||||
export class OidcService {
|
||||
@@ -42,10 +41,8 @@ export class OidcService {
|
||||
);
|
||||
}
|
||||
|
||||
public getRouter() {
|
||||
const router = Router();
|
||||
|
||||
const config = {
|
||||
public getConfiguration() {
|
||||
return {
|
||||
issuer: this.baseUrl,
|
||||
token_endpoint: `${this.baseUrl}/v1/token`,
|
||||
userinfo_endpoint: `${this.baseUrl}/v1/userinfo`,
|
||||
@@ -69,53 +66,27 @@ export class OidcService {
|
||||
claims_supported: ['sub', 'ent'],
|
||||
grant_types_supported: [],
|
||||
};
|
||||
}
|
||||
|
||||
router.get('/.well-known/openid-configuration', (_req, res) => {
|
||||
res.json(config);
|
||||
public async listPublicKeys() {
|
||||
return await this.tokenIssuer.listPublicKeys();
|
||||
}
|
||||
|
||||
public async getUserInfo({ token }: { token: string }) {
|
||||
const credentials = await this.auth.authenticate(token, {
|
||||
allowLimitedAccess: true,
|
||||
});
|
||||
if (!this.auth.isPrincipal(credentials, 'user')) {
|
||||
throw new InputError(
|
||||
'Userinfo endpoint must be called with a token that represents a user principal',
|
||||
);
|
||||
}
|
||||
|
||||
router.get('/.well-known/jwks.json', async (_req, res) => {
|
||||
const { keys } = await this.tokenIssuer.listPublicKeys();
|
||||
res.json({ keys });
|
||||
});
|
||||
const { sub: userEntityRef } = decodeJwt(token);
|
||||
|
||||
router.get('/v1/token', (_req, res) => {
|
||||
res.status(501).send('Not Implemented');
|
||||
});
|
||||
|
||||
// This endpoint doesn't use the regular HttpAuthService, since the contract
|
||||
// is specifically for the header to be communicated in the Authorization
|
||||
// header, regardless of token type
|
||||
router.get('/v1/userinfo', async (req, res) => {
|
||||
const matches = req.headers.authorization?.match(/^Bearer[ ]+(\S+)$/i);
|
||||
const token = matches?.[1];
|
||||
if (!token) {
|
||||
throw new AuthenticationError('No token provided');
|
||||
}
|
||||
|
||||
const credentials = await this.auth.authenticate(token, {
|
||||
allowLimitedAccess: true,
|
||||
});
|
||||
if (!this.auth.isPrincipal(credentials, 'user')) {
|
||||
throw new InputError(
|
||||
'Userinfo endpoint must be called with a token that represents a user principal',
|
||||
);
|
||||
}
|
||||
|
||||
const { sub: userEntityRef } = decodeJwt(token);
|
||||
|
||||
if (typeof userEntityRef !== 'string') {
|
||||
throw new Error('Invalid user token, user entity ref must be a string');
|
||||
}
|
||||
const userInfo = await this.userInfo.getUserInfo(userEntityRef);
|
||||
if (!userInfo) {
|
||||
res.status(404).send('User info not found');
|
||||
return;
|
||||
}
|
||||
|
||||
res.json(userInfo);
|
||||
});
|
||||
|
||||
return router;
|
||||
if (typeof userEntityRef !== 'string') {
|
||||
throw new Error('Invalid user token, user entity ref must be a string');
|
||||
}
|
||||
return await this.userInfo.getUserInfo(userEntityRef);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import { StaticTokenIssuer } from '../identity/StaticTokenIssuer';
|
||||
import { StaticKeyStore } from '../identity/StaticKeyStore';
|
||||
import { bindProviderRouters, ProviderFactories } from '../providers/router';
|
||||
import { OidcService } from './OidcService';
|
||||
import { OidcRouter } from './OidcRouter';
|
||||
|
||||
interface RouterOptions {
|
||||
logger: LoggerService;
|
||||
@@ -148,14 +149,14 @@ export async function createRouter(
|
||||
auth: options.auth,
|
||||
});
|
||||
|
||||
router.use(
|
||||
OidcService.create({
|
||||
auth: options.auth,
|
||||
tokenIssuer,
|
||||
baseUrl: authUrl,
|
||||
userInfo,
|
||||
}).getRouter(),
|
||||
);
|
||||
const oidcRouter = OidcRouter.create({
|
||||
auth: options.auth,
|
||||
tokenIssuer,
|
||||
baseUrl: authUrl,
|
||||
userInfo,
|
||||
});
|
||||
|
||||
router.use(oidcRouter.getRouter());
|
||||
|
||||
// Gives a more helpful error message than a plain 404
|
||||
router.use('/:provider/', req => {
|
||||
|
||||
Reference in New Issue
Block a user