auth-backend: add initial userinfo endpoint
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
Initial implementation of the `/v1/userinfo` endpoint, which is now able to parse and return the `sub` and `ent` claims from a Backstage user token.
|
||||
@@ -38,7 +38,7 @@ describe('authPlugin', () => {
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toMatchObject({
|
||||
claims_supported: ['sub'],
|
||||
claims_supported: ['sub', 'ent'],
|
||||
issuer: `http://localhost:${server.port()}/api/auth`,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,6 +54,8 @@ export const authPlugin = createBackendPlugin({
|
||||
database: coreServices.database,
|
||||
discovery: coreServices.discovery,
|
||||
tokenManager: coreServices.tokenManager,
|
||||
auth: coreServices.auth,
|
||||
httpAuth: coreServices.httpAuth,
|
||||
catalogApi: catalogServiceRef,
|
||||
},
|
||||
async init({
|
||||
@@ -63,6 +65,8 @@ export const authPlugin = createBackendPlugin({
|
||||
database,
|
||||
discovery,
|
||||
tokenManager,
|
||||
auth,
|
||||
httpAuth,
|
||||
catalogApi,
|
||||
}) {
|
||||
const router = await createRouter({
|
||||
@@ -71,6 +75,8 @@ export const authPlugin = createBackendPlugin({
|
||||
database,
|
||||
discovery,
|
||||
tokenManager,
|
||||
auth,
|
||||
httpAuth,
|
||||
catalogApi,
|
||||
providerFactories: Object.fromEntries(providers),
|
||||
disableDefaultProviderFactories: true,
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2020 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 {
|
||||
coreServices,
|
||||
createBackendPlugin,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { mockServices, startTestBackend } from '@backstage/backend-test-utils';
|
||||
import Router from 'express-promise-router';
|
||||
import request from 'supertest';
|
||||
import { bindOidcRouter } from './router';
|
||||
|
||||
describe('bindOidcRouter', () => {
|
||||
it('should return user info', async () => {
|
||||
const auth = mockServices.auth.mock();
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
createBackendPlugin({
|
||||
pluginId: 'auth',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: { httpRouter: coreServices.httpRouter },
|
||||
async init({ httpRouter }) {
|
||||
const router = Router();
|
||||
bindOidcRouter(router, {
|
||||
baseUrl: 'http://localhost:7000',
|
||||
auth,
|
||||
tokenIssuer: {} as any,
|
||||
});
|
||||
httpRouter.use(router);
|
||||
httpRouter.addAuthPolicy({
|
||||
path: '/',
|
||||
allow: 'unauthenticated',
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
auth.authenticate.mockResolvedValueOnce({} as any);
|
||||
auth.isPrincipal.mockReturnValueOnce(true);
|
||||
|
||||
await request(server)
|
||||
.get('/api/auth/v1/userinfo')
|
||||
.set(
|
||||
'Authorization',
|
||||
`Bearer h.${btoa(
|
||||
JSON.stringify({ sub: 'k/ns:n', ent: ['k/ns:a', 'k/ns:b'] }),
|
||||
)}.s`,
|
||||
)
|
||||
.expect(200, {
|
||||
sub: 'k/ns:n',
|
||||
ent: ['k/ns:a', 'k/ns:b'],
|
||||
});
|
||||
|
||||
expect('test').toBe('test');
|
||||
});
|
||||
});
|
||||
@@ -17,15 +17,19 @@
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { TokenIssuer } from './types';
|
||||
import { AuthService } from '@backstage/backend-plugin-api';
|
||||
import { decodeJwt } from 'jose';
|
||||
import { AuthenticationError, InputError } from '@backstage/errors';
|
||||
|
||||
export function bindOidcRouter(
|
||||
targetRouter: express.Router,
|
||||
options: {
|
||||
baseUrl: string;
|
||||
auth: AuthService;
|
||||
tokenIssuer: TokenIssuer;
|
||||
},
|
||||
) {
|
||||
const { baseUrl, tokenIssuer } = options;
|
||||
const { baseUrl, auth, tokenIssuer } = options;
|
||||
|
||||
const router = Router();
|
||||
targetRouter.use(router);
|
||||
@@ -51,7 +55,7 @@ export function bindOidcRouter(
|
||||
],
|
||||
scopes_supported: ['openid'],
|
||||
token_endpoint_auth_methods_supported: [],
|
||||
claims_supported: ['sub'],
|
||||
claims_supported: ['sub', 'ent'],
|
||||
grant_types_supported: [],
|
||||
};
|
||||
|
||||
@@ -68,7 +72,40 @@ export function bindOidcRouter(
|
||||
res.status(501).send('Not Implemented');
|
||||
});
|
||||
|
||||
router.get('/v1/userinfo', (_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 auth.authenticate(token, {
|
||||
allowLimitedAccess: true,
|
||||
});
|
||||
if (!auth.isPrincipal(credentials, 'user')) {
|
||||
throw new InputError(
|
||||
'Userinfo endpoint must be called with a token that represents a user principal',
|
||||
);
|
||||
}
|
||||
|
||||
const { sub: userEntityRef, ent: ownershipEntityRefs = [] } =
|
||||
decodeJwt(token);
|
||||
|
||||
if (typeof userEntityRef !== 'string') {
|
||||
throw new Error('Invalid user token, user entity ref must be a string');
|
||||
}
|
||||
if (
|
||||
!Array.isArray(ownershipEntityRefs) ||
|
||||
ownershipEntityRefs.some(ref => typeof ref !== 'string')
|
||||
) {
|
||||
throw new Error(
|
||||
'Invalid user token, ownership entity refs must be an array of strings',
|
||||
);
|
||||
}
|
||||
|
||||
res.json({ sub: userEntityRef, ent: ownershipEntityRefs });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -151,6 +151,7 @@ export async function createRouter(
|
||||
});
|
||||
|
||||
bindOidcRouter(router, {
|
||||
auth,
|
||||
tokenIssuer,
|
||||
baseUrl: authUrl,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user