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:
@@ -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
|
||||
|
||||
+6
-1
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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:^"
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user