backend-app-api: test service to service auth

Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com>
Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>
This commit is contained in:
Vincenzo Scamporlino
2024-03-27 12:57:43 +01:00
parent f1a0569506
commit ed99c79cf1
6 changed files with 161 additions and 55 deletions
@@ -21,17 +21,20 @@
* @returns { Promise<void> }
*/
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<void> }
*/
exports.down = async function down(knex) {
return knex.schema.dropTable('signing_keys');
return knex.schema.dropTable('backstage_backend_public_keys__keys');
};
@@ -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<JWK> {
@@ -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,
@@ -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<BackstageCredentials> {
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
@@ -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<Row>(TABLE).insert({
id: options.key.kid,
key: JSON.stringify(options.key),
@@ -111,5 +115,6 @@ export function applyDatabaseMigrations(knex: Knex): Promise<void> {
return knex.migrate.latest({
directory: migrationsDir,
tableName: MIGRATIONS_TABLE,
});
}
+5 -4
View File
@@ -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:^"
+101 -36
View File
@@ -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();