backend-defaults: do not forward token verification errors to caller

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-12-05 12:19:12 +01:00
parent 45b5f3440f
commit 57e0b112d6
7 changed files with 46 additions and 12 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-defaults': patch
---
The user and plugin token verification in the default `AuthService` implementation will no longer forward verification errors to the caller, and instead log them as warnings.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-defaults': patch
---
The default `authServiceFactory` now correctly depends on the plugin scoped `Logger` services rather than the root scoped one.
@@ -109,8 +109,9 @@ describe('authServiceFactory', () => {
});
it('should authenticate issued tokens with new auth', async () => {
const logger = mockServices.logger.mock();
const tester = ServiceFactoryTester.from(authServiceFactory, {
dependencies: mockDeps,
dependencies: [...mockDeps, logger.factory],
});
const searchAuth = await tester.getSubject('search');
@@ -134,8 +135,13 @@ describe('authServiceFactory', () => {
targetPluginId: 'catalog',
});
expect(logger.warn).not.toHaveBeenCalled();
await expect(searchAuth.authenticate(searchToken)).rejects.toThrow(
'Invalid plugin token',
'Failed plugin token verification',
);
expect(logger.warn).toHaveBeenCalledWith(
'Failed to verify incoming plugin token',
expect.any(Error),
);
await expect(catalogAuth.authenticate(searchToken)).resolves.toEqual(
expect.objectContaining({
@@ -59,7 +59,7 @@ export const authServiceFactory = createServiceFactory({
service: coreServices.auth,
deps: {
config: coreServices.rootConfig,
logger: coreServices.rootLogger,
logger: coreServices.logger,
discovery: coreServices.discovery,
plugin: coreServices.pluginMetadata,
database: coreServices.database,
@@ -89,6 +89,7 @@ export const authServiceFactory = createServiceFactory({
const userTokens = UserTokenHandler.create({
discovery,
logger,
});
const pluginTokens = pluginTokenHandlerDecorator(
@@ -120,7 +120,8 @@ export class DefaultPluginTokenHandler implements PluginTokenHandler {
requiredClaims: ['iat', 'exp', 'sub', 'aud'],
},
).catch(e => {
throw new AuthenticationError('Invalid plugin token', e);
this.logger.warn('Failed to verify incoming plugin token', e);
throw new AuthenticationError('Failed plugin token verification');
});
return { subject: `plugin:${payload.sub}`, limitedUserToken: payload.obo };
@@ -64,13 +64,17 @@ async function createToken(options: {
describe('UserTokenHandler', () => {
let userTokenHandler: UserTokenHandler;
const logger = mockServices.logger.mock();
registerMswTestHooks(server);
beforeEach(() => {
jest.useRealTimers();
jest.resetAllMocks();
userTokenHandler = UserTokenHandler.create({
discovery: mockServices.discovery(),
logger,
});
server.use(
@@ -120,7 +124,7 @@ describe('UserTokenHandler', () => {
signature: 'sig',
}),
),
).rejects.toThrow('signature verification failed');
).rejects.toThrow('Failed user token verification');
await expect(
userTokenHandler.verifyToken(
@@ -130,7 +134,7 @@ describe('UserTokenHandler', () => {
signature: 'sig',
}),
),
).rejects.toThrow('signature verification failed');
).rejects.toThrow('Failed user token verification');
});
it('should fail to verify tokens that have a bad alg', async () => {
@@ -156,8 +160,13 @@ describe('UserTokenHandler', () => {
});
const token = `${header}.${payload}.`;
expect(logger.warn).not.toHaveBeenCalled();
await expect(userTokenHandler.verifyToken(token)).rejects.toThrow(
/Unsupported "alg" value/,
'Failed user token verification',
);
expect(logger.warn).toHaveBeenCalledWith(
'Failed to verify incoming user token',
expect.any(Error),
);
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { DiscoveryService } from '@backstage/backend-plugin-api';
import { DiscoveryService, LoggerService } from '@backstage/backend-plugin-api';
import { AuthenticationError } from '@backstage/errors';
import { tokenTypes } from '@backstage/plugin-auth-node';
import {
@@ -33,15 +33,21 @@ import { JwksClient } from '../JwksClient';
* @internal
*/
export class UserTokenHandler {
static create(options: { discovery: DiscoveryService }): UserTokenHandler {
static create(options: {
discovery: DiscoveryService;
logger: LoggerService;
}): UserTokenHandler {
const jwksClient = new JwksClient(async () => {
const url = await options.discovery.getBaseUrl('auth');
return new URL(`${url}/.well-known/jwks.json`);
});
return new UserTokenHandler(jwksClient);
return new UserTokenHandler(jwksClient, options.logger);
}
constructor(private readonly jwksClient: JwksClient) {}
constructor(
private readonly jwksClient: JwksClient,
private readonly logger: LoggerService,
) {}
async verifyToken(token: string) {
const verifyOpts = this.#getTokenVerificationOptions(token);
@@ -57,7 +63,8 @@ export class UserTokenHandler {
this.jwksClient.getKey,
verifyOpts,
).catch(e => {
throw new AuthenticationError('Invalid token', e);
this.logger.warn('Failed to verify incoming user token', e);
throw new AuthenticationError('Failed user token verification');
});
const userEntityRef = payload.sub;