auth-backend: initial identity provider implementation

This commit is contained in:
Patrik Oldsberg
2020-06-18 12:56:54 +02:00
parent cddfff19c8
commit 0a305d7f70
8 changed files with 354 additions and 1 deletions
@@ -0,0 +1,49 @@
/*
* Copyright 2020 Spotify AB
*
* 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.
*/
// @ts-check
/**
* @param {import('knex')} knex
*/
exports.up = async function up(knex) {
return knex.schema.createTable('signing_keys', table => {
table.comment(
'Signing keys that are currently in use or have recently been used to issue tokens',
);
table
.string('kid')
.primary()
.notNullable()
.comment('ID of the signing key');
table
.timestamp('created_at')
.notNullable()
.defaultTo(knex.fn.now())
.comment('The creation time of the key');
table
.string('key')
.notNullable()
.comment('The serialized public part of the signing key');
});
};
/**
* @param {import('knex')} knex
*/
exports.down = async function down(knex) {
return knex.schema.dropTable('auth_keystore');
};
+3 -1
View File
@@ -32,7 +32,9 @@
"express-promise-router": "^3.0.3",
"fs-extra": "^9.0.0",
"helmet": "^3.22.0",
"jose": "^1.27.1",
"jwt-decode": "2.2.0",
"knex": "^0.21.1",
"morgan": "^1.10.0",
"passport": "^0.4.1",
"passport-github2": "^0.1.12",
@@ -46,10 +48,10 @@
"@types/body-parser": "^1.19.0",
"@types/cookie-parser": "^1.4.2",
"@types/jwt-decode": "2.2.1",
"@types/passport": "^1.0.3",
"@types/passport-github2": "^1.2.4",
"@types/passport-google-oauth20": "^2.0.3",
"@types/passport-saml": "^1.1.2",
"@types/passport": "^1.0.3",
"jest-fetch-mock": "^3.0.3"
},
"files": [
@@ -0,0 +1,76 @@
/*
* Copyright 2020 Spotify AB
*
* 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 Knex from 'knex';
import path from 'path';
import { Logger } from 'winston';
import { PublicKey } from './types';
const migrationsDir = path.resolve(
require.resolve('@backstage/plugin-auth-backend/package.json'),
'../migrations',
);
const TABLE = 'signing_keys';
type Row = {
created_at: Date;
kid: string;
key: string;
};
type Options = {
logger: Logger;
database: Knex;
};
export class DatabaseKeyStore {
static async create(options: Options): Promise<DatabaseKeyStore> {
const { logger, database } = options;
await database.migrate.latest({
directory: migrationsDir,
});
return new DatabaseKeyStore({ logger, database });
}
private readonly logger: Logger;
private readonly database: Knex;
private constructor(options: Options) {
const { logger, database } = options;
this.database = database;
this.logger = logger.child({ service: 'key-store' });
}
async addPublicKey(key: PublicKey): Promise<void> {
this.logger.info(`Storing public key ${key.kid}`);
await this.database<Row>(TABLE).insert({
kid: key.kid,
key: JSON.stringify(key),
});
console.log(`DEBUG: stored key`);
}
async listPublicKeys(): Promise<PublicKey[]> {
const rows = await this.database<Row>(TABLE).select();
console.log('DEBUG: rows =', rows);
return rows.map(row => JSON.parse(row.key));
}
}
@@ -0,0 +1,85 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { TokenIssuer, TokenParams, KeyStore, PublicKey } from './types';
import { JSONWebKey, JWK, JWS } from 'jose';
import { Logger } from 'winston';
type Options = {
issuer: string;
logger: Logger;
keyStore: KeyStore;
};
export class TokenFactory implements TokenIssuer {
private readonly issuer: string;
private readonly logger: Logger;
private readonly keyStore: KeyStore;
private privateKey?: Promise<JSONWebKey>;
constructor(options: Options) {
const { issuer, logger, keyStore } = options;
this.issuer = issuer;
this.keyStore = keyStore;
this.logger = logger.child({ service: 'issuer' });
}
async issueToken(claims: TokenParams): Promise<string> {
const key = await this.getKey();
const iss = this.issuer;
const sub = claims.sub;
const aud = 'backstage';
const iat = (Date.now() / 1000) | 0;
const exp = iat + 3600;
this.logger.info(`Issuing token for ${sub}`);
return JWS.sign({ iss, sub, aud, iat, exp }, key, {
alg: key.alg,
kid: key.kid,
});
}
private async getKey(): Promise<JSONWebKey> {
if (this.privateKey) {
return this.privateKey;
}
this.privateKey = (async () => {
const dateStr = new Date().toISOString();
const randStr = Math.random().toString(36).slice(2, 6);
const kid = `key-${dateStr}-${randStr}`;
const key = await JWK.generate('EC', 'P-384', { use: 'sig', kid });
await this.keyStore.addPublicKey(
(key.toJWK(false) as unknown) as PublicKey,
);
return key as JSONWebKey;
})();
try {
await this.privateKey;
} catch (error) {
this.logger.error(`Failed to generate signing key, ${error}`);
}
return this.privateKey;
}
}
@@ -0,0 +1,20 @@
/*
* Copyright 2020 Spotify AB
*
* 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.
*/
export { createOidcRouter } from './router';
export { TokenFactory } from './TokenFactory';
export { DatabaseKeyStore } from './DatabaseKeyStore';
export type { KeyStore, TokenIssuer, TokenParams } from './types';
@@ -0,0 +1,73 @@
/*
* Copyright 2020 Spotify AB
*
* 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 Router from 'express-promise-router';
import { Logger } from 'winston';
import { KeyStore } from './types';
export type Options = {
logger: Logger;
baseUrl: string;
keyStore: KeyStore;
};
export function createOidcRouter(options: Options) {
const {
logger: baseLogger,
keyStore,
baseUrl = 'http://localhost:7000/auth',
} = options;
const logger = baseLogger.child({ router: 'identity' });
const router = Router();
const config = {
issuer: baseUrl,
token_endpoint: `${baseUrl}/v1/token`,
userinfo_endpoint: `${baseUrl}/v1/userinfo`,
jwks_uri: `${baseUrl}/v1/certs`,
response_types_supported: ['id_token'],
subject_types_supported: ['public'],
id_token_signing_alg_values_supported: ['RS256'],
scopes_supported: ['openid'],
token_endpoint_auth_methods_supported: [],
claims_supported: ['sub'],
grant_types_supported: [],
};
router.get('/.well-known/openid-configuration', (_req, res) => {
logger.info('request configuration');
res.json(config);
});
router.get('/v1/token', (_req, res) => {
logger.info('request token');
res.status(501).send('Not Implemented');
});
router.get('/v1/userinfo', (_req, res) => {
logger.info('request userinfo');
res.status(501).send('Not Implemented');
});
router.get('/v1/certs', async (_req, res) => {
logger.info('request certs');
const keys = await keyStore.listPublicKeys();
res.json(keys);
});
return router;
}
@@ -0,0 +1,36 @@
/*
* Copyright 2020 Spotify AB
*
* 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.
*/
export interface PublicKey extends Record<string, string> {
use: 'sig' | 'enc';
alg: string;
kid: string;
kty: string;
}
export type KeyStore = {
addPublicKey(key: PublicKey): Promise<void>;
listPublicKeys(): Promise<PublicKey[]>;
};
export type TokenParams = {
sub: string;
};
export type TokenIssuer = {
issueToken(claims: TokenParams): Promise<string>;
};