From 701c51abc92192115ae1dfeeb6594af0255815c3 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Wed, 3 Apr 2024 15:09:46 +0200 Subject: [PATCH] backend-app-api: verify service-to-service token Signed-off-by: Vincenzo Scamporlino --- .../auth/PluginTokenHandler.ts | 44 +++++++++++++++---- .../implementations/auth/PublicKeysClient.ts | 41 ----------------- .../auth/authServiceFactory.ts | 12 ++++- 3 files changed, 46 insertions(+), 51 deletions(-) delete mode 100644 packages/backend-app-api/src/services/implementations/auth/PublicKeysClient.ts diff --git a/packages/backend-app-api/src/services/implementations/auth/PluginTokenHandler.ts b/packages/backend-app-api/src/services/implementations/auth/PluginTokenHandler.ts index 4a1b17d3b9..fa6337d8e1 100644 --- a/packages/backend-app-api/src/services/implementations/auth/PluginTokenHandler.ts +++ b/packages/backend-app-api/src/services/implementations/auth/PluginTokenHandler.ts @@ -26,7 +26,8 @@ import { import { DateTime } from 'luxon'; import { v4 as uuid } from 'uuid'; import { InternalKey, KeyStore } from './types'; -import { DefaultPublicKeysClient, PublicKeysClient } from './PublicKeysClient'; +import { AuthenticationError } from '@backstage/errors'; +import { createRemoteJWKSet, jwtVerify } from 'jose'; type Options = { publicKeyStore: KeyStore; @@ -48,7 +49,7 @@ type Options = { export class PluginTokenHandler { private privateKeyPromise?: Promise; private keyExpiry?: Date; - private publicKeysClient: PublicKeysClient; + private jwksMap: Record> = {}; static create(options: Options) { return new PluginTokenHandler( @@ -65,13 +66,30 @@ export class PluginTokenHandler { readonly publicKeyStore: KeyStore, readonly keyDurationSeconds: number, readonly algorithm: string, - discovery: DiscoveryService, - ) { - this.publicKeysClient = new DefaultPublicKeysClient(discovery); - } + readonly discovery: DiscoveryService, + ) {} - async verifyToken(_token: string): Promise<{ subject: string }> { - return { subject: 'is me' }; + async verifyToken(token: string): Promise<{ subject: string }> { + const claims = decodeJwt(token); + const pluginId = claims.sub; + if (!pluginId) { + throw new AuthenticationError('Invalid subject'); + } + + const JWKS = await this.getJWKS(pluginId); + try { + const { payload, protectedHeader } = await jwtVerify(token, JWKS, { + issuer: 'backstage-plugin', + // TODO(vinzscam): add audience verification + }); + + console.log('payload', payload); + console.log('protected header', protectedHeader); + return { subject: 'is me' }; + } catch (e) { + // TODO(vinzscam): here we need to handle errors properly + throw e; + } } async issueToken(options: { @@ -101,6 +119,16 @@ export class PluginTokenHandler { return { token }; } + private async getJWKS(pluginId: string) { + if (!this.jwksMap[pluginId]) { + const url = `${await this.discovery.getBaseUrl( + pluginId, + )}/.backstage/auth/v1/jwks.json`; + console.log('fetching from', url); + this.jwksMap[pluginId] = createRemoteJWKSet(new URL(url)); + } + return this.jwksMap[pluginId]; + } private async getKey(): Promise { // Make sure that we only generate one key at a time if (this.privateKeyPromise) { diff --git a/packages/backend-app-api/src/services/implementations/auth/PublicKeysClient.ts b/packages/backend-app-api/src/services/implementations/auth/PublicKeysClient.ts deleted file mode 100644 index 6040e64c15..0000000000 --- a/packages/backend-app-api/src/services/implementations/auth/PublicKeysClient.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2024 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 { DiscoveryService } from '@backstage/backend-plugin-api'; -import { ResponseError } from '@backstage/errors'; -import { JsonObject } from '@backstage/types'; - -export class DefaultPublicKeysClient implements PublicKeysClient { - constructor(private readonly discovery: DiscoveryService) {} - - // TODO: cache stuff - async listPublicKeys(pluginId: string) { - const response = await fetch( - `${await this.discovery.getBaseUrl( - pluginId, - )}/.backstage/auth/v1/jwks.json`, - ); - - if (response.ok) { - return response.json(); - } - throw await ResponseError.fromResponse(response); - } -} - -export type PublicKeysClient = { - listPublicKeys(pluginId: string): Promise<{ keys: JsonObject[] }>; -}; diff --git a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts index 3522da63e2..3b7a7322cb 100644 --- a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts @@ -31,6 +31,7 @@ import { UserTokenHandler } from './UserTokenHandler'; import { PluginTokenHandler } from './PluginTokenHandler'; import { JsonObject } from '@backstage/types'; import { DatabaseKeyStore } from './DatabaseKeyStore'; +import { KeyStore } from './types'; /** @internal */ export type InternalBackstageCredentials = @@ -110,10 +111,13 @@ class DefaultAuthService implements AuthService { private readonly userTokenHandler: UserTokenHandler, private readonly pluginId: string, private readonly disableDefaultAuthPolicy: boolean, + private readonly publicKeyStore: KeyStore, private readonly pluginTokenHandler: PluginTokenHandler, ) {} - listPublicServiceKeys(): Promise<{ keys: JsonObject[] }> { - throw new Error('Method not implemented.'); + + async listPublicServiceKeys(): Promise<{ keys: JsonObject[] }> { + const { keys } = await this.publicKeyStore.listKeys(); + return { keys: keys.map(({ key }) => key) }; } // allowLimitedAccess is currently ignored, since we currently always use the full user tokens @@ -256,6 +260,7 @@ export const authServiceFactory = createServiceFactory({ // new auth services in the new backend system. tokenManager: coreServices.tokenManager, }, + async factory({ config, discovery, plugin, tokenManager, logger, database }) { const disableDefaultAuthPolicy = Boolean( config.getOptionalBoolean( @@ -264,16 +269,19 @@ export const authServiceFactory = createServiceFactory({ ); const publicKeyStore = await DatabaseKeyStore.create({ database }); + return new DefaultAuthService( tokenManager, new UserTokenHandler({ discovery }), plugin.getId(), disableDefaultAuthPolicy, + publicKeyStore, PluginTokenHandler.create({ keyDurationSeconds: 60 * 60, issuer: `plugin:${plugin.getId()}`, logger, publicKeyStore, + discovery, }), ); },