From ed99c79cf1401c0e362247d8cb2b34e3d1ea091e Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Wed, 27 Mar 2024 12:57:43 +0100 Subject: [PATCH] backend-app-api: test service to service auth Co-authored-by: Patrik Oldsberg Signed-off-by: Vincenzo Scamporlino --- .../migrations/20240327104803_public_keys.js | 23 +-- .../auth/PluginTokenHandler.ts | 31 +++- .../auth/authServiceFactory.ts | 9 +- .../publicKeyStoreServiceFactory.ts | 7 +- packages/backend-next/package.json | 9 +- packages/backend-next/src/index.ts | 137 +++++++++++++----- 6 files changed, 161 insertions(+), 55 deletions(-) diff --git a/packages/backend-app-api/migrations/20240327104803_public_keys.js b/packages/backend-app-api/migrations/20240327104803_public_keys.js index 4498729eb9..378209c9a2 100644 --- a/packages/backend-app-api/migrations/20240327104803_public_keys.js +++ b/packages/backend-app-api/migrations/20240327104803_public_keys.js @@ -21,17 +21,20 @@ * @returns { Promise } */ exports.up = async function up(knex) { - await knex.schema.createTable('signing_keys', table => { - table - .string('id') - .primary() - .notNullable() - .comment('The unique ID of a public key'); + await knex.schema.createTable( + 'backstage_backend_public_keys__keys', + table => { + table + .string('id') + .primary() + .notNullable() + .comment('The unique ID of a public key'); - table.text('keys').notNullable().comment('JSON serialized public key'); + table.text('key').notNullable().comment('JSON serialized public key'); - table.timestamp('expires_at').notNullable(); - }); + table.timestamp('expires_at').notNullable(); + }, + ); }; /** @@ -39,5 +42,5 @@ exports.up = async function up(knex) { * @returns { Promise } */ exports.down = async function down(knex) { - return knex.schema.dropTable('signing_keys'); + return knex.schema.dropTable('backstage_backend_public_keys__keys'); }; 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 85aedc86ab..2f7f8098f4 100644 --- a/packages/backend-app-api/src/services/implementations/auth/PluginTokenHandler.ts +++ b/packages/backend-app-api/src/services/implementations/auth/PluginTokenHandler.ts @@ -15,10 +15,11 @@ */ import { + BackstageCredentials, LoggerService, PublicKeyStoreService, } from '@backstage/backend-plugin-api'; -import { exportJWK, generateKeyPair, JWK } from 'jose'; +import { exportJWK, generateKeyPair, JWK, importJWK, SignJWT } from 'jose'; import { DateTime } from 'luxon'; import { v4 as uuid } from 'uuid'; @@ -58,12 +59,35 @@ export class PluginTokenHandler { readonly algorithm: string, ) {} + async verifyToken(token: string): Promise<{ subject: string }> { + return { subject: 'is me' }; + } + async issueToken(options: { pluginId: string; targetPluginId: string; }): Promise<{ token: string }> { - await this.getKey(); - return { token: undefined! }; + const key = await this.getKey(); + + const iss = 'backstage-plugin'; + const sub = options.pluginId; + const aud = options.targetPluginId; + const iat = Math.floor(Date.now() / 1000); + const exp = iat + this.keyDurationSeconds; + + this.logger.info(`Issuing token for ${sub} with audentice ${aud}`); + + const claims = { iss, sub, aud, iat, exp }; + const token = await new SignJWT(claims) + .setProtectedHeader({ alg: this.algorithm, kid: key.kid }) + .setIssuer(iss) + .setAudience(aud) + .setSubject(sub) + .setIssuedAt(iat) + .setExpirationTime(exp) + .sign(await importJWK(key)); + + return { token }; } private async getKey(): Promise { @@ -102,6 +126,7 @@ export class PluginTokenHandler { // the new one. This also needs to be implemented cross-service though, meaning new services // that boot up need to be able to grab an existing key to use for signing. this.logger.info(`Created new signing key ${kid}`); + console.log(`DEBUG: publicKey=`, publicKey); await this.publicKeyStore.addKey({ id: kid, key: publicKey, 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 eaef7fb4bf..27f98ffb0a 100644 --- a/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/auth/authServiceFactory.ts @@ -114,7 +114,14 @@ class DefaultAuthService implements AuthService { // allowLimitedAccess is currently ignored, since we currently always use the full user tokens async authenticate(token: string): Promise { - const { sub, aud } = decodeJwt(token); + const { sub, aud, iss } = decodeJwt(token); + console.log(`DEBUG: iss=`, iss); + + if (iss === 'backstage-plugin') { + console.log('DO THE STUFF!'); + const { subject } = await this.pluginTokenHandler.verifyToken(token); + return createCredentialsWithServicePrincipal(subject); + } // # identify new token // 1. generate and store public keys in database diff --git a/packages/backend-app-api/src/services/implementations/publicKeyStore/publicKeyStoreServiceFactory.ts b/packages/backend-app-api/src/services/implementations/publicKeyStore/publicKeyStoreServiceFactory.ts index 58400bacf1..718aa641ed 100644 --- a/packages/backend-app-api/src/services/implementations/publicKeyStore/publicKeyStoreServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/publicKeyStore/publicKeyStoreServiceFactory.ts @@ -25,7 +25,8 @@ import { Knex } from 'knex'; import { JsonObject } from '@backstage/types'; import { resolvePackagePath } from '@backstage/backend-common'; -const TABLE = 'signing_keys'; +const MIGRATIONS_TABLE = 'backstage_backend_public_keys__knex_migrations'; +const TABLE = 'backstage_backend_public_keys__keys'; type Row = { id: string; @@ -40,6 +41,8 @@ export class DatabaseKeyStore implements PublicKeyStoreService { static async create(options: { database: DatabaseService }) { const { database } = options; + console.log(`DEBUG: ###### CREATING STORE`); + const client = await database.getClient(); if (!database.migrations?.skip) { await applyDatabaseMigrations(client); @@ -52,6 +55,7 @@ export class DatabaseKeyStore implements PublicKeyStoreService { key: JsonObject & { kid: string }; expiresAt: Date; }) { + console.log(`DEBUG: STORING KEY`, options); await this.client(TABLE).insert({ id: options.key.kid, key: JSON.stringify(options.key), @@ -111,5 +115,6 @@ export function applyDatabaseMigrations(knex: Knex): Promise { return knex.migrate.latest({ directory: migrationsDir, + tableName: MIGRATIONS_TABLE, }); } diff --git a/packages/backend-next/package.json b/packages/backend-next/package.json index afd591a336..12d1a7872b 100644 --- a/packages/backend-next/package.json +++ b/packages/backend-next/package.json @@ -18,11 +18,11 @@ "backstage" ], "scripts": { - "start": "backstage-cli package start", "build": "backstage-cli package build", + "clean": "backstage-cli package clean", "lint": "backstage-cli package lint", - "test": "backstage-cli package test", - "clean": "backstage-cli package clean" + "start": "backstage-cli package start", + "test": "backstage-cli package test" }, "dependencies": { "@backstage/backend-defaults": "workspace:^", @@ -66,7 +66,8 @@ "@backstage/plugin-signals-backend": "workspace:^", "@backstage/plugin-sonarqube-backend": "workspace:^", "@backstage/plugin-techdocs-backend": "workspace:^", - "@backstage/plugin-todo-backend": "workspace:^" + "@backstage/plugin-todo-backend": "workspace:^", + "express-promise-router": "^4.1.0" }, "devDependencies": { "@backstage/cli": "workspace:^" diff --git a/packages/backend-next/src/index.ts b/packages/backend-next/src/index.ts index 4965462b2c..47f1308c89 100644 --- a/packages/backend-next/src/index.ts +++ b/packages/backend-next/src/index.ts @@ -15,48 +15,113 @@ */ import { createBackend } from '@backstage/backend-defaults'; +import { createBackendPlugin } from '@backstage/backend-plugin-api'; +import Router from 'express-promise-router'; +import { coreServices } from '@backstage/backend-plugin-api'; const backend = createBackend(); -backend.add(import('@backstage/plugin-auth-backend')); -backend.add(import('./authModuleGithubProvider')); -backend.add(import('@backstage/plugin-auth-backend-module-guest-provider')); +backend.add( + createBackendPlugin({ + pluginId: 'receiver', + register(reg) { + reg.registerInit({ + deps: { + httpRouter: coreServices.httpRouter, + httpAuth: coreServices.httpAuth, + }, + async init({ httpRouter, httpAuth }) { + const router = Router(); + router.get('/hello', async (req, res) => { + console.log('Got hello'); + const credentials = await httpAuth.credentials(req); + console.log('Got credentials', credentials); -backend.add(import('@backstage/plugin-adr-backend')); -backend.add(import('@backstage/plugin-app-backend/alpha')); -backend.add(import('@backstage/plugin-azure-devops-backend')); -backend.add(import('@backstage/plugin-badges-backend')); -backend.add(import('@backstage/plugin-catalog-backend-module-unprocessed')); -backend.add( - import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'), + res.json({ hello: 'world' }); + }); + httpRouter.use(router); + }, + }); + }, + }), ); -backend.add(import('@backstage/plugin-catalog-backend/alpha')); -backend.add(import('@backstage/plugin-devtools-backend')); -backend.add(import('@backstage/plugin-entity-feedback-backend')); -backend.add(import('@backstage/plugin-jenkins-backend')); -backend.add(import('@backstage/plugin-kubernetes-backend/alpha')); -backend.add(import('@backstage/plugin-lighthouse-backend')); -backend.add(import('@backstage/plugin-linguist-backend')); -backend.add(import('@backstage/plugin-playlist-backend')); -backend.add(import('@backstage/plugin-nomad-backend')); + backend.add( - import('@backstage/plugin-permission-backend-module-allow-all-policy'), + createBackendPlugin({ + pluginId: 'caller', + register(reg) { + reg.registerInit({ + deps: { + rootLifecycle: coreServices.rootLifecycle, + auth: coreServices.auth, + discovery: coreServices.discovery, + httpRouter: coreServices.httpRouter, + }, + async init({ rootLifecycle, auth, discovery }) { + rootLifecycle.addStartupHook(async () => { + const { token } = await auth.getPluginRequestToken({ + onBehalfOf: await auth.getOwnServiceCredentials(), + targetPluginId: 'receiver', + }); + console.log(`DEBUG: token=`, token); + const res = await fetch( + `${await discovery.getBaseUrl('receiver')}/hello`, + { + headers: { + authorization: `Bearer ${token}`, + }, + }, + ); + console.log( + `DEBUG: res ${res.status} ${res.statusText}`, + await res.json(), + ); + }); + }, + }); + }, + }), ); -backend.add(import('@backstage/plugin-permission-backend/alpha')); -backend.add(import('@backstage/plugin-proxy-backend/alpha')); -backend.add(import('@backstage/plugin-scaffolder-backend/alpha')); -backend.add(import('@backstage/plugin-scaffolder-backend-module-github')); -backend.add(import('@backstage/plugin-search-backend-module-catalog/alpha')); -backend.add(import('@backstage/plugin-search-backend-module-explore/alpha')); -backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha')); -backend.add( - import('@backstage/plugin-catalog-backend-module-backstage-openapi'), -); -backend.add(import('@backstage/plugin-search-backend/alpha')); -backend.add(import('@backstage/plugin-techdocs-backend/alpha')); -backend.add(import('@backstage/plugin-todo-backend')); -backend.add(import('@backstage/plugin-sonarqube-backend')); -backend.add(import('@backstage/plugin-signals-backend')); -backend.add(import('@backstage/plugin-notifications-backend')); + +// backend.add(import('@backstage/plugin-auth-backend')); +// backend.add(import('./authModuleGithubProvider')); +// backend.add(import('@backstage/plugin-auth-backend-module-guest-provider')); + +// backend.add(import('@backstage/plugin-adr-backend')); +// backend.add(import('@backstage/plugin-app-backend/alpha')); +// backend.add(import('@backstage/plugin-azure-devops-backend')); +// backend.add(import('@backstage/plugin-badges-backend')); +// backend.add(import('@backstage/plugin-catalog-backend-module-unprocessed')); +// backend.add( +// import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'), +// ); +// backend.add(import('@backstage/plugin-catalog-backend/alpha')); +// backend.add(import('@backstage/plugin-devtools-backend')); +// backend.add(import('@backstage/plugin-entity-feedback-backend')); +// backend.add(import('@backstage/plugin-jenkins-backend')); +// backend.add(import('@backstage/plugin-kubernetes-backend/alpha')); +// backend.add(import('@backstage/plugin-lighthouse-backend')); +// backend.add(import('@backstage/plugin-linguist-backend')); +// backend.add(import('@backstage/plugin-playlist-backend')); +// backend.add(import('@backstage/plugin-nomad-backend')); +// backend.add( +// import('@backstage/plugin-permission-backend-module-allow-all-policy'), +// ); +// backend.add(import('@backstage/plugin-permission-backend/alpha')); +// backend.add(import('@backstage/plugin-proxy-backend/alpha')); +// backend.add(import('@backstage/plugin-scaffolder-backend/alpha')); +// backend.add(import('@backstage/plugin-scaffolder-backend-module-github')); +// backend.add(import('@backstage/plugin-search-backend-module-catalog/alpha')); +// backend.add(import('@backstage/plugin-search-backend-module-explore/alpha')); +// backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha')); +// backend.add( +// import('@backstage/plugin-catalog-backend-module-backstage-openapi'), +// ); +// backend.add(import('@backstage/plugin-search-backend/alpha')); +// backend.add(import('@backstage/plugin-techdocs-backend/alpha')); +// backend.add(import('@backstage/plugin-todo-backend')); +// backend.add(import('@backstage/plugin-sonarqube-backend')); +// backend.add(import('@backstage/plugin-signals-backend')); +// backend.add(import('@backstage/plugin-notifications-backend')); backend.start();