auth-backend: initial identity provider implementation
This commit is contained in:
@@ -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');
|
||||
};
|
||||
@@ -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>;
|
||||
};
|
||||
Reference in New Issue
Block a user