@backstage/plugin-auth-backend 0.9.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');
@@ -149,15 +150,14 @@ const verifyNonce = (req, providerId) => {
149
150
  throw new Error("Invalid nonce");
150
151
  }
151
152
  };
152
- const getCookieConfig = (authUrl, providerId) => {
153
- const { hostname: cookieDomain, pathname, protocol } = authUrl;
153
+ const defaultCookieConfigurer = ({
154
+ callbackUrl,
155
+ providerId
156
+ }) => {
157
+ const { hostname: domain, pathname, protocol } = new URL(callbackUrl);
154
158
  const secure = protocol === "https:";
155
- const cookiePath = pathname.endsWith(`${providerId}/handler/frame`) ? pathname.slice(0, -"/handler/frame".length) : `${pathname}/${providerId}`;
156
- return {
157
- cookieDomain,
158
- cookiePath,
159
- secure
160
- };
159
+ const path = pathname.endsWith(`${providerId}/handler/frame`) ? pathname.slice(0, -"/handler/frame".length) : `${pathname}/${providerId}`;
160
+ return { domain, path, secure };
161
161
  };
162
162
 
163
163
  class OAuthEnvironmentHandler {
@@ -281,58 +281,54 @@ class OAuthAdapter {
281
281
  this.setNonceCookie = (res, nonce) => {
282
282
  res.cookie(`${this.options.providerId}-nonce`, nonce, {
283
283
  maxAge: TEN_MINUTES_MS,
284
- secure: this.options.secure,
285
- sameSite: "lax",
286
- domain: this.options.cookieDomain,
287
- path: `${this.options.cookiePath}/handler`,
288
- httpOnly: true
284
+ ...this.baseCookieOptions,
285
+ path: `${this.options.cookiePath}/handler`
289
286
  });
290
287
  };
291
- this.setScopesCookie = (res, scope) => {
292
- res.cookie(`${this.options.providerId}-scope`, scope, {
293
- maxAge: TEN_MINUTES_MS,
294
- secure: this.options.secure,
295
- sameSite: "lax",
296
- domain: this.options.cookieDomain,
297
- path: `${this.options.cookiePath}/handler`,
298
- httpOnly: true
288
+ this.setGrantedScopeCookie = (res, scope) => {
289
+ res.cookie(`${this.options.providerId}-granted-scope`, scope, {
290
+ maxAge: THOUSAND_DAYS_MS,
291
+ ...this.baseCookieOptions
299
292
  });
300
293
  };
301
- this.getScopesFromCookie = (req, providerId) => {
302
- return req.cookies[`${providerId}-scope`];
294
+ this.getGrantedScopeFromCookie = (req) => {
295
+ return req.cookies[`${this.options.providerId}-granted-scope`];
303
296
  };
304
297
  this.setRefreshTokenCookie = (res, refreshToken) => {
305
298
  res.cookie(`${this.options.providerId}-refresh-token`, refreshToken, {
306
299
  maxAge: THOUSAND_DAYS_MS,
307
- secure: this.options.secure,
308
- sameSite: "lax",
309
- domain: this.options.cookieDomain,
310
- path: this.options.cookiePath,
311
- httpOnly: true
300
+ ...this.baseCookieOptions
312
301
  });
313
302
  };
314
303
  this.removeRefreshTokenCookie = (res) => {
315
304
  res.cookie(`${this.options.providerId}-refresh-token`, "", {
316
305
  maxAge: 0,
317
- secure: this.options.secure,
318
- sameSite: "lax",
319
- domain: this.options.cookieDomain,
320
- path: this.options.cookiePath,
321
- httpOnly: true
306
+ ...this.baseCookieOptions
322
307
  });
323
308
  };
309
+ this.baseCookieOptions = {
310
+ httpOnly: true,
311
+ sameSite: "lax",
312
+ secure: this.options.secure,
313
+ path: this.options.cookiePath,
314
+ domain: this.options.cookieDomain
315
+ };
324
316
  }
325
317
  static fromConfig(config, handlers, options) {
326
318
  var _a;
327
319
  const { origin: appOrigin } = new url.URL(config.appUrl);
328
- const authUrl = new url.URL((_a = options.callbackUrl) != null ? _a : config.baseUrl);
329
- const { cookieDomain, cookiePath, secure } = getCookieConfig(authUrl, options.providerId);
320
+ const cookieConfigurer = (_a = config.cookieConfigurer) != null ? _a : defaultCookieConfigurer;
321
+ const cookieConfig = cookieConfigurer({
322
+ providerId: options.providerId,
323
+ baseUrl: config.baseUrl,
324
+ callbackUrl: options.callbackUrl
325
+ });
330
326
  return new OAuthAdapter(handlers, {
331
327
  ...options,
332
328
  appOrigin,
333
- cookieDomain,
334
- cookiePath,
335
- secure,
329
+ cookieDomain: cookieConfig.domain,
330
+ cookiePath: cookieConfig.path,
331
+ secure: cookieConfig.secure,
336
332
  isOriginAllowed: config.isOriginAllowed
337
333
  });
338
334
  }
@@ -344,12 +340,12 @@ class OAuthAdapter {
344
340
  if (!env) {
345
341
  throw new errors.InputError("No env provided in request query parameters");
346
342
  }
347
- if (this.options.persistScopes) {
348
- this.setScopesCookie(res, scope);
349
- }
350
343
  const nonce = crypto__default["default"].randomBytes(16).toString("base64");
351
344
  this.setNonceCookie(res, nonce);
352
345
  const state = { nonce, env, origin };
346
+ if (this.options.persistScopes) {
347
+ state.scope = scope;
348
+ }
353
349
  const forwardReq = Object.assign(req, { scope, state });
354
350
  const { url, status } = await this.handlers.start(forwardReq);
355
351
  res.statusCode = status || 302;
@@ -374,9 +370,9 @@ class OAuthAdapter {
374
370
  }
375
371
  verifyNonce(req, this.options.providerId);
376
372
  const { response, refreshToken } = await this.handlers.handler(req);
377
- if (this.options.persistScopes) {
378
- const grantedScopes = this.getScopesFromCookie(req, this.options.providerId);
379
- response.providerInfo.scope = grantedScopes;
373
+ if (this.options.persistScopes && state.scope) {
374
+ this.setGrantedScopeCookie(res, state.scope);
375
+ response.providerInfo.scope = state.scope;
380
376
  }
381
377
  if (refreshToken && !this.options.disableRefresh) {
382
378
  this.setRefreshTokenCookie(res, refreshToken);
@@ -414,7 +410,10 @@ class OAuthAdapter {
414
410
  if (!refreshToken) {
415
411
  throw new errors.InputError("Missing session cookie");
416
412
  }
417
- const scope = (_b = (_a = req.query.scope) == null ? void 0 : _a.toString()) != null ? _b : "";
413
+ let scope = (_b = (_a = req.query.scope) == null ? void 0 : _a.toString()) != null ? _b : "";
414
+ if (this.options.persistScopes) {
415
+ scope = this.getGrantedScopeFromCookie(req);
416
+ }
418
417
  const forwardReq = Object.assign(req, { scope, refreshToken });
419
418
  const { response, refreshToken: newRefreshToken } = await this.handlers.refresh(forwardReq);
420
419
  const backstageIdentity = await this.populateIdentity(response.backstageIdentity);
@@ -718,7 +717,8 @@ const createAtlassianProvider = (options) => {
718
717
  const clientId = envConfig.getString("clientId");
719
718
  const clientSecret = envConfig.getString("clientSecret");
720
719
  const scopes = envConfig.getString("scopes");
721
- const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
720
+ const customCallbackUrl = envConfig.getOptionalString("callbackUrl");
721
+ const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`;
722
722
  const catalogIdentityClient = new CatalogIdentityClient({
723
723
  catalogApi,
724
724
  tokenManager
@@ -736,9 +736,9 @@ const createAtlassianProvider = (options) => {
736
736
  tokenIssuer
737
737
  });
738
738
  return OAuthAdapter.fromConfig(globalConfig, provider, {
739
- disableRefresh: true,
740
739
  providerId,
741
- tokenIssuer
740
+ tokenIssuer,
741
+ callbackUrl
742
742
  });
743
743
  });
744
744
  };
@@ -854,7 +854,8 @@ const createAuth0Provider = (options) => {
854
854
  const clientId = envConfig.getString("clientId");
855
855
  const clientSecret = envConfig.getString("clientSecret");
856
856
  const domain = envConfig.getString("domain");
857
- const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
857
+ const customCallbackUrl = envConfig.getOptionalString("callbackUrl");
858
+ const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`;
858
859
  const catalogIdentityClient = new CatalogIdentityClient({
859
860
  catalogApi,
860
861
  tokenManager
@@ -877,7 +878,8 @@ const createAuth0Provider = (options) => {
877
878
  return OAuthAdapter.fromConfig(globalConfig, provider, {
878
879
  disableRefresh: true,
879
880
  providerId,
880
- tokenIssuer
881
+ tokenIssuer,
882
+ callbackUrl
881
883
  });
882
884
  });
883
885
  };
@@ -1128,7 +1130,8 @@ const createBitbucketProvider = (options) => {
1128
1130
  var _a;
1129
1131
  const clientId = envConfig.getString("clientId");
1130
1132
  const clientSecret = envConfig.getString("clientSecret");
1131
- const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
1133
+ const customCallbackUrl = envConfig.getOptionalString("callbackUrl");
1134
+ const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`;
1132
1135
  const catalogIdentityClient = new CatalogIdentityClient({
1133
1136
  catalogApi,
1134
1137
  tokenManager
@@ -1149,11 +1152,14 @@ const createBitbucketProvider = (options) => {
1149
1152
  return OAuthAdapter.fromConfig(globalConfig, provider, {
1150
1153
  disableRefresh: false,
1151
1154
  providerId,
1152
- tokenIssuer
1155
+ tokenIssuer,
1156
+ callbackUrl
1153
1157
  });
1154
1158
  });
1155
1159
  };
1156
1160
 
1161
+ const ACCESS_TOKEN_PREFIX = "access-token.";
1162
+ const BACKSTAGE_SESSION_EXPIRATION = 3600;
1157
1163
  class GithubAuthProvider {
1158
1164
  constructor(options) {
1159
1165
  this.signInResolver = options.signInResolver;
@@ -1181,21 +1187,43 @@ class GithubAuthProvider {
1181
1187
  }
1182
1188
  async handler(req) {
1183
1189
  const { result, privateInfo } = await executeFrameHandlerStrategy(req, this._strategy);
1190
+ let refreshToken = privateInfo.refreshToken;
1191
+ if (!refreshToken && !result.params.expires_in) {
1192
+ refreshToken = ACCESS_TOKEN_PREFIX + result.accessToken;
1193
+ }
1184
1194
  return {
1185
1195
  response: await this.handleResult(result),
1186
- refreshToken: privateInfo.refreshToken
1196
+ refreshToken
1187
1197
  };
1188
1198
  }
1189
1199
  async refresh(req) {
1190
- const { accessToken, refreshToken, params } = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
1191
- const fullProfile = await executeFetchUserProfileStrategy(this._strategy, accessToken);
1200
+ const { scope, refreshToken } = req;
1201
+ if (refreshToken == null ? void 0 : refreshToken.startsWith(ACCESS_TOKEN_PREFIX)) {
1202
+ const accessToken = refreshToken.slice(ACCESS_TOKEN_PREFIX.length);
1203
+ const fullProfile = await executeFetchUserProfileStrategy(this._strategy, accessToken).catch((error) => {
1204
+ var _a;
1205
+ if (((_a = error.oauthError) == null ? void 0 : _a.statusCode) === 401) {
1206
+ throw new Error("Invalid access token");
1207
+ }
1208
+ throw error;
1209
+ });
1210
+ return {
1211
+ response: await this.handleResult({
1212
+ fullProfile,
1213
+ params: { scope },
1214
+ accessToken
1215
+ }),
1216
+ refreshToken
1217
+ };
1218
+ }
1219
+ const result = await executeRefreshTokenStrategy(this._strategy, refreshToken, scope);
1192
1220
  return {
1193
1221
  response: await this.handleResult({
1194
- fullProfile,
1195
- params,
1196
- accessToken
1222
+ fullProfile: await executeFetchUserProfileStrategy(this._strategy, result.accessToken),
1223
+ params: { ...result.params, scope },
1224
+ accessToken: result.accessToken
1197
1225
  }),
1198
- refreshToken
1226
+ refreshToken: result.refreshToken
1199
1227
  };
1200
1228
  }
1201
1229
  async handleResult(result) {
@@ -1206,21 +1234,28 @@ class GithubAuthProvider {
1206
1234
  };
1207
1235
  const { profile } = await this.authHandler(result, context);
1208
1236
  const expiresInStr = result.params.expires_in;
1209
- const response = {
1237
+ let expiresInSeconds = expiresInStr === void 0 ? void 0 : Number(expiresInStr);
1238
+ let backstageIdentity = void 0;
1239
+ if (this.signInResolver) {
1240
+ backstageIdentity = await this.signInResolver({
1241
+ result,
1242
+ profile
1243
+ }, context);
1244
+ if (expiresInSeconds) {
1245
+ expiresInSeconds = Math.min(expiresInSeconds, BACKSTAGE_SESSION_EXPIRATION);
1246
+ } else {
1247
+ expiresInSeconds = BACKSTAGE_SESSION_EXPIRATION;
1248
+ }
1249
+ }
1250
+ return {
1251
+ backstageIdentity,
1210
1252
  providerInfo: {
1211
1253
  accessToken: result.accessToken,
1212
1254
  scope: result.params.scope,
1213
- expiresInSeconds: expiresInStr === void 0 ? void 0 : Number(expiresInStr)
1255
+ expiresInSeconds
1214
1256
  },
1215
1257
  profile
1216
1258
  };
1217
- if (this.signInResolver) {
1218
- response.backstageIdentity = await this.signInResolver({
1219
- result,
1220
- profile
1221
- }, context);
1222
- }
1223
- return response;
1224
1259
  }
1225
1260
  }
1226
1261
  const githubDefaultSignInResolver = async (info, ctx) => {
@@ -1392,7 +1427,8 @@ const createGitlabProvider = (options) => {
1392
1427
  const clientSecret = envConfig.getString("clientSecret");
1393
1428
  const audience = envConfig.getOptionalString("audience");
1394
1429
  const baseUrl = audience || "https://gitlab.com";
1395
- const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
1430
+ const customCallbackUrl = envConfig.getOptionalString("callbackUrl");
1431
+ const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`;
1396
1432
  const catalogIdentityClient = new CatalogIdentityClient({
1397
1433
  catalogApi,
1398
1434
  tokenManager
@@ -1418,7 +1454,8 @@ const createGitlabProvider = (options) => {
1418
1454
  return OAuthAdapter.fromConfig(globalConfig, provider, {
1419
1455
  disableRefresh: false,
1420
1456
  providerId,
1421
- tokenIssuer
1457
+ tokenIssuer,
1458
+ callbackUrl
1422
1459
  });
1423
1460
  });
1424
1461
  };
@@ -1547,7 +1584,8 @@ const createGoogleProvider = (options) => {
1547
1584
  var _a, _b;
1548
1585
  const clientId = envConfig.getString("clientId");
1549
1586
  const clientSecret = envConfig.getString("clientSecret");
1550
- const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
1587
+ const customCallbackUrl = envConfig.getOptionalString("callbackUrl");
1588
+ const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`;
1551
1589
  const catalogIdentityClient = new CatalogIdentityClient({
1552
1590
  catalogApi,
1553
1591
  tokenManager
@@ -1574,7 +1612,8 @@ const createGoogleProvider = (options) => {
1574
1612
  return OAuthAdapter.fromConfig(globalConfig, provider, {
1575
1613
  disableRefresh: false,
1576
1614
  providerId,
1577
- tokenIssuer
1615
+ tokenIssuer,
1616
+ callbackUrl
1578
1617
  });
1579
1618
  });
1580
1619
  };
@@ -1706,7 +1745,8 @@ const createMicrosoftProvider = (options) => {
1706
1745
  const clientId = envConfig.getString("clientId");
1707
1746
  const clientSecret = envConfig.getString("clientSecret");
1708
1747
  const tenantId = envConfig.getString("tenantId");
1709
- const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
1748
+ const customCallbackUrl = envConfig.getOptionalString("callbackUrl");
1749
+ const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`;
1710
1750
  const authorizationUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`;
1711
1751
  const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
1712
1752
  const catalogIdentityClient = new CatalogIdentityClient({
@@ -1737,7 +1777,8 @@ const createMicrosoftProvider = (options) => {
1737
1777
  return OAuthAdapter.fromConfig(globalConfig, provider, {
1738
1778
  disableRefresh: false,
1739
1779
  providerId,
1740
- tokenIssuer
1780
+ tokenIssuer,
1781
+ callbackUrl
1741
1782
  });
1742
1783
  });
1743
1784
  };
@@ -1851,7 +1892,8 @@ const createOAuth2Provider = (options) => {
1851
1892
  var _a, _b, _c;
1852
1893
  const clientId = envConfig.getString("clientId");
1853
1894
  const clientSecret = envConfig.getString("clientSecret");
1854
- const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
1895
+ const customCallbackUrl = envConfig.getOptionalString("callbackUrl");
1896
+ const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`;
1855
1897
  const authorizationUrl = envConfig.getString("authorizationUrl");
1856
1898
  const tokenUrl = envConfig.getString("tokenUrl");
1857
1899
  const scope = envConfig.getOptionalString("scope");
@@ -1887,447 +1929,108 @@ const createOAuth2Provider = (options) => {
1887
1929
  return OAuthAdapter.fromConfig(globalConfig, provider, {
1888
1930
  disableRefresh,
1889
1931
  providerId,
1890
- tokenIssuer
1932
+ tokenIssuer,
1933
+ callbackUrl
1891
1934
  });
1892
1935
  });
1893
1936
  };
1894
1937
 
1895
- function createOidcRouter(options) {
1896
- const { baseUrl, tokenIssuer } = options;
1897
- const router = Router__default["default"]();
1898
- const config = {
1899
- issuer: baseUrl,
1900
- token_endpoint: `${baseUrl}/v1/token`,
1901
- userinfo_endpoint: `${baseUrl}/v1/userinfo`,
1902
- jwks_uri: `${baseUrl}/.well-known/jwks.json`,
1903
- response_types_supported: ["id_token"],
1904
- subject_types_supported: ["public"],
1905
- id_token_signing_alg_values_supported: ["RS256"],
1906
- scopes_supported: ["openid"],
1907
- token_endpoint_auth_methods_supported: [],
1908
- claims_supported: ["sub"],
1909
- grant_types_supported: []
1910
- };
1911
- router.get("/.well-known/openid-configuration", (_req, res) => {
1912
- res.json(config);
1913
- });
1914
- router.get("/.well-known/jwks.json", async (_req, res) => {
1915
- const { keys } = await tokenIssuer.listPublicKeys();
1916
- res.json({ keys });
1917
- });
1918
- router.get("/v1/token", (_req, res) => {
1919
- res.status(501).send("Not Implemented");
1920
- });
1921
- router.get("/v1/userinfo", (_req, res) => {
1922
- res.status(501).send("Not Implemented");
1923
- });
1924
- return router;
1925
- }
1926
-
1927
- const CLOCK_MARGIN_S = 10;
1928
- class IdentityClient {
1938
+ const OAUTH2_PROXY_JWT_HEADER = "X-OAUTH2-PROXY-ID-TOKEN";
1939
+ class Oauth2ProxyAuthProvider {
1929
1940
  constructor(options) {
1930
- this.discovery = options.discovery;
1931
- this.issuer = options.issuer;
1932
- this.keyStore = new jose.JWKS.KeyStore();
1933
- 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;
1934
1946
  }
1935
- async authenticate(token) {
1936
- var _a;
1937
- if (!token) {
1938
- throw new errors.AuthenticationError("No token specified");
1939
- }
1940
- const key = await this.getKey(token);
1941
- if (!key) {
1942
- throw new errors.AuthenticationError("No signing key matching token found");
1943
- }
1944
- const decoded = jose.JWT.IdToken.verify(token, key, {
1945
- algorithms: ["ES256"],
1946
- audience: "backstage",
1947
- issuer: this.issuer
1948
- });
1949
- if (!decoded.sub) {
1950
- throw new errors.AuthenticationError("No user sub found in token");
1951
- }
1952
- const user = {
1953
- id: decoded.sub,
1954
- token,
1955
- identity: {
1956
- type: "user",
1957
- userEntityRef: decoded.sub,
1958
- ownershipEntityRefs: (_a = decoded.ent) != null ? _a : []
1959
- }
1960
- };
1961
- return user;
1947
+ frameHandler() {
1948
+ return Promise.resolve(void 0);
1962
1949
  }
1963
- static getBearerToken(authorizationHeader) {
1964
- if (typeof authorizationHeader !== "string") {
1965
- 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();
1966
1959
  }
1967
- const matches = authorizationHeader.match(/Bearer\s+(\S+)/i);
1968
- return matches == null ? void 0 : matches[1];
1969
1960
  }
1970
- async getKey(rawJwtToken) {
1971
- const { header, payload } = jose.JWT.decode(rawJwtToken, {
1972
- complete: true
1973
- });
1974
- const keyStoreHasKey = !!this.keyStore.get({ kid: header.kid });
1975
- const issuedAfterLastRefresh = (payload == null ? void 0 : payload.iat) && payload.iat > this.keyStoreUpdated - CLOCK_MARGIN_S;
1976
- if (!keyStoreHasKey && issuedAfterLastRefresh) {
1977
- await this.refreshKeyStore();
1978
- }
1979
- return this.keyStore.get({ kid: header.kid });
1961
+ start() {
1962
+ return Promise.resolve(void 0);
1980
1963
  }
1981
- async listPublicKeys() {
1982
- const url = `${await this.discovery.getBaseUrl("auth")}/.well-known/jwks.json`;
1983
- const response = await fetch__default["default"](url);
1984
- if (!response.ok) {
1985
- const payload = await response.text();
1986
- const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
1987
- throw new Error(message);
1988
- }
1989
- const publicKeys = await response.json();
1990
- 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
+ };
1991
1982
  }
1992
- async refreshKeyStore() {
1993
- const now = Date.now() / 1e3;
1994
- const publicKeys = await this.listPublicKeys();
1995
- this.keyStore = jose.JWKS.asKeyStore({
1996
- keys: publicKeys.keys.map((key) => key)
1997
- });
1998
- 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
+ };
1999
1994
  }
2000
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
+ };
2001
2011
 
2002
- const MS_IN_S = 1e3;
2003
- class TokenFactory {
2012
+ class OidcAuthProvider {
2004
2013
  constructor(options) {
2005
- 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;
2006
2021
  this.logger = options.logger;
2007
- this.keyStore = options.keyStore;
2008
- this.keyDurationSeconds = options.keyDurationSeconds;
2009
- }
2010
- async issueToken(params) {
2011
- const key = await this.getKey();
2012
- const iss = this.issuer;
2013
- const sub = params.claims.sub;
2014
- const ent = params.claims.ent;
2015
- const aud = "backstage";
2016
- const iat = Math.floor(Date.now() / MS_IN_S);
2017
- const exp = iat + this.keyDurationSeconds;
2018
- this.logger.info(`Issuing token for ${sub}, with entities ${ent != null ? ent : []}`);
2019
- return jose.JWS.sign({ iss, sub, aud, iat, exp, ent }, key, {
2020
- alg: key.alg,
2021
- kid: key.kid
2022
- });
2023
2022
  }
2024
- async listPublicKeys() {
2025
- const { items: keys } = await this.keyStore.listKeys();
2026
- const validKeys = [];
2027
- const expiredKeys = [];
2028
- for (const key of keys) {
2029
- const expireAt = luxon.DateTime.fromJSDate(key.createdAt).plus({
2030
- seconds: 3 * this.keyDurationSeconds
2031
- });
2032
- if (expireAt < luxon.DateTime.local()) {
2033
- expiredKeys.push(key);
2034
- } else {
2035
- validKeys.push(key);
2036
- }
2037
- }
2038
- if (expiredKeys.length > 0) {
2039
- const kids = expiredKeys.map(({ key }) => key.kid);
2040
- this.logger.info(`Removing expired signing keys, '${kids.join("', '")}'`);
2041
- this.keyStore.removeKeys(kids).catch((error) => {
2042
- this.logger.error(`Failed to remove expired keys, ${error}`);
2043
- });
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;
2044
2032
  }
2045
- return { keys: validKeys.map(({ key }) => key) };
2046
- }
2047
- async getKey() {
2048
- if (this.privateKeyPromise) {
2049
- if (this.keyExpiry && luxon.DateTime.fromJSDate(this.keyExpiry) > luxon.DateTime.local()) {
2050
- return this.privateKeyPromise;
2051
- }
2052
- this.logger.info(`Signing key has expired, generating new key`);
2053
- delete this.privateKeyPromise;
2054
- }
2055
- this.keyExpiry = luxon.DateTime.utc().plus({
2056
- seconds: this.keyDurationSeconds
2057
- }).toJSDate();
2058
- const promise = (async () => {
2059
- const key = await jose.JWK.generate("EC", "P-256", {
2060
- use: "sig",
2061
- kid: uuid.v4(),
2062
- alg: "ES256"
2063
- });
2064
- this.logger.info(`Created new signing key ${key.kid}`);
2065
- await this.keyStore.addKey(key.toJWK(false));
2066
- return key;
2067
- })();
2068
- this.privateKeyPromise = promise;
2069
- try {
2070
- await promise;
2071
- } catch (error) {
2072
- this.logger.error(`Failed to generate new signing key, ${error}`);
2073
- delete this.keyExpiry;
2074
- delete this.privateKeyPromise;
2075
- }
2076
- return promise;
2077
- }
2078
- }
2079
-
2080
- const migrationsDir = backendCommon.resolvePackagePath("@backstage/plugin-auth-backend", "migrations");
2081
- const TABLE = "signing_keys";
2082
- const parseDate = (date) => {
2083
- const parsedDate = typeof date === "string" ? luxon.DateTime.fromSQL(date, { zone: "UTC" }) : luxon.DateTime.fromJSDate(date);
2084
- if (!parsedDate.isValid) {
2085
- throw new Error(`Failed to parse date, reason: ${parsedDate.invalidReason}, explanation: ${parsedDate.invalidExplanation}`);
2086
- }
2087
- return parsedDate.toJSDate();
2088
- };
2089
- class DatabaseKeyStore {
2090
- static async create(options) {
2091
- const { database } = options;
2092
- await database.migrate.latest({
2093
- directory: migrationsDir
2094
- });
2095
- return new DatabaseKeyStore(options);
2096
- }
2097
- constructor(options) {
2098
- this.database = options.database;
2099
- }
2100
- async addKey(key) {
2101
- await this.database(TABLE).insert({
2102
- kid: key.kid,
2103
- key: JSON.stringify(key)
2104
- });
2105
- }
2106
- async listKeys() {
2107
- const rows = await this.database(TABLE).select();
2108
- return {
2109
- items: rows.map((row) => ({
2110
- key: JSON.parse(row.key),
2111
- createdAt: parseDate(row.created_at)
2112
- }))
2113
- };
2114
- }
2115
- async removeKeys(kids) {
2116
- await this.database(TABLE).delete().whereIn("kid", kids);
2117
- }
2118
- }
2119
-
2120
- class MemoryKeyStore {
2121
- constructor() {
2122
- this.keys = /* @__PURE__ */ new Map();
2123
- }
2124
- async addKey(key) {
2125
- this.keys.set(key.kid, {
2126
- createdAt: luxon.DateTime.utc().toJSDate(),
2127
- key: JSON.stringify(key)
2128
- });
2129
- }
2130
- async removeKeys(kids) {
2131
- for (const kid of kids) {
2132
- this.keys.delete(kid);
2133
- }
2134
- }
2135
- async listKeys() {
2136
- return {
2137
- items: Array.from(this.keys).map(([, { createdAt, key: keyStr }]) => ({
2138
- createdAt,
2139
- key: JSON.parse(keyStr)
2140
- }))
2141
- };
2142
- }
2143
- }
2144
-
2145
- const DEFAULT_TIMEOUT_MS = 1e4;
2146
- const DEFAULT_DOCUMENT_PATH = "sessions";
2147
- class FirestoreKeyStore {
2148
- constructor(database, path, timeout) {
2149
- this.database = database;
2150
- this.path = path;
2151
- this.timeout = timeout;
2152
- }
2153
- static async create(settings) {
2154
- const { path, timeout, ...firestoreSettings } = settings != null ? settings : {};
2155
- const database = new firestore.Firestore(firestoreSettings);
2156
- return new FirestoreKeyStore(database, path != null ? path : DEFAULT_DOCUMENT_PATH, timeout != null ? timeout : DEFAULT_TIMEOUT_MS);
2157
- }
2158
- static async verifyConnection(keyStore, logger) {
2159
- try {
2160
- await keyStore.verify();
2161
- } catch (error) {
2162
- if (process.env.NODE_ENV !== "development") {
2163
- throw new Error(`Failed to connect to database: ${error.message}`);
2164
- }
2165
- logger == null ? void 0 : logger.warn(`Failed to connect to database: ${error.message}`);
2166
- }
2167
- }
2168
- async addKey(key) {
2169
- await this.withTimeout(this.database.collection(this.path).doc(key.kid).set({
2170
- kid: key.kid,
2171
- key: JSON.stringify(key)
2172
- }));
2173
- }
2174
- async listKeys() {
2175
- const keys = await this.withTimeout(this.database.collection(this.path).get());
2176
- return {
2177
- items: keys.docs.map((key) => ({
2178
- key: key.data(),
2179
- createdAt: key.createTime.toDate()
2180
- }))
2181
- };
2182
- }
2183
- async removeKeys(kids) {
2184
- for (const kid of kids) {
2185
- await this.withTimeout(this.database.collection(this.path).doc(kid).delete());
2186
- }
2187
- }
2188
- async withTimeout(operation) {
2189
- const timer = new Promise((_, reject) => setTimeout(() => {
2190
- reject(new Error(`Operation timed out after ${this.timeout}ms`));
2191
- }, this.timeout));
2192
- return Promise.race([operation, timer]);
2193
- }
2194
- async verify() {
2195
- await this.withTimeout(this.database.collection(this.path).limit(1).get());
2196
- }
2197
- }
2198
-
2199
- class KeyStores {
2200
- static async fromConfig(config, options) {
2201
- var _a;
2202
- const { logger, database } = options != null ? options : {};
2203
- const ks = config.getOptionalConfig("auth.keyStore");
2204
- const provider = (_a = ks == null ? void 0 : ks.getOptionalString("provider")) != null ? _a : "database";
2205
- logger == null ? void 0 : logger.info(`Configuring "${provider}" as KeyStore provider`);
2206
- if (provider === "database") {
2207
- if (!database) {
2208
- throw new Error("This KeyStore provider requires a database");
2209
- }
2210
- return await DatabaseKeyStore.create({
2211
- database: await database.getClient()
2212
- });
2213
- }
2214
- if (provider === "memory") {
2215
- return new MemoryKeyStore();
2216
- }
2217
- if (provider === "firestore") {
2218
- const settings = ks == null ? void 0 : ks.getConfig(provider);
2219
- const keyStore = await FirestoreKeyStore.create(lodash.pickBy({
2220
- projectId: settings == null ? void 0 : settings.getOptionalString("projectId"),
2221
- keyFilename: settings == null ? void 0 : settings.getOptionalString("keyFilename"),
2222
- host: settings == null ? void 0 : settings.getOptionalString("host"),
2223
- port: settings == null ? void 0 : settings.getOptionalNumber("port"),
2224
- ssl: settings == null ? void 0 : settings.getOptionalBoolean("ssl"),
2225
- path: settings == null ? void 0 : settings.getOptionalString("path"),
2226
- timeout: settings == null ? void 0 : settings.getOptionalNumber("timeout")
2227
- }, (value) => value !== void 0));
2228
- await FirestoreKeyStore.verifyConnection(keyStore, logger);
2229
- return keyStore;
2230
- }
2231
- throw new Error(`Unknown KeyStore provider: ${provider}`);
2232
- }
2233
- }
2234
-
2235
- const OAUTH2_PROXY_JWT_HEADER = "X-OAUTH2-PROXY-ID-TOKEN";
2236
- class Oauth2ProxyAuthProvider {
2237
- constructor(options) {
2238
- this.catalogIdentityClient = options.catalogIdentityClient;
2239
- this.logger = options.logger;
2240
- this.tokenIssuer = options.tokenIssuer;
2241
- this.signInResolver = options.signInResolver;
2242
- this.authHandler = options.authHandler;
2243
- }
2244
- frameHandler() {
2245
- return Promise.resolve(void 0);
2246
- }
2247
- async refresh(req, res) {
2248
- try {
2249
- const result = this.getResult(req);
2250
- const response = await this.handleResult(result);
2251
- res.json(response);
2252
- } catch (e) {
2253
- this.logger.error(`Exception occurred during ${OAUTH2_PROXY_JWT_HEADER} refresh`, e);
2254
- res.status(401);
2255
- res.end();
2256
- }
2257
- }
2258
- start() {
2259
- return Promise.resolve(void 0);
2260
- }
2261
- async handleResult(result) {
2262
- const ctx = {
2263
- logger: this.logger,
2264
- tokenIssuer: this.tokenIssuer,
2265
- catalogIdentityClient: this.catalogIdentityClient
2266
- };
2267
- const { profile } = await this.authHandler(result, ctx);
2268
- const backstageSignInResult = await this.signInResolver({
2269
- result,
2270
- profile
2271
- }, ctx);
2272
- return {
2273
- providerInfo: {
2274
- accessToken: result.accessToken
2275
- },
2276
- backstageIdentity: prepareBackstageIdentityResponse(backstageSignInResult),
2277
- profile
2278
- };
2279
- }
2280
- getResult(req) {
2281
- const authHeader = req.header(OAUTH2_PROXY_JWT_HEADER);
2282
- const jwt = IdentityClient.getBearerToken(authHeader);
2283
- if (!jwt) {
2284
- throw new errors.AuthenticationError(`Missing or in incorrect format - Oauth2Proxy OIDC header: ${OAUTH2_PROXY_JWT_HEADER}`);
2285
- }
2286
- const decodedJWT = jose.JWT.decode(jwt);
2287
- return {
2288
- fullProfile: decodedJWT,
2289
- accessToken: jwt
2290
- };
2291
- }
2292
- }
2293
- const createOauth2ProxyProvider = (options) => ({ catalogApi, logger, tokenIssuer, tokenManager }) => {
2294
- const signInResolver = options.signIn.resolver;
2295
- const authHandler = options.authHandler;
2296
- const catalogIdentityClient = new CatalogIdentityClient({
2297
- catalogApi,
2298
- tokenManager
2299
- });
2300
- return new Oauth2ProxyAuthProvider({
2301
- logger,
2302
- signInResolver,
2303
- authHandler,
2304
- tokenIssuer,
2305
- catalogIdentityClient
2306
- });
2307
- };
2308
-
2309
- class OidcAuthProvider {
2310
- constructor(options) {
2311
- this.implementation = this.setupStrategy(options);
2312
- this.scope = options.scope;
2313
- this.prompt = options.prompt;
2314
- this.signInResolver = options.signInResolver;
2315
- this.authHandler = options.authHandler;
2316
- this.tokenIssuer = options.tokenIssuer;
2317
- this.catalogIdentityClient = options.catalogIdentityClient;
2318
- this.logger = options.logger;
2319
- }
2320
- async start(req) {
2321
- const { strategy } = await this.implementation;
2322
- const options = {
2323
- scope: req.scope || this.scope || "openid profile email",
2324
- state: encodeState(req.state)
2325
- };
2326
- const prompt = this.prompt || "none";
2327
- if (prompt !== "auto") {
2328
- options.prompt = prompt;
2329
- }
2330
- return await executeRedirectStrategy(req, strategy, options);
2033
+ return await executeRedirectStrategy(req, strategy, options);
2331
2034
  }
2332
2035
  async handler(req) {
2333
2036
  const { strategy } = await this.implementation;
@@ -2426,7 +2129,8 @@ const createOidcProvider = (options) => {
2426
2129
  var _a, _b;
2427
2130
  const clientId = envConfig.getString("clientId");
2428
2131
  const clientSecret = envConfig.getString("clientSecret");
2429
- const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
2132
+ const customCallbackUrl = envConfig.getOptionalString("callbackUrl");
2133
+ const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`;
2430
2134
  const metadataUrl = envConfig.getString("metadataUrl");
2431
2135
  const tokenSignedResponseAlg = envConfig.getOptionalString("tokenSignedResponseAlg");
2432
2136
  const scope = envConfig.getOptionalString("scope");
@@ -2465,7 +2169,8 @@ const createOidcProvider = (options) => {
2465
2169
  return OAuthAdapter.fromConfig(globalConfig, provider, {
2466
2170
  disableRefresh: false,
2467
2171
  providerId,
2468
- tokenIssuer
2172
+ tokenIssuer,
2173
+ callbackUrl
2469
2174
  });
2470
2175
  });
2471
2176
  };
@@ -2595,7 +2300,8 @@ const createOktaProvider = (_options) => {
2595
2300
  const clientId = envConfig.getString("clientId");
2596
2301
  const clientSecret = envConfig.getString("clientSecret");
2597
2302
  const audience = envConfig.getString("audience");
2598
- const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
2303
+ const customCallbackUrl = envConfig.getOptionalString("callbackUrl");
2304
+ const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`;
2599
2305
  if (!audience.startsWith("https://")) {
2600
2306
  throw new Error("URL for 'audience' must start with 'https://'.");
2601
2307
  }
@@ -2626,7 +2332,8 @@ const createOktaProvider = (_options) => {
2626
2332
  return OAuthAdapter.fromConfig(globalConfig, provider, {
2627
2333
  disableRefresh: false,
2628
2334
  providerId,
2629
- tokenIssuer
2335
+ tokenIssuer,
2336
+ callbackUrl
2630
2337
  });
2631
2338
  });
2632
2339
  };
@@ -2690,290 +2397,557 @@ class OneLoginProvider {
2690
2397
  };
2691
2398
  const { profile } = await this.authHandler(result, context);
2692
2399
  const response = {
2693
- providerInfo: {
2694
- idToken: result.params.id_token,
2695
- accessToken: result.accessToken,
2696
- scope: result.params.scope,
2697
- expiresInSeconds: result.params.expires_in
2698
- },
2699
- profile
2400
+ providerInfo: {
2401
+ idToken: result.params.id_token,
2402
+ accessToken: result.accessToken,
2403
+ scope: result.params.scope,
2404
+ expiresInSeconds: result.params.expires_in
2405
+ },
2406
+ profile
2407
+ };
2408
+ if (this.signInResolver) {
2409
+ response.backstageIdentity = await this.signInResolver({
2410
+ result,
2411
+ profile
2412
+ }, context);
2413
+ }
2414
+ return response;
2415
+ }
2416
+ }
2417
+ const defaultSignInResolver = async (info) => {
2418
+ const { profile } = info;
2419
+ if (!profile.email) {
2420
+ throw new Error("OIDC profile contained no email");
2421
+ }
2422
+ const id = profile.email.split("@")[0];
2423
+ return { id, token: "" };
2424
+ };
2425
+ const createOneLoginProvider = (options) => {
2426
+ return ({
2427
+ providerId,
2428
+ globalConfig,
2429
+ config,
2430
+ tokenIssuer,
2431
+ tokenManager,
2432
+ catalogApi,
2433
+ logger
2434
+ }) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
2435
+ var _a, _b;
2436
+ const clientId = envConfig.getString("clientId");
2437
+ const clientSecret = envConfig.getString("clientSecret");
2438
+ const issuer = envConfig.getString("issuer");
2439
+ const customCallbackUrl = envConfig.getOptionalString("callbackUrl");
2440
+ const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`;
2441
+ const catalogIdentityClient = new CatalogIdentityClient({
2442
+ catalogApi,
2443
+ tokenManager
2444
+ });
2445
+ const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ fullProfile, params }) => ({
2446
+ profile: makeProfileInfo(fullProfile, params.id_token)
2447
+ });
2448
+ const signInResolver = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : defaultSignInResolver;
2449
+ const provider = new OneLoginProvider({
2450
+ clientId,
2451
+ clientSecret,
2452
+ callbackUrl,
2453
+ issuer,
2454
+ authHandler,
2455
+ signInResolver,
2456
+ tokenIssuer,
2457
+ catalogIdentityClient,
2458
+ logger
2459
+ });
2460
+ return OAuthAdapter.fromConfig(globalConfig, provider, {
2461
+ disableRefresh: false,
2462
+ providerId,
2463
+ tokenIssuer,
2464
+ callbackUrl
2465
+ });
2466
+ });
2467
+ };
2468
+
2469
+ class SamlAuthProvider {
2470
+ constructor(options) {
2471
+ this.appUrl = options.appUrl;
2472
+ this.signInResolver = options.signInResolver;
2473
+ this.authHandler = options.authHandler;
2474
+ this.tokenIssuer = options.tokenIssuer;
2475
+ this.catalogIdentityClient = options.catalogIdentityClient;
2476
+ this.logger = options.logger;
2477
+ this.strategy = new passportSaml.Strategy({ ...options }, (fullProfile, done) => {
2478
+ done(void 0, { fullProfile });
2479
+ });
2480
+ }
2481
+ async start(req, res) {
2482
+ const { url } = await executeRedirectStrategy(req, this.strategy, {});
2483
+ res.redirect(url);
2484
+ }
2485
+ async frameHandler(req, res) {
2486
+ try {
2487
+ const context = {
2488
+ logger: this.logger,
2489
+ catalogIdentityClient: this.catalogIdentityClient,
2490
+ tokenIssuer: this.tokenIssuer
2491
+ };
2492
+ const { result } = await executeFrameHandlerStrategy(req, this.strategy);
2493
+ const { profile } = await this.authHandler(result, context);
2494
+ const response = {
2495
+ profile,
2496
+ providerInfo: {}
2497
+ };
2498
+ if (this.signInResolver) {
2499
+ const signInResponse = await this.signInResolver({
2500
+ result,
2501
+ profile
2502
+ }, context);
2503
+ response.backstageIdentity = prepareBackstageIdentityResponse(signInResponse);
2504
+ }
2505
+ return postMessageResponse(res, this.appUrl, {
2506
+ type: "authorization_response",
2507
+ response
2508
+ });
2509
+ } catch (error) {
2510
+ const { name, message } = errors.isError(error) ? error : new Error("Encountered invalid error");
2511
+ return postMessageResponse(res, this.appUrl, {
2512
+ type: "authorization_response",
2513
+ error: { name, message }
2514
+ });
2515
+ }
2516
+ }
2517
+ async logout(_req, res) {
2518
+ res.end();
2519
+ }
2520
+ }
2521
+ const samlDefaultSignInResolver = async (info, ctx) => {
2522
+ const id = info.result.fullProfile.nameID;
2523
+ const token = await ctx.tokenIssuer.issueToken({
2524
+ claims: { sub: id }
2525
+ });
2526
+ return { id, token };
2527
+ };
2528
+ const createSamlProvider = (options) => {
2529
+ return ({
2530
+ providerId,
2531
+ globalConfig,
2532
+ config,
2533
+ tokenIssuer,
2534
+ tokenManager,
2535
+ catalogApi,
2536
+ logger
2537
+ }) => {
2538
+ var _a, _b;
2539
+ const catalogIdentityClient = new CatalogIdentityClient({
2540
+ catalogApi,
2541
+ tokenManager
2542
+ });
2543
+ const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ fullProfile }) => ({
2544
+ profile: {
2545
+ email: fullProfile.email,
2546
+ displayName: fullProfile.displayName
2547
+ }
2548
+ });
2549
+ const signInResolverFn = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : samlDefaultSignInResolver;
2550
+ const signInResolver = (info) => signInResolverFn(info, {
2551
+ catalogIdentityClient,
2552
+ tokenIssuer,
2553
+ logger
2554
+ });
2555
+ return new SamlAuthProvider({
2556
+ callbackUrl: `${globalConfig.baseUrl}/${providerId}/handler/frame`,
2557
+ entryPoint: config.getString("entryPoint"),
2558
+ logoutUrl: config.getOptionalString("logoutUrl"),
2559
+ audience: config.getOptionalString("audience"),
2560
+ issuer: config.getString("issuer"),
2561
+ cert: config.getString("cert"),
2562
+ privateKey: config.getOptionalString("privateKey"),
2563
+ authnContext: config.getOptionalStringArray("authnContext"),
2564
+ identifierFormat: config.getOptionalString("identifierFormat"),
2565
+ decryptionPvk: config.getOptionalString("decryptionPvk"),
2566
+ signatureAlgorithm: config.getOptionalString("signatureAlgorithm"),
2567
+ digestAlgorithm: config.getOptionalString("digestAlgorithm"),
2568
+ acceptedClockSkewMs: config.getOptionalNumber("acceptedClockSkewMs"),
2569
+ tokenIssuer,
2570
+ appUrl: globalConfig.appUrl,
2571
+ authHandler,
2572
+ signInResolver,
2573
+ logger,
2574
+ catalogIdentityClient
2575
+ });
2576
+ };
2577
+ };
2578
+
2579
+ const IAP_JWT_HEADER = "x-goog-iap-jwt-assertion";
2580
+
2581
+ function createTokenValidator(audience, mockClient) {
2582
+ const client = mockClient != null ? mockClient : new googleAuthLibrary.OAuth2Client();
2583
+ return async function tokenValidator(token) {
2584
+ const response = await client.getIapPublicKeys();
2585
+ const ticket = await client.verifySignedJwtWithCertsAsync(token, response.pubkeys, audience, ["https://cloud.google.com/iap"]);
2586
+ const payload = ticket.getPayload();
2587
+ if (!payload) {
2588
+ throw new TypeError("Token had no payload");
2589
+ }
2590
+ return payload;
2591
+ };
2592
+ }
2593
+ async function parseRequestToken(jwtToken, tokenValidator) {
2594
+ if (typeof jwtToken !== "string" || !jwtToken) {
2595
+ throw new errors.AuthenticationError(`Missing Google IAP header: ${IAP_JWT_HEADER}`);
2596
+ }
2597
+ let payload;
2598
+ try {
2599
+ payload = await tokenValidator(jwtToken);
2600
+ } catch (e) {
2601
+ throw new errors.AuthenticationError(`Google IAP token verification failed, ${e}`);
2602
+ }
2603
+ if (!payload.sub || !payload.email) {
2604
+ throw new errors.AuthenticationError("Google IAP token payload is missing sub and/or email claim");
2605
+ }
2606
+ return {
2607
+ iapToken: {
2608
+ ...payload,
2609
+ sub: payload.sub,
2610
+ email: payload.email
2611
+ }
2612
+ };
2613
+ }
2614
+ const defaultAuthHandler = async ({
2615
+ iapToken
2616
+ }) => ({ profile: { email: iapToken.email } });
2617
+
2618
+ class GcpIapProvider {
2619
+ constructor(options) {
2620
+ this.authHandler = options.authHandler;
2621
+ this.signInResolver = options.signInResolver;
2622
+ this.tokenValidator = options.tokenValidator;
2623
+ this.tokenIssuer = options.tokenIssuer;
2624
+ this.catalogIdentityClient = options.catalogIdentityClient;
2625
+ this.logger = options.logger;
2626
+ }
2627
+ async start() {
2628
+ }
2629
+ async frameHandler() {
2630
+ }
2631
+ async refresh(req, res) {
2632
+ const result = await parseRequestToken(req.header(IAP_JWT_HEADER), this.tokenValidator);
2633
+ const context = {
2634
+ logger: this.logger,
2635
+ catalogIdentityClient: this.catalogIdentityClient,
2636
+ tokenIssuer: this.tokenIssuer
2637
+ };
2638
+ const { profile } = await this.authHandler(result, context);
2639
+ const backstageIdentity = await this.signInResolver({ profile, result }, context);
2640
+ const response = {
2641
+ providerInfo: { iapToken: result.iapToken },
2642
+ profile,
2643
+ backstageIdentity: prepareBackstageIdentityResponse(backstageIdentity)
2700
2644
  };
2701
- if (this.signInResolver) {
2702
- response.backstageIdentity = await this.signInResolver({
2703
- result,
2704
- profile
2705
- }, context);
2706
- }
2707
- return response;
2645
+ res.json(response);
2708
2646
  }
2709
2647
  }
2710
- const defaultSignInResolver = async (info) => {
2711
- const { profile } = info;
2712
- if (!profile.email) {
2713
- throw new Error("OIDC profile contained no email");
2714
- }
2715
- const id = profile.email.split("@")[0];
2716
- return { id, token: "" };
2717
- };
2718
- const createOneLoginProvider = (options) => {
2719
- return ({
2720
- providerId,
2721
- globalConfig,
2722
- config,
2723
- tokenIssuer,
2724
- tokenManager,
2725
- catalogApi,
2726
- logger
2727
- }) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
2728
- var _a, _b;
2729
- const clientId = envConfig.getString("clientId");
2730
- const clientSecret = envConfig.getString("clientSecret");
2731
- const issuer = envConfig.getString("issuer");
2732
- const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
2648
+ function createGcpIapProvider(options) {
2649
+ return ({ config, tokenIssuer, catalogApi, logger, tokenManager }) => {
2650
+ var _a;
2651
+ const audience = config.getString("audience");
2652
+ const authHandler = (_a = options.authHandler) != null ? _a : defaultAuthHandler;
2653
+ const signInResolver = options.signIn.resolver;
2654
+ const tokenValidator = createTokenValidator(audience);
2733
2655
  const catalogIdentityClient = new CatalogIdentityClient({
2734
2656
  catalogApi,
2735
2657
  tokenManager
2736
2658
  });
2737
- const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ fullProfile, params }) => ({
2738
- profile: makeProfileInfo(fullProfile, params.id_token)
2739
- });
2740
- const signInResolver = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : defaultSignInResolver;
2741
- const provider = new OneLoginProvider({
2742
- clientId,
2743
- clientSecret,
2744
- callbackUrl,
2745
- issuer,
2659
+ return new GcpIapProvider({
2746
2660
  authHandler,
2747
2661
  signInResolver,
2662
+ tokenValidator,
2748
2663
  tokenIssuer,
2749
2664
  catalogIdentityClient,
2750
2665
  logger
2751
2666
  });
2752
- return OAuthAdapter.fromConfig(globalConfig, provider, {
2753
- disableRefresh: false,
2754
- providerId,
2755
- tokenIssuer
2756
- });
2757
- });
2667
+ };
2668
+ }
2669
+
2670
+ const factories = {
2671
+ google: createGoogleProvider(),
2672
+ github: createGithubProvider(),
2673
+ gitlab: createGitlabProvider(),
2674
+ saml: createSamlProvider(),
2675
+ okta: createOktaProvider(),
2676
+ auth0: createAuth0Provider(),
2677
+ microsoft: createMicrosoftProvider(),
2678
+ oauth2: createOAuth2Provider(),
2679
+ oidc: createOidcProvider(),
2680
+ onelogin: createOneLoginProvider(),
2681
+ awsalb: createAwsAlbProvider(),
2682
+ bitbucket: createBitbucketProvider(),
2683
+ atlassian: createAtlassianProvider()
2758
2684
  };
2759
2685
 
2760
- class SamlAuthProvider {
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 {
2761
2720
  constructor(options) {
2762
- this.appUrl = options.appUrl;
2763
- this.signInResolver = options.signInResolver;
2764
- this.authHandler = options.authHandler;
2765
- this.tokenIssuer = options.tokenIssuer;
2766
- this.catalogIdentityClient = options.catalogIdentityClient;
2721
+ this.issuer = options.issuer;
2767
2722
  this.logger = options.logger;
2768
- this.strategy = new passportSaml.Strategy({ ...options }, (fullProfile, done) => {
2769
- done(void 0, { fullProfile });
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
2770
2738
  });
2771
2739
  }
2772
- async start(req, res) {
2773
- const { url } = await executeRedirectStrategy(req, this.strategy, {});
2774
- res.redirect(url);
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) };
2775
2762
  }
2776
- async frameHandler(req, res) {
2777
- try {
2778
- const context = {
2779
- logger: this.logger,
2780
- catalogIdentityClient: this.catalogIdentityClient,
2781
- tokenIssuer: this.tokenIssuer
2782
- };
2783
- const { result } = await executeFrameHandlerStrategy(req, this.strategy);
2784
- const { profile } = await this.authHandler(result, context);
2785
- const response = {
2786
- profile,
2787
- providerInfo: {}
2788
- };
2789
- if (this.signInResolver) {
2790
- const signInResponse = await this.signInResolver({
2791
- result,
2792
- profile
2793
- }, context);
2794
- response.backstageIdentity = prepareBackstageIdentityResponse(signInResponse);
2763
+ async getKey() {
2764
+ if (this.privateKeyPromise) {
2765
+ if (this.keyExpiry && luxon.DateTime.fromJSDate(this.keyExpiry) > luxon.DateTime.local()) {
2766
+ return this.privateKeyPromise;
2795
2767
  }
2796
- return postMessageResponse(res, this.appUrl, {
2797
- type: "authorization_response",
2798
- response
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"
2799
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;
2800
2787
  } catch (error) {
2801
- const { name, message } = errors.isError(error) ? error : new Error("Encountered invalid error");
2802
- return postMessageResponse(res, this.appUrl, {
2803
- type: "authorization_response",
2804
- error: { name, message }
2805
- });
2788
+ this.logger.error(`Failed to generate new signing key, ${error}`);
2789
+ delete this.keyExpiry;
2790
+ delete this.privateKeyPromise;
2806
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
+ };
2807
2830
  }
2808
- async logout(_req, res) {
2809
- res.end();
2831
+ async removeKeys(kids) {
2832
+ await this.database(TABLE).delete().whereIn("kid", kids);
2810
2833
  }
2811
2834
  }
2812
- const samlDefaultSignInResolver = async (info, ctx) => {
2813
- const id = info.result.fullProfile.nameID;
2814
- const token = await ctx.tokenIssuer.issueToken({
2815
- claims: { sub: id }
2816
- });
2817
- return { id, token };
2818
- };
2819
- const createSamlProvider = (options) => {
2820
- return ({
2821
- providerId,
2822
- globalConfig,
2823
- config,
2824
- tokenIssuer,
2825
- tokenManager,
2826
- catalogApi,
2827
- logger
2828
- }) => {
2829
- var _a, _b;
2830
- const catalogIdentityClient = new CatalogIdentityClient({
2831
- catalogApi,
2832
- tokenManager
2833
- });
2834
- const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ fullProfile }) => ({
2835
- profile: {
2836
- email: fullProfile.email,
2837
- displayName: fullProfile.displayName
2838
- }
2839
- });
2840
- const signInResolverFn = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : samlDefaultSignInResolver;
2841
- const signInResolver = (info) => signInResolverFn(info, {
2842
- catalogIdentityClient,
2843
- tokenIssuer,
2844
- logger
2845
- });
2846
- return new SamlAuthProvider({
2847
- callbackUrl: `${globalConfig.baseUrl}/${providerId}/handler/frame`,
2848
- entryPoint: config.getString("entryPoint"),
2849
- logoutUrl: config.getOptionalString("logoutUrl"),
2850
- audience: config.getOptionalString("audience"),
2851
- issuer: config.getString("issuer"),
2852
- cert: config.getString("cert"),
2853
- privateKey: config.getOptionalString("privateKey"),
2854
- authnContext: config.getOptionalStringArray("authnContext"),
2855
- identifierFormat: config.getOptionalString("identifierFormat"),
2856
- decryptionPvk: config.getOptionalString("decryptionPvk"),
2857
- signatureAlgorithm: config.getOptionalString("signatureAlgorithm"),
2858
- digestAlgorithm: config.getOptionalString("digestAlgorithm"),
2859
- acceptedClockSkewMs: config.getOptionalNumber("acceptedClockSkewMs"),
2860
- tokenIssuer,
2861
- appUrl: globalConfig.appUrl,
2862
- authHandler,
2863
- signInResolver,
2864
- logger,
2865
- catalogIdentityClient
2866
- });
2867
- };
2868
- };
2869
-
2870
- const IAP_JWT_HEADER = "x-goog-iap-jwt-assertion";
2871
2835
 
2872
- function createTokenValidator(audience, mockClient) {
2873
- const client = mockClient != null ? mockClient : new googleAuthLibrary.OAuth2Client();
2874
- return async function tokenValidator(token) {
2875
- const response = await client.getIapPublicKeys();
2876
- const ticket = await client.verifySignedJwtWithCertsAsync(token, response.pubkeys, audience, ["https://cloud.google.com/iap"]);
2877
- const payload = ticket.getPayload();
2878
- if (!payload) {
2879
- throw new TypeError("Token had no payload");
2880
- }
2881
- return payload;
2882
- };
2883
- }
2884
- async function parseRequestToken(jwtToken, tokenValidator) {
2885
- if (typeof jwtToken !== "string" || !jwtToken) {
2886
- throw new errors.AuthenticationError(`Missing Google IAP header: ${IAP_JWT_HEADER}`);
2887
- }
2888
- let payload;
2889
- try {
2890
- payload = await tokenValidator(jwtToken);
2891
- } catch (e) {
2892
- throw new errors.AuthenticationError(`Google IAP token verification failed, ${e}`);
2836
+ class MemoryKeyStore {
2837
+ constructor() {
2838
+ this.keys = /* @__PURE__ */ new Map();
2893
2839
  }
2894
- if (!payload.sub || !payload.email) {
2895
- throw new errors.AuthenticationError("Google IAP token payload is missing sub and/or email claim");
2840
+ async addKey(key) {
2841
+ this.keys.set(key.kid, {
2842
+ createdAt: luxon.DateTime.utc().toJSDate(),
2843
+ key: JSON.stringify(key)
2844
+ });
2896
2845
  }
2897
- return {
2898
- iapToken: {
2899
- ...payload,
2900
- sub: payload.sub,
2901
- email: payload.email
2846
+ async removeKeys(kids) {
2847
+ for (const kid of kids) {
2848
+ this.keys.delete(kid);
2902
2849
  }
2903
- };
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
+ }
2904
2859
  }
2905
- const defaultAuthHandler = async ({
2906
- iapToken
2907
- }) => ({ profile: { email: iapToken.email } });
2908
2860
 
2909
- class GcpIapProvider {
2910
- constructor(options) {
2911
- this.authHandler = options.authHandler;
2912
- this.signInResolver = options.signInResolver;
2913
- this.tokenValidator = options.tokenValidator;
2914
- this.tokenIssuer = options.tokenIssuer;
2915
- this.catalogIdentityClient = options.catalogIdentityClient;
2916
- this.logger = options.logger;
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;
2917
2868
  }
2918
- async start() {
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);
2919
2873
  }
2920
- async frameHandler() {
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
+ }
2921
2883
  }
2922
- async refresh(req, res) {
2923
- const result = await parseRequestToken(req.header(IAP_JWT_HEADER), this.tokenValidator);
2924
- const context = {
2925
- logger: this.logger,
2926
- catalogIdentityClient: this.catalogIdentityClient,
2927
- tokenIssuer: this.tokenIssuer
2928
- };
2929
- const { profile } = await this.authHandler(result, context);
2930
- const backstageIdentity = await this.signInResolver({ profile, result }, context);
2931
- const response = {
2932
- providerInfo: { iapToken: result.iapToken },
2933
- profile,
2934
- backstageIdentity: prepareBackstageIdentityResponse(backstageIdentity)
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
+ }))
2935
2897
  };
2936
- res.json(response);
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());
2937
2912
  }
2938
2913
  }
2939
- function createGcpIapProvider(options) {
2940
- return ({ config, tokenIssuer, catalogApi, logger, tokenManager }) => {
2914
+
2915
+ class KeyStores {
2916
+ static async fromConfig(config, options) {
2941
2917
  var _a;
2942
- const audience = config.getString("audience");
2943
- const authHandler = (_a = options.authHandler) != null ? _a : defaultAuthHandler;
2944
- const signInResolver = options.signIn.resolver;
2945
- const tokenValidator = createTokenValidator(audience);
2946
- const catalogIdentityClient = new CatalogIdentityClient({
2947
- catalogApi,
2948
- tokenManager
2949
- });
2950
- return new GcpIapProvider({
2951
- authHandler,
2952
- signInResolver,
2953
- tokenValidator,
2954
- tokenIssuer,
2955
- catalogIdentityClient,
2956
- logger
2957
- });
2958
- };
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
+ }
2959
2949
  }
2960
2950
 
2961
- const factories = {
2962
- google: createGoogleProvider(),
2963
- github: createGithubProvider(),
2964
- gitlab: createGitlabProvider(),
2965
- saml: createSamlProvider(),
2966
- okta: createOktaProvider(),
2967
- auth0: createAuth0Provider(),
2968
- microsoft: createMicrosoftProvider(),
2969
- oauth2: createOAuth2Provider(),
2970
- oidc: createOidcProvider(),
2971
- onelogin: createOneLoginProvider(),
2972
- awsalb: createAwsAlbProvider(),
2973
- bitbucket: createBitbucketProvider(),
2974
- atlassian: createAtlassianProvider()
2975
- };
2976
-
2977
2951
  async function createRouter(options) {
2978
2952
  const {
2979
2953
  logger,
@@ -3025,7 +2999,11 @@ async function createRouter(options) {
3025
2999
  try {
3026
3000
  const provider = providerFactory({
3027
3001
  providerId,
3028
- globalConfig: { baseUrl: authUrl, appUrl, isOriginAllowed },
3002
+ globalConfig: {
3003
+ baseUrl: authUrl,
3004
+ appUrl,
3005
+ isOriginAllowed
3006
+ },
3029
3007
  config: providersConfig.getConfig(providerId),
3030
3008
  logger,
3031
3009
  tokenManager,
@@ -3085,7 +3063,6 @@ function createOriginFilter(config) {
3085
3063
  }
3086
3064
 
3087
3065
  exports.CatalogIdentityClient = CatalogIdentityClient;
3088
- exports.IdentityClient = IdentityClient;
3089
3066
  exports.OAuthAdapter = OAuthAdapter;
3090
3067
  exports.OAuthEnvironmentHandler = OAuthEnvironmentHandler;
3091
3068
  exports.bitbucketUserIdSignInResolver = bitbucketUserIdSignInResolver;