@backstage/plugin-auth-backend 0.10.0-next.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs.js CHANGED
@@ -20,17 +20,18 @@ var passportGithub2 = require('passport-github2');
20
20
  var passportGitlab2 = require('passport-gitlab2');
21
21
  var passportGoogleOauth20 = require('passport-google-oauth20');
22
22
  var passportMicrosoft = require('passport-microsoft');
23
- var uuid = require('uuid');
24
- var luxon = require('luxon');
25
- var backendCommon = require('@backstage/backend-common');
26
- var firestore = require('@google-cloud/firestore');
27
- var lodash = require('lodash');
23
+ var pluginAuthNode = require('@backstage/plugin-auth-node');
28
24
  var openidClient = require('openid-client');
29
25
  var passportOktaOauth = require('passport-okta-oauth');
30
26
  var passportOneloginOauth = require('passport-onelogin-oauth');
31
27
  var passportSaml = require('passport-saml');
32
28
  var googleAuthLibrary = require('google-auth-library');
33
29
  var catalogClient = require('@backstage/catalog-client');
30
+ var uuid = require('uuid');
31
+ var luxon = require('luxon');
32
+ var backendCommon = require('@backstage/backend-common');
33
+ var firestore = require('@google-cloud/firestore');
34
+ var lodash = require('lodash');
34
35
  var session = require('express-session');
35
36
  var passport = require('passport');
36
37
  var minimatch = require('minimatch');
@@ -1934,462 +1935,122 @@ const createOAuth2Provider = (options) => {
1934
1935
  });
1935
1936
  };
1936
1937
 
1937
- function createOidcRouter(options) {
1938
- const { baseUrl, tokenIssuer } = options;
1939
- const router = Router__default["default"]();
1940
- const config = {
1941
- issuer: baseUrl,
1942
- token_endpoint: `${baseUrl}/v1/token`,
1943
- userinfo_endpoint: `${baseUrl}/v1/userinfo`,
1944
- jwks_uri: `${baseUrl}/.well-known/jwks.json`,
1945
- response_types_supported: ["id_token"],
1946
- subject_types_supported: ["public"],
1947
- id_token_signing_alg_values_supported: ["RS256"],
1948
- scopes_supported: ["openid"],
1949
- token_endpoint_auth_methods_supported: [],
1950
- claims_supported: ["sub"],
1951
- grant_types_supported: []
1952
- };
1953
- router.get("/.well-known/openid-configuration", (_req, res) => {
1954
- res.json(config);
1955
- });
1956
- router.get("/.well-known/jwks.json", async (_req, res) => {
1957
- const { keys } = await tokenIssuer.listPublicKeys();
1958
- res.json({ keys });
1959
- });
1960
- router.get("/v1/token", (_req, res) => {
1961
- res.status(501).send("Not Implemented");
1962
- });
1963
- router.get("/v1/userinfo", (_req, res) => {
1964
- res.status(501).send("Not Implemented");
1965
- });
1966
- return router;
1967
- }
1968
-
1969
- const CLOCK_MARGIN_S = 10;
1970
- class IdentityClient {
1938
+ const OAUTH2_PROXY_JWT_HEADER = "X-OAUTH2-PROXY-ID-TOKEN";
1939
+ class Oauth2ProxyAuthProvider {
1971
1940
  constructor(options) {
1972
- this.discovery = options.discovery;
1973
- this.issuer = options.issuer;
1974
- this.keyStore = new jose.JWKS.KeyStore();
1975
- this.keyStoreUpdated = 0;
1941
+ this.catalogIdentityClient = options.catalogIdentityClient;
1942
+ this.logger = options.logger;
1943
+ this.tokenIssuer = options.tokenIssuer;
1944
+ this.signInResolver = options.signInResolver;
1945
+ this.authHandler = options.authHandler;
1976
1946
  }
1977
- async authenticate(token) {
1978
- var _a;
1979
- if (!token) {
1980
- throw new errors.AuthenticationError("No token specified");
1981
- }
1982
- const key = await this.getKey(token);
1983
- if (!key) {
1984
- throw new errors.AuthenticationError("No signing key matching token found");
1985
- }
1986
- const decoded = jose.JWT.IdToken.verify(token, key, {
1987
- algorithms: ["ES256"],
1988
- audience: "backstage",
1989
- issuer: this.issuer
1990
- });
1991
- if (!decoded.sub) {
1992
- throw new errors.AuthenticationError("No user sub found in token");
1993
- }
1994
- const user = {
1995
- id: decoded.sub,
1996
- token,
1997
- identity: {
1998
- type: "user",
1999
- userEntityRef: decoded.sub,
2000
- ownershipEntityRefs: (_a = decoded.ent) != null ? _a : []
2001
- }
2002
- };
2003
- return user;
1947
+ frameHandler() {
1948
+ return Promise.resolve(void 0);
2004
1949
  }
2005
- static getBearerToken(authorizationHeader) {
2006
- if (typeof authorizationHeader !== "string") {
2007
- return void 0;
1950
+ async refresh(req, res) {
1951
+ try {
1952
+ const result = this.getResult(req);
1953
+ const response = await this.handleResult(result);
1954
+ res.json(response);
1955
+ } catch (e) {
1956
+ this.logger.error(`Exception occurred during ${OAUTH2_PROXY_JWT_HEADER} refresh`, e);
1957
+ res.status(401);
1958
+ res.end();
2008
1959
  }
2009
- const matches = authorizationHeader.match(/Bearer\s+(\S+)/i);
2010
- return matches == null ? void 0 : matches[1];
2011
1960
  }
2012
- async getKey(rawJwtToken) {
2013
- const { header, payload } = jose.JWT.decode(rawJwtToken, {
2014
- complete: true
2015
- });
2016
- const keyStoreHasKey = !!this.keyStore.get({ kid: header.kid });
2017
- const issuedAfterLastRefresh = (payload == null ? void 0 : payload.iat) && payload.iat > this.keyStoreUpdated - CLOCK_MARGIN_S;
2018
- if (!keyStoreHasKey && issuedAfterLastRefresh) {
2019
- await this.refreshKeyStore();
2020
- }
2021
- return this.keyStore.get({ kid: header.kid });
1961
+ start() {
1962
+ return Promise.resolve(void 0);
2022
1963
  }
2023
- async listPublicKeys() {
2024
- const url = `${await this.discovery.getBaseUrl("auth")}/.well-known/jwks.json`;
2025
- const response = await fetch__default["default"](url);
2026
- if (!response.ok) {
2027
- const payload = await response.text();
2028
- const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
2029
- throw new Error(message);
2030
- }
2031
- const publicKeys = await response.json();
2032
- return publicKeys;
1964
+ async handleResult(result) {
1965
+ const ctx = {
1966
+ logger: this.logger,
1967
+ tokenIssuer: this.tokenIssuer,
1968
+ catalogIdentityClient: this.catalogIdentityClient
1969
+ };
1970
+ const { profile } = await this.authHandler(result, ctx);
1971
+ const backstageSignInResult = await this.signInResolver({
1972
+ result,
1973
+ profile
1974
+ }, ctx);
1975
+ return {
1976
+ providerInfo: {
1977
+ accessToken: result.accessToken
1978
+ },
1979
+ backstageIdentity: prepareBackstageIdentityResponse(backstageSignInResult),
1980
+ profile
1981
+ };
2033
1982
  }
2034
- async refreshKeyStore() {
2035
- const now = Date.now() / 1e3;
2036
- const publicKeys = await this.listPublicKeys();
2037
- this.keyStore = jose.JWKS.asKeyStore({
2038
- keys: publicKeys.keys.map((key) => key)
2039
- });
2040
- this.keyStoreUpdated = now;
1983
+ getResult(req) {
1984
+ const authHeader = req.header(OAUTH2_PROXY_JWT_HEADER);
1985
+ const jwt = pluginAuthNode.getBearerTokenFromAuthorizationHeader(authHeader);
1986
+ if (!jwt) {
1987
+ throw new errors.AuthenticationError(`Missing or in incorrect format - Oauth2Proxy OIDC header: ${OAUTH2_PROXY_JWT_HEADER}`);
1988
+ }
1989
+ const decodedJWT = jose.JWT.decode(jwt);
1990
+ return {
1991
+ fullProfile: decodedJWT,
1992
+ accessToken: jwt
1993
+ };
2041
1994
  }
2042
1995
  }
1996
+ const createOauth2ProxyProvider = (options) => ({ catalogApi, logger, tokenIssuer, tokenManager }) => {
1997
+ const signInResolver = options.signIn.resolver;
1998
+ const authHandler = options.authHandler;
1999
+ const catalogIdentityClient = new CatalogIdentityClient({
2000
+ catalogApi,
2001
+ tokenManager
2002
+ });
2003
+ return new Oauth2ProxyAuthProvider({
2004
+ logger,
2005
+ signInResolver,
2006
+ authHandler,
2007
+ tokenIssuer,
2008
+ catalogIdentityClient
2009
+ });
2010
+ };
2043
2011
 
2044
- const MS_IN_S = 1e3;
2045
- class TokenFactory {
2012
+ class OidcAuthProvider {
2046
2013
  constructor(options) {
2047
- this.issuer = options.issuer;
2014
+ this.implementation = this.setupStrategy(options);
2015
+ this.scope = options.scope;
2016
+ this.prompt = options.prompt;
2017
+ this.signInResolver = options.signInResolver;
2018
+ this.authHandler = options.authHandler;
2019
+ this.tokenIssuer = options.tokenIssuer;
2020
+ this.catalogIdentityClient = options.catalogIdentityClient;
2048
2021
  this.logger = options.logger;
2049
- this.keyStore = options.keyStore;
2050
- this.keyDurationSeconds = options.keyDurationSeconds;
2051
2022
  }
2052
- async issueToken(params) {
2053
- const key = await this.getKey();
2054
- const iss = this.issuer;
2055
- const sub = params.claims.sub;
2056
- const ent = params.claims.ent;
2057
- const aud = "backstage";
2058
- const iat = Math.floor(Date.now() / MS_IN_S);
2059
- const exp = iat + this.keyDurationSeconds;
2060
- this.logger.info(`Issuing token for ${sub}, with entities ${ent != null ? ent : []}`);
2061
- return jose.JWS.sign({ iss, sub, aud, iat, exp, ent }, key, {
2062
- alg: key.alg,
2063
- kid: key.kid
2064
- });
2065
- }
2066
- async listPublicKeys() {
2067
- const { items: keys } = await this.keyStore.listKeys();
2068
- const validKeys = [];
2069
- const expiredKeys = [];
2070
- for (const key of keys) {
2071
- const expireAt = luxon.DateTime.fromJSDate(key.createdAt).plus({
2072
- seconds: 3 * this.keyDurationSeconds
2073
- });
2074
- if (expireAt < luxon.DateTime.local()) {
2075
- expiredKeys.push(key);
2076
- } else {
2077
- validKeys.push(key);
2078
- }
2079
- }
2080
- if (expiredKeys.length > 0) {
2081
- const kids = expiredKeys.map(({ key }) => key.kid);
2082
- this.logger.info(`Removing expired signing keys, '${kids.join("', '")}'`);
2083
- this.keyStore.removeKeys(kids).catch((error) => {
2084
- this.logger.error(`Failed to remove expired keys, ${error}`);
2085
- });
2086
- }
2087
- return { keys: validKeys.map(({ key }) => key) };
2088
- }
2089
- async getKey() {
2090
- if (this.privateKeyPromise) {
2091
- if (this.keyExpiry && luxon.DateTime.fromJSDate(this.keyExpiry) > luxon.DateTime.local()) {
2092
- return this.privateKeyPromise;
2093
- }
2094
- this.logger.info(`Signing key has expired, generating new key`);
2095
- delete this.privateKeyPromise;
2096
- }
2097
- this.keyExpiry = luxon.DateTime.utc().plus({
2098
- seconds: this.keyDurationSeconds
2099
- }).toJSDate();
2100
- const promise = (async () => {
2101
- const key = await jose.JWK.generate("EC", "P-256", {
2102
- use: "sig",
2103
- kid: uuid.v4(),
2104
- alg: "ES256"
2105
- });
2106
- this.logger.info(`Created new signing key ${key.kid}`);
2107
- await this.keyStore.addKey(key.toJWK(false));
2108
- return key;
2109
- })();
2110
- this.privateKeyPromise = promise;
2111
- try {
2112
- await promise;
2113
- } catch (error) {
2114
- this.logger.error(`Failed to generate new signing key, ${error}`);
2115
- delete this.keyExpiry;
2116
- delete this.privateKeyPromise;
2023
+ async start(req) {
2024
+ const { strategy } = await this.implementation;
2025
+ const options = {
2026
+ scope: req.scope || this.scope || "openid profile email",
2027
+ state: encodeState(req.state)
2028
+ };
2029
+ const prompt = this.prompt || "none";
2030
+ if (prompt !== "auto") {
2031
+ options.prompt = prompt;
2117
2032
  }
2118
- return promise;
2119
- }
2120
- }
2121
-
2122
- const migrationsDir = backendCommon.resolvePackagePath("@backstage/plugin-auth-backend", "migrations");
2123
- const TABLE = "signing_keys";
2124
- const parseDate = (date) => {
2125
- const parsedDate = typeof date === "string" ? luxon.DateTime.fromSQL(date, { zone: "UTC" }) : luxon.DateTime.fromJSDate(date);
2126
- if (!parsedDate.isValid) {
2127
- throw new Error(`Failed to parse date, reason: ${parsedDate.invalidReason}, explanation: ${parsedDate.invalidExplanation}`);
2128
- }
2129
- return parsedDate.toJSDate();
2130
- };
2131
- class DatabaseKeyStore {
2132
- static async create(options) {
2133
- const { database } = options;
2134
- await database.migrate.latest({
2135
- directory: migrationsDir
2136
- });
2137
- return new DatabaseKeyStore(options);
2138
- }
2139
- constructor(options) {
2140
- this.database = options.database;
2141
- }
2142
- async addKey(key) {
2143
- await this.database(TABLE).insert({
2144
- kid: key.kid,
2145
- key: JSON.stringify(key)
2146
- });
2033
+ return await executeRedirectStrategy(req, strategy, options);
2147
2034
  }
2148
- async listKeys() {
2149
- const rows = await this.database(TABLE).select();
2035
+ async handler(req) {
2036
+ const { strategy } = await this.implementation;
2037
+ const { result, privateInfo } = await executeFrameHandlerStrategy(req, strategy);
2150
2038
  return {
2151
- items: rows.map((row) => ({
2152
- key: JSON.parse(row.key),
2153
- createdAt: parseDate(row.created_at)
2154
- }))
2039
+ response: await this.handleResult(result),
2040
+ refreshToken: privateInfo.refreshToken
2155
2041
  };
2156
2042
  }
2157
- async removeKeys(kids) {
2158
- await this.database(TABLE).delete().whereIn("kid", kids);
2159
- }
2160
- }
2161
-
2162
- class MemoryKeyStore {
2163
- constructor() {
2164
- this.keys = /* @__PURE__ */ new Map();
2165
- }
2166
- async addKey(key) {
2167
- this.keys.set(key.kid, {
2168
- createdAt: luxon.DateTime.utc().toJSDate(),
2169
- key: JSON.stringify(key)
2170
- });
2171
- }
2172
- async removeKeys(kids) {
2173
- for (const kid of kids) {
2174
- this.keys.delete(kid);
2175
- }
2176
- }
2177
- async listKeys() {
2178
- return {
2179
- items: Array.from(this.keys).map(([, { createdAt, key: keyStr }]) => ({
2180
- createdAt,
2181
- key: JSON.parse(keyStr)
2182
- }))
2183
- };
2184
- }
2185
- }
2186
-
2187
- const DEFAULT_TIMEOUT_MS = 1e4;
2188
- const DEFAULT_DOCUMENT_PATH = "sessions";
2189
- class FirestoreKeyStore {
2190
- constructor(database, path, timeout) {
2191
- this.database = database;
2192
- this.path = path;
2193
- this.timeout = timeout;
2194
- }
2195
- static async create(settings) {
2196
- const { path, timeout, ...firestoreSettings } = settings != null ? settings : {};
2197
- const database = new firestore.Firestore(firestoreSettings);
2198
- return new FirestoreKeyStore(database, path != null ? path : DEFAULT_DOCUMENT_PATH, timeout != null ? timeout : DEFAULT_TIMEOUT_MS);
2199
- }
2200
- static async verifyConnection(keyStore, logger) {
2201
- try {
2202
- await keyStore.verify();
2203
- } catch (error) {
2204
- if (process.env.NODE_ENV !== "development") {
2205
- throw new Error(`Failed to connect to database: ${error.message}`);
2206
- }
2207
- logger == null ? void 0 : logger.warn(`Failed to connect to database: ${error.message}`);
2208
- }
2209
- }
2210
- async addKey(key) {
2211
- await this.withTimeout(this.database.collection(this.path).doc(key.kid).set({
2212
- kid: key.kid,
2213
- key: JSON.stringify(key)
2214
- }));
2215
- }
2216
- async listKeys() {
2217
- const keys = await this.withTimeout(this.database.collection(this.path).get());
2218
- return {
2219
- items: keys.docs.map((key) => ({
2220
- key: key.data(),
2221
- createdAt: key.createTime.toDate()
2222
- }))
2223
- };
2224
- }
2225
- async removeKeys(kids) {
2226
- for (const kid of kids) {
2227
- await this.withTimeout(this.database.collection(this.path).doc(kid).delete());
2228
- }
2229
- }
2230
- async withTimeout(operation) {
2231
- const timer = new Promise((_, reject) => setTimeout(() => {
2232
- reject(new Error(`Operation timed out after ${this.timeout}ms`));
2233
- }, this.timeout));
2234
- return Promise.race([operation, timer]);
2235
- }
2236
- async verify() {
2237
- await this.withTimeout(this.database.collection(this.path).limit(1).get());
2238
- }
2239
- }
2240
-
2241
- class KeyStores {
2242
- static async fromConfig(config, options) {
2243
- var _a;
2244
- const { logger, database } = options != null ? options : {};
2245
- const ks = config.getOptionalConfig("auth.keyStore");
2246
- const provider = (_a = ks == null ? void 0 : ks.getOptionalString("provider")) != null ? _a : "database";
2247
- logger == null ? void 0 : logger.info(`Configuring "${provider}" as KeyStore provider`);
2248
- if (provider === "database") {
2249
- if (!database) {
2250
- throw new Error("This KeyStore provider requires a database");
2251
- }
2252
- return await DatabaseKeyStore.create({
2253
- database: await database.getClient()
2254
- });
2255
- }
2256
- if (provider === "memory") {
2257
- return new MemoryKeyStore();
2258
- }
2259
- if (provider === "firestore") {
2260
- const settings = ks == null ? void 0 : ks.getConfig(provider);
2261
- const keyStore = await FirestoreKeyStore.create(lodash.pickBy({
2262
- projectId: settings == null ? void 0 : settings.getOptionalString("projectId"),
2263
- keyFilename: settings == null ? void 0 : settings.getOptionalString("keyFilename"),
2264
- host: settings == null ? void 0 : settings.getOptionalString("host"),
2265
- port: settings == null ? void 0 : settings.getOptionalNumber("port"),
2266
- ssl: settings == null ? void 0 : settings.getOptionalBoolean("ssl"),
2267
- path: settings == null ? void 0 : settings.getOptionalString("path"),
2268
- timeout: settings == null ? void 0 : settings.getOptionalNumber("timeout")
2269
- }, (value) => value !== void 0));
2270
- await FirestoreKeyStore.verifyConnection(keyStore, logger);
2271
- return keyStore;
2272
- }
2273
- throw new Error(`Unknown KeyStore provider: ${provider}`);
2274
- }
2275
- }
2276
-
2277
- const OAUTH2_PROXY_JWT_HEADER = "X-OAUTH2-PROXY-ID-TOKEN";
2278
- class Oauth2ProxyAuthProvider {
2279
- constructor(options) {
2280
- this.catalogIdentityClient = options.catalogIdentityClient;
2281
- this.logger = options.logger;
2282
- this.tokenIssuer = options.tokenIssuer;
2283
- this.signInResolver = options.signInResolver;
2284
- this.authHandler = options.authHandler;
2285
- }
2286
- frameHandler() {
2287
- return Promise.resolve(void 0);
2288
- }
2289
- async refresh(req, res) {
2290
- try {
2291
- const result = this.getResult(req);
2292
- const response = await this.handleResult(result);
2293
- res.json(response);
2294
- } catch (e) {
2295
- this.logger.error(`Exception occurred during ${OAUTH2_PROXY_JWT_HEADER} refresh`, e);
2296
- res.status(401);
2297
- res.end();
2298
- }
2299
- }
2300
- start() {
2301
- return Promise.resolve(void 0);
2302
- }
2303
- async handleResult(result) {
2304
- const ctx = {
2305
- logger: this.logger,
2306
- tokenIssuer: this.tokenIssuer,
2307
- catalogIdentityClient: this.catalogIdentityClient
2308
- };
2309
- const { profile } = await this.authHandler(result, ctx);
2310
- const backstageSignInResult = await this.signInResolver({
2311
- result,
2312
- profile
2313
- }, ctx);
2314
- return {
2315
- providerInfo: {
2316
- accessToken: result.accessToken
2317
- },
2318
- backstageIdentity: prepareBackstageIdentityResponse(backstageSignInResult),
2319
- profile
2320
- };
2321
- }
2322
- getResult(req) {
2323
- const authHeader = req.header(OAUTH2_PROXY_JWT_HEADER);
2324
- const jwt = IdentityClient.getBearerToken(authHeader);
2325
- if (!jwt) {
2326
- throw new errors.AuthenticationError(`Missing or in incorrect format - Oauth2Proxy OIDC header: ${OAUTH2_PROXY_JWT_HEADER}`);
2327
- }
2328
- const decodedJWT = jose.JWT.decode(jwt);
2329
- return {
2330
- fullProfile: decodedJWT,
2331
- accessToken: jwt
2332
- };
2333
- }
2334
- }
2335
- const createOauth2ProxyProvider = (options) => ({ catalogApi, logger, tokenIssuer, tokenManager }) => {
2336
- const signInResolver = options.signIn.resolver;
2337
- const authHandler = options.authHandler;
2338
- const catalogIdentityClient = new CatalogIdentityClient({
2339
- catalogApi,
2340
- tokenManager
2341
- });
2342
- return new Oauth2ProxyAuthProvider({
2343
- logger,
2344
- signInResolver,
2345
- authHandler,
2346
- tokenIssuer,
2347
- catalogIdentityClient
2348
- });
2349
- };
2350
-
2351
- class OidcAuthProvider {
2352
- constructor(options) {
2353
- this.implementation = this.setupStrategy(options);
2354
- this.scope = options.scope;
2355
- this.prompt = options.prompt;
2356
- this.signInResolver = options.signInResolver;
2357
- this.authHandler = options.authHandler;
2358
- this.tokenIssuer = options.tokenIssuer;
2359
- this.catalogIdentityClient = options.catalogIdentityClient;
2360
- this.logger = options.logger;
2361
- }
2362
- async start(req) {
2363
- const { strategy } = await this.implementation;
2364
- const options = {
2365
- scope: req.scope || this.scope || "openid profile email",
2366
- state: encodeState(req.state)
2367
- };
2368
- const prompt = this.prompt || "none";
2369
- if (prompt !== "auto") {
2370
- options.prompt = prompt;
2371
- }
2372
- return await executeRedirectStrategy(req, strategy, options);
2373
- }
2374
- async handler(req) {
2375
- const { strategy } = await this.implementation;
2376
- const { result, privateInfo } = await executeFrameHandlerStrategy(req, strategy);
2377
- return {
2378
- response: await this.handleResult(result),
2379
- refreshToken: privateInfo.refreshToken
2380
- };
2381
- }
2382
- async refresh(req) {
2383
- const { client } = await this.implementation;
2384
- const tokenset = await client.refresh(req.refreshToken);
2385
- if (!tokenset.access_token) {
2386
- throw new Error("Refresh failed");
2387
- }
2388
- const userinfo = await client.userinfo(tokenset.access_token);
2389
- return {
2390
- response: await this.handleResult({ tokenset, userinfo }),
2391
- refreshToken: tokenset.refresh_token
2392
- };
2043
+ async refresh(req) {
2044
+ const { client } = await this.implementation;
2045
+ const tokenset = await client.refresh(req.refreshToken);
2046
+ if (!tokenset.access_token) {
2047
+ throw new Error("Refresh failed");
2048
+ }
2049
+ const userinfo = await client.userinfo(tokenset.access_token);
2050
+ return {
2051
+ response: await this.handleResult({ tokenset, userinfo }),
2052
+ refreshToken: tokenset.refresh_token
2053
+ };
2393
2054
  }
2394
2055
  async setupStrategy(options) {
2395
2056
  const issuer = await openidClient.Issuer.discover(options.metadataUrl);
@@ -3022,6 +2683,271 @@ const factories = {
3022
2683
  atlassian: createAtlassianProvider()
3023
2684
  };
3024
2685
 
2686
+ function createOidcRouter(options) {
2687
+ const { baseUrl, tokenIssuer } = options;
2688
+ const router = Router__default["default"]();
2689
+ const config = {
2690
+ issuer: baseUrl,
2691
+ token_endpoint: `${baseUrl}/v1/token`,
2692
+ userinfo_endpoint: `${baseUrl}/v1/userinfo`,
2693
+ jwks_uri: `${baseUrl}/.well-known/jwks.json`,
2694
+ response_types_supported: ["id_token"],
2695
+ subject_types_supported: ["public"],
2696
+ id_token_signing_alg_values_supported: ["RS256"],
2697
+ scopes_supported: ["openid"],
2698
+ token_endpoint_auth_methods_supported: [],
2699
+ claims_supported: ["sub"],
2700
+ grant_types_supported: []
2701
+ };
2702
+ router.get("/.well-known/openid-configuration", (_req, res) => {
2703
+ res.json(config);
2704
+ });
2705
+ router.get("/.well-known/jwks.json", async (_req, res) => {
2706
+ const { keys } = await tokenIssuer.listPublicKeys();
2707
+ res.json({ keys });
2708
+ });
2709
+ router.get("/v1/token", (_req, res) => {
2710
+ res.status(501).send("Not Implemented");
2711
+ });
2712
+ router.get("/v1/userinfo", (_req, res) => {
2713
+ res.status(501).send("Not Implemented");
2714
+ });
2715
+ return router;
2716
+ }
2717
+
2718
+ const MS_IN_S = 1e3;
2719
+ class TokenFactory {
2720
+ constructor(options) {
2721
+ this.issuer = options.issuer;
2722
+ this.logger = options.logger;
2723
+ this.keyStore = options.keyStore;
2724
+ this.keyDurationSeconds = options.keyDurationSeconds;
2725
+ }
2726
+ async issueToken(params) {
2727
+ const key = await this.getKey();
2728
+ const iss = this.issuer;
2729
+ const sub = params.claims.sub;
2730
+ const ent = params.claims.ent;
2731
+ const aud = "backstage";
2732
+ const iat = Math.floor(Date.now() / MS_IN_S);
2733
+ const exp = iat + this.keyDurationSeconds;
2734
+ this.logger.info(`Issuing token for ${sub}, with entities ${ent != null ? ent : []}`);
2735
+ return jose.JWS.sign({ iss, sub, aud, iat, exp, ent }, key, {
2736
+ alg: key.alg,
2737
+ kid: key.kid
2738
+ });
2739
+ }
2740
+ async listPublicKeys() {
2741
+ const { items: keys } = await this.keyStore.listKeys();
2742
+ const validKeys = [];
2743
+ const expiredKeys = [];
2744
+ for (const key of keys) {
2745
+ const expireAt = luxon.DateTime.fromJSDate(key.createdAt).plus({
2746
+ seconds: 3 * this.keyDurationSeconds
2747
+ });
2748
+ if (expireAt < luxon.DateTime.local()) {
2749
+ expiredKeys.push(key);
2750
+ } else {
2751
+ validKeys.push(key);
2752
+ }
2753
+ }
2754
+ if (expiredKeys.length > 0) {
2755
+ const kids = expiredKeys.map(({ key }) => key.kid);
2756
+ this.logger.info(`Removing expired signing keys, '${kids.join("', '")}'`);
2757
+ this.keyStore.removeKeys(kids).catch((error) => {
2758
+ this.logger.error(`Failed to remove expired keys, ${error}`);
2759
+ });
2760
+ }
2761
+ return { keys: validKeys.map(({ key }) => key) };
2762
+ }
2763
+ async getKey() {
2764
+ if (this.privateKeyPromise) {
2765
+ if (this.keyExpiry && luxon.DateTime.fromJSDate(this.keyExpiry) > luxon.DateTime.local()) {
2766
+ return this.privateKeyPromise;
2767
+ }
2768
+ this.logger.info(`Signing key has expired, generating new key`);
2769
+ delete this.privateKeyPromise;
2770
+ }
2771
+ this.keyExpiry = luxon.DateTime.utc().plus({
2772
+ seconds: this.keyDurationSeconds
2773
+ }).toJSDate();
2774
+ const promise = (async () => {
2775
+ const key = await jose.JWK.generate("EC", "P-256", {
2776
+ use: "sig",
2777
+ kid: uuid.v4(),
2778
+ alg: "ES256"
2779
+ });
2780
+ this.logger.info(`Created new signing key ${key.kid}`);
2781
+ await this.keyStore.addKey(key.toJWK(false));
2782
+ return key;
2783
+ })();
2784
+ this.privateKeyPromise = promise;
2785
+ try {
2786
+ await promise;
2787
+ } catch (error) {
2788
+ this.logger.error(`Failed to generate new signing key, ${error}`);
2789
+ delete this.keyExpiry;
2790
+ delete this.privateKeyPromise;
2791
+ }
2792
+ return promise;
2793
+ }
2794
+ }
2795
+
2796
+ const migrationsDir = backendCommon.resolvePackagePath("@backstage/plugin-auth-backend", "migrations");
2797
+ const TABLE = "signing_keys";
2798
+ const parseDate = (date) => {
2799
+ const parsedDate = typeof date === "string" ? luxon.DateTime.fromSQL(date, { zone: "UTC" }) : luxon.DateTime.fromJSDate(date);
2800
+ if (!parsedDate.isValid) {
2801
+ throw new Error(`Failed to parse date, reason: ${parsedDate.invalidReason}, explanation: ${parsedDate.invalidExplanation}`);
2802
+ }
2803
+ return parsedDate.toJSDate();
2804
+ };
2805
+ class DatabaseKeyStore {
2806
+ static async create(options) {
2807
+ const { database } = options;
2808
+ await database.migrate.latest({
2809
+ directory: migrationsDir
2810
+ });
2811
+ return new DatabaseKeyStore(options);
2812
+ }
2813
+ constructor(options) {
2814
+ this.database = options.database;
2815
+ }
2816
+ async addKey(key) {
2817
+ await this.database(TABLE).insert({
2818
+ kid: key.kid,
2819
+ key: JSON.stringify(key)
2820
+ });
2821
+ }
2822
+ async listKeys() {
2823
+ const rows = await this.database(TABLE).select();
2824
+ return {
2825
+ items: rows.map((row) => ({
2826
+ key: JSON.parse(row.key),
2827
+ createdAt: parseDate(row.created_at)
2828
+ }))
2829
+ };
2830
+ }
2831
+ async removeKeys(kids) {
2832
+ await this.database(TABLE).delete().whereIn("kid", kids);
2833
+ }
2834
+ }
2835
+
2836
+ class MemoryKeyStore {
2837
+ constructor() {
2838
+ this.keys = /* @__PURE__ */ new Map();
2839
+ }
2840
+ async addKey(key) {
2841
+ this.keys.set(key.kid, {
2842
+ createdAt: luxon.DateTime.utc().toJSDate(),
2843
+ key: JSON.stringify(key)
2844
+ });
2845
+ }
2846
+ async removeKeys(kids) {
2847
+ for (const kid of kids) {
2848
+ this.keys.delete(kid);
2849
+ }
2850
+ }
2851
+ async listKeys() {
2852
+ return {
2853
+ items: Array.from(this.keys).map(([, { createdAt, key: keyStr }]) => ({
2854
+ createdAt,
2855
+ key: JSON.parse(keyStr)
2856
+ }))
2857
+ };
2858
+ }
2859
+ }
2860
+
2861
+ const DEFAULT_TIMEOUT_MS = 1e4;
2862
+ const DEFAULT_DOCUMENT_PATH = "sessions";
2863
+ class FirestoreKeyStore {
2864
+ constructor(database, path, timeout) {
2865
+ this.database = database;
2866
+ this.path = path;
2867
+ this.timeout = timeout;
2868
+ }
2869
+ static async create(settings) {
2870
+ const { path, timeout, ...firestoreSettings } = settings != null ? settings : {};
2871
+ const database = new firestore.Firestore(firestoreSettings);
2872
+ return new FirestoreKeyStore(database, path != null ? path : DEFAULT_DOCUMENT_PATH, timeout != null ? timeout : DEFAULT_TIMEOUT_MS);
2873
+ }
2874
+ static async verifyConnection(keyStore, logger) {
2875
+ try {
2876
+ await keyStore.verify();
2877
+ } catch (error) {
2878
+ if (process.env.NODE_ENV !== "development") {
2879
+ throw new Error(`Failed to connect to database: ${error.message}`);
2880
+ }
2881
+ logger == null ? void 0 : logger.warn(`Failed to connect to database: ${error.message}`);
2882
+ }
2883
+ }
2884
+ async addKey(key) {
2885
+ await this.withTimeout(this.database.collection(this.path).doc(key.kid).set({
2886
+ kid: key.kid,
2887
+ key: JSON.stringify(key)
2888
+ }));
2889
+ }
2890
+ async listKeys() {
2891
+ const keys = await this.withTimeout(this.database.collection(this.path).get());
2892
+ return {
2893
+ items: keys.docs.map((key) => ({
2894
+ key: key.data(),
2895
+ createdAt: key.createTime.toDate()
2896
+ }))
2897
+ };
2898
+ }
2899
+ async removeKeys(kids) {
2900
+ for (const kid of kids) {
2901
+ await this.withTimeout(this.database.collection(this.path).doc(kid).delete());
2902
+ }
2903
+ }
2904
+ async withTimeout(operation) {
2905
+ const timer = new Promise((_, reject) => setTimeout(() => {
2906
+ reject(new Error(`Operation timed out after ${this.timeout}ms`));
2907
+ }, this.timeout));
2908
+ return Promise.race([operation, timer]);
2909
+ }
2910
+ async verify() {
2911
+ await this.withTimeout(this.database.collection(this.path).limit(1).get());
2912
+ }
2913
+ }
2914
+
2915
+ class KeyStores {
2916
+ static async fromConfig(config, options) {
2917
+ var _a;
2918
+ const { logger, database } = options != null ? options : {};
2919
+ const ks = config.getOptionalConfig("auth.keyStore");
2920
+ const provider = (_a = ks == null ? void 0 : ks.getOptionalString("provider")) != null ? _a : "database";
2921
+ logger == null ? void 0 : logger.info(`Configuring "${provider}" as KeyStore provider`);
2922
+ if (provider === "database") {
2923
+ if (!database) {
2924
+ throw new Error("This KeyStore provider requires a database");
2925
+ }
2926
+ return await DatabaseKeyStore.create({
2927
+ database: await database.getClient()
2928
+ });
2929
+ }
2930
+ if (provider === "memory") {
2931
+ return new MemoryKeyStore();
2932
+ }
2933
+ if (provider === "firestore") {
2934
+ const settings = ks == null ? void 0 : ks.getConfig(provider);
2935
+ const keyStore = await FirestoreKeyStore.create(lodash.pickBy({
2936
+ projectId: settings == null ? void 0 : settings.getOptionalString("projectId"),
2937
+ keyFilename: settings == null ? void 0 : settings.getOptionalString("keyFilename"),
2938
+ host: settings == null ? void 0 : settings.getOptionalString("host"),
2939
+ port: settings == null ? void 0 : settings.getOptionalNumber("port"),
2940
+ ssl: settings == null ? void 0 : settings.getOptionalBoolean("ssl"),
2941
+ path: settings == null ? void 0 : settings.getOptionalString("path"),
2942
+ timeout: settings == null ? void 0 : settings.getOptionalNumber("timeout")
2943
+ }, (value) => value !== void 0));
2944
+ await FirestoreKeyStore.verifyConnection(keyStore, logger);
2945
+ return keyStore;
2946
+ }
2947
+ throw new Error(`Unknown KeyStore provider: ${provider}`);
2948
+ }
2949
+ }
2950
+
3025
2951
  async function createRouter(options) {
3026
2952
  const {
3027
2953
  logger,
@@ -3137,7 +3063,6 @@ function createOriginFilter(config) {
3137
3063
  }
3138
3064
 
3139
3065
  exports.CatalogIdentityClient = CatalogIdentityClient;
3140
- exports.IdentityClient = IdentityClient;
3141
3066
  exports.OAuthAdapter = OAuthAdapter;
3142
3067
  exports.OAuthEnvironmentHandler = OAuthEnvironmentHandler;
3143
3068
  exports.bitbucketUserIdSignInResolver = bitbucketUserIdSignInResolver;