@backstage/plugin-auth-backend 0.6.1 → 0.8.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,17 @@ 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
28
  var openidClient = require('openid-client');
24
29
  var passportOktaOauth = require('passport-okta-oauth');
25
30
  var passportOneloginOauth = require('passport-onelogin-oauth');
26
31
  var passportSaml = require('passport-saml');
27
32
  var googleAuthLibrary = require('google-auth-library');
28
33
  var catalogClient = require('@backstage/catalog-client');
29
- var uuid = require('uuid');
30
- var luxon = require('luxon');
31
- var backendCommon = require('@backstage/backend-common');
32
- var firestore = require('@google-cloud/firestore');
33
- var lodash = require('lodash');
34
34
  var session = require('express-session');
35
35
  var passport = require('passport');
36
36
  var minimatch = require('minimatch');
@@ -149,6 +149,16 @@ const verifyNonce = (req, providerId) => {
149
149
  throw new Error("Invalid nonce");
150
150
  }
151
151
  };
152
+ const getCookieConfig = (authUrl, providerId) => {
153
+ const { hostname: cookieDomain, pathname, protocol } = authUrl;
154
+ 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
+ };
161
+ };
152
162
 
153
163
  class OAuthEnvironmentHandler {
154
164
  constructor(handlers) {
@@ -245,6 +255,10 @@ function parseJwtPayload(token) {
245
255
  }
246
256
  function prepareBackstageIdentityResponse(result) {
247
257
  const { sub, ent } = parseJwtPayload(result.token);
258
+ const userEntityRef = catalogModel.stringifyEntityRef(catalogModel.parseEntityRef(sub, {
259
+ defaultKind: "user",
260
+ defaultNamespace: catalogModel.ENTITY_DEFAULT_NAMESPACE
261
+ }));
248
262
  return {
249
263
  ...{
250
264
  idToken: result.token,
@@ -252,7 +266,7 @@ function prepareBackstageIdentityResponse(result) {
252
266
  },
253
267
  identity: {
254
268
  type: "user",
255
- userEntityRef: sub,
269
+ userEntityRef,
256
270
  ownershipEntityRefs: ent != null ? ent : []
257
271
  }
258
272
  };
@@ -309,14 +323,14 @@ class OAuthAdapter {
309
323
  };
310
324
  }
311
325
  static fromConfig(config, handlers, options) {
326
+ var _a;
312
327
  const { origin: appOrigin } = new url.URL(config.appUrl);
313
- const secure = config.baseUrl.startsWith("https://");
314
- const url$1 = new url.URL(config.baseUrl);
315
- const cookiePath = `${url$1.pathname}/${options.providerId}`;
328
+ const authUrl = new url.URL((_a = options.callbackUrl) != null ? _a : config.baseUrl);
329
+ const { cookieDomain, cookiePath, secure } = getCookieConfig(authUrl, options.providerId);
316
330
  return new OAuthAdapter(handlers, {
317
331
  ...options,
318
332
  appOrigin,
319
- cookieDomain: url$1.hostname,
333
+ cookieDomain,
320
334
  cookiePath,
321
335
  secure,
322
336
  isOriginAllowed: config.isOriginAllowed
@@ -655,7 +669,12 @@ class AtlassianAuthProvider {
655
669
  };
656
670
  }
657
671
  async handleResult(result) {
658
- const { profile } = await this.authHandler(result);
672
+ const context = {
673
+ logger: this.logger,
674
+ catalogIdentityClient: this.catalogIdentityClient,
675
+ tokenIssuer: this.tokenIssuer
676
+ };
677
+ const { profile } = await this.authHandler(result, context);
659
678
  const response = {
660
679
  providerInfo: {
661
680
  idToken: result.params.id_token,
@@ -669,11 +688,7 @@ class AtlassianAuthProvider {
669
688
  response.backstageIdentity = await this.signInResolver({
670
689
  result,
671
690
  profile
672
- }, {
673
- tokenIssuer: this.tokenIssuer,
674
- catalogIdentityClient: this.catalogIdentityClient,
675
- logger: this.logger
676
- });
691
+ }, context);
677
692
  }
678
693
  return response;
679
694
  }
@@ -793,7 +808,12 @@ class Auth0AuthProvider {
793
808
  };
794
809
  }
795
810
  async handleResult(result) {
796
- const { profile } = await this.authHandler(result);
811
+ const context = {
812
+ logger: this.logger,
813
+ catalogIdentityClient: this.catalogIdentityClient,
814
+ tokenIssuer: this.tokenIssuer
815
+ };
816
+ const { profile } = await this.authHandler(result, context);
797
817
  const response = {
798
818
  providerInfo: {
799
819
  idToken: result.params.id_token,
@@ -807,11 +827,7 @@ class Auth0AuthProvider {
807
827
  response.backstageIdentity = await this.signInResolver({
808
828
  result,
809
829
  profile
810
- }, {
811
- tokenIssuer: this.tokenIssuer,
812
- catalogIdentityClient: this.catalogIdentityClient,
813
- logger: this.logger
814
- });
830
+ }, context);
815
831
  }
816
832
  return response;
817
833
  }
@@ -937,15 +953,16 @@ class AwsAlbAuthProvider {
937
953
  }
938
954
  }
939
955
  async handleResult(result) {
940
- const { profile } = await this.authHandler(result);
941
- const backstageIdentity = await this.signInResolver({
942
- result,
943
- profile
944
- }, {
956
+ const context = {
945
957
  tokenIssuer: this.tokenIssuer,
946
958
  catalogIdentityClient: this.catalogIdentityClient,
947
959
  logger: this.logger
948
- });
960
+ };
961
+ const { profile } = await this.authHandler(result, context);
962
+ const backstageIdentity = await this.signInResolver({
963
+ result,
964
+ profile
965
+ }, context);
949
966
  return {
950
967
  providerInfo: {
951
968
  accessToken: result.accessToken,
@@ -1045,7 +1062,12 @@ class BitbucketAuthProvider {
1045
1062
  }
1046
1063
  async handleResult(result) {
1047
1064
  result.fullProfile.avatarUrl = result.fullProfile._json.links.avatar.href;
1048
- const { profile } = await this.authHandler(result);
1065
+ const context = {
1066
+ logger: this.logger,
1067
+ catalogIdentityClient: this.catalogIdentityClient,
1068
+ tokenIssuer: this.tokenIssuer
1069
+ };
1070
+ const { profile } = await this.authHandler(result, context);
1049
1071
  const response = {
1050
1072
  providerInfo: {
1051
1073
  idToken: result.params.id_token,
@@ -1059,11 +1081,7 @@ class BitbucketAuthProvider {
1059
1081
  response.backstageIdentity = await this.signInResolver({
1060
1082
  result,
1061
1083
  profile
1062
- }, {
1063
- tokenIssuer: this.tokenIssuer,
1064
- catalogIdentityClient: this.catalogIdentityClient,
1065
- logger: this.logger
1066
- });
1084
+ }, context);
1067
1085
  }
1068
1086
  return response;
1069
1087
  }
@@ -1179,7 +1197,12 @@ class GithubAuthProvider {
1179
1197
  };
1180
1198
  }
1181
1199
  async handleResult(result) {
1182
- const { profile } = await this.authHandler(result);
1200
+ const context = {
1201
+ logger: this.logger,
1202
+ catalogIdentityClient: this.catalogIdentityClient,
1203
+ tokenIssuer: this.tokenIssuer
1204
+ };
1205
+ const { profile } = await this.authHandler(result, context);
1183
1206
  const expiresInStr = result.params.expires_in;
1184
1207
  const response = {
1185
1208
  providerInfo: {
@@ -1193,11 +1216,7 @@ class GithubAuthProvider {
1193
1216
  response.backstageIdentity = await this.signInResolver({
1194
1217
  result,
1195
1218
  profile
1196
- }, {
1197
- tokenIssuer: this.tokenIssuer,
1198
- catalogIdentityClient: this.catalogIdentityClient,
1199
- logger: this.logger
1200
- });
1219
+ }, context);
1201
1220
  }
1202
1221
  return response;
1203
1222
  }
@@ -1206,7 +1225,10 @@ const githubDefaultSignInResolver = async (info, ctx) => {
1206
1225
  const { fullProfile } = info.result;
1207
1226
  const userId = fullProfile.username || fullProfile.id;
1208
1227
  const token = await ctx.tokenIssuer.issueToken({
1209
- claims: { sub: userId, ent: [`user:default/${userId}`] }
1228
+ claims: {
1229
+ sub: `user:default/${userId}`,
1230
+ ent: [`user:default/${userId}`]
1231
+ }
1210
1232
  });
1211
1233
  return { id: userId, token };
1212
1234
  };
@@ -1261,7 +1283,8 @@ const createGithubProvider = (options) => {
1261
1283
  return OAuthAdapter.fromConfig(globalConfig, provider, {
1262
1284
  persistScopes: true,
1263
1285
  providerId,
1264
- tokenIssuer
1286
+ tokenIssuer,
1287
+ callbackUrl
1265
1288
  });
1266
1289
  });
1267
1290
  };
@@ -1273,7 +1296,7 @@ const gitlabDefaultSignInResolver = async (info, ctx) => {
1273
1296
  id = profile.email.split("@")[0];
1274
1297
  }
1275
1298
  const token = await ctx.tokenIssuer.issueToken({
1276
- claims: { sub: id, ent: [`user:default/${id}`] }
1299
+ claims: { sub: `user:default/${id}`, ent: [`user:default/${id}`] }
1277
1300
  });
1278
1301
  return { id, token };
1279
1302
  };
@@ -1327,7 +1350,12 @@ class GitlabAuthProvider {
1327
1350
  };
1328
1351
  }
1329
1352
  async handleResult(result) {
1330
- const { profile } = await this.authHandler(result);
1353
+ const context = {
1354
+ logger: this.logger,
1355
+ catalogIdentityClient: this.catalogIdentityClient,
1356
+ tokenIssuer: this.tokenIssuer
1357
+ };
1358
+ const { profile } = await this.authHandler(result, context);
1331
1359
  const response = {
1332
1360
  providerInfo: {
1333
1361
  idToken: result.params.id_token,
@@ -1341,11 +1369,7 @@ class GitlabAuthProvider {
1341
1369
  response.backstageIdentity = await this.signInResolver({
1342
1370
  result,
1343
1371
  profile
1344
- }, {
1345
- tokenIssuer: this.tokenIssuer,
1346
- catalogIdentityClient: this.catalogIdentityClient,
1347
- logger: this.logger
1348
- });
1372
+ }, context);
1349
1373
  }
1350
1374
  return response;
1351
1375
  }
@@ -1446,7 +1470,12 @@ class GoogleAuthProvider {
1446
1470
  };
1447
1471
  }
1448
1472
  async handleResult(result) {
1449
- const { profile } = await this.authHandler(result);
1473
+ const context = {
1474
+ logger: this.logger,
1475
+ catalogIdentityClient: this.catalogIdentityClient,
1476
+ tokenIssuer: this.tokenIssuer
1477
+ };
1478
+ const { profile } = await this.authHandler(result, context);
1450
1479
  const response = {
1451
1480
  providerInfo: {
1452
1481
  idToken: result.params.id_token,
@@ -1460,11 +1489,7 @@ class GoogleAuthProvider {
1460
1489
  response.backstageIdentity = await this.signInResolver({
1461
1490
  result,
1462
1491
  profile
1463
- }, {
1464
- tokenIssuer: this.tokenIssuer,
1465
- catalogIdentityClient: this.catalogIdentityClient,
1466
- logger: this.logger
1467
- });
1492
+ }, context);
1468
1493
  }
1469
1494
  return response;
1470
1495
  }
@@ -1501,7 +1526,7 @@ const googleDefaultSignInResolver = async (info, ctx) => {
1501
1526
  userId = profile.email.split("@")[0];
1502
1527
  }
1503
1528
  const token = await ctx.tokenIssuer.issueToken({
1504
- claims: { sub: userId, ent: [`user:default/${userId}`] }
1529
+ claims: { sub: `user:default/${userId}`, ent: [`user:default/${userId}`] }
1505
1530
  });
1506
1531
  return { id: userId, token };
1507
1532
  };
@@ -1595,7 +1620,12 @@ class MicrosoftAuthProvider {
1595
1620
  async handleResult(result) {
1596
1621
  const photo = await this.getUserPhoto(result.accessToken);
1597
1622
  result.fullProfile.photos = photo ? [{ value: photo }] : void 0;
1598
- const { profile } = await this.authHandler(result);
1623
+ const context = {
1624
+ logger: this.logger,
1625
+ catalogIdentityClient: this.catalogIdentityClient,
1626
+ tokenIssuer: this.tokenIssuer
1627
+ };
1628
+ const { profile } = await this.authHandler(result, context);
1599
1629
  const response = {
1600
1630
  providerInfo: {
1601
1631
  idToken: result.params.id_token,
@@ -1609,11 +1639,7 @@ class MicrosoftAuthProvider {
1609
1639
  response.backstageIdentity = await this.signInResolver({
1610
1640
  result,
1611
1641
  profile
1612
- }, {
1613
- tokenIssuer: this.tokenIssuer,
1614
- catalogIdentityClient: this.catalogIdentityClient,
1615
- logger: this.logger
1616
- });
1642
+ }, context);
1617
1643
  }
1618
1644
  return response;
1619
1645
  }
@@ -1654,7 +1680,10 @@ const microsoftDefaultSignInResolver = async (info, ctx) => {
1654
1680
  }
1655
1681
  const userId = profile.email.split("@")[0];
1656
1682
  const token = await ctx.tokenIssuer.issueToken({
1657
- claims: { sub: userId, ent: [`user:default/${userId}`] }
1683
+ claims: {
1684
+ sub: `user:default/${userId}`,
1685
+ ent: [`user:default/${userId}`]
1686
+ }
1658
1687
  });
1659
1688
  return { id: userId, token };
1660
1689
  };
@@ -1765,7 +1794,12 @@ class OAuth2AuthProvider {
1765
1794
  };
1766
1795
  }
1767
1796
  async handleResult(result) {
1768
- const { profile } = await this.authHandler(result);
1797
+ const context = {
1798
+ logger: this.logger,
1799
+ catalogIdentityClient: this.catalogIdentityClient,
1800
+ tokenIssuer: this.tokenIssuer
1801
+ };
1802
+ const { profile } = await this.authHandler(result, context);
1769
1803
  const response = {
1770
1804
  providerInfo: {
1771
1805
  idToken: result.params.id_token,
@@ -1779,11 +1813,7 @@ class OAuth2AuthProvider {
1779
1813
  response.backstageIdentity = await this.signInResolver({
1780
1814
  result,
1781
1815
  profile
1782
- }, {
1783
- tokenIssuer: this.tokenIssuer,
1784
- catalogIdentityClient: this.catalogIdentityClient,
1785
- logger: this.logger
1786
- });
1816
+ }, context);
1787
1817
  }
1788
1818
  return response;
1789
1819
  }
@@ -1798,7 +1828,7 @@ const oAuth2DefaultSignInResolver$1 = async (info, ctx) => {
1798
1828
  }
1799
1829
  const userId = profile.email.split("@")[0];
1800
1830
  const token = await ctx.tokenIssuer.issueToken({
1801
- claims: { sub: userId, ent: [`user:default/${userId}`] }
1831
+ claims: { sub: `user:default/${userId}`, ent: [`user:default/${userId}`] }
1802
1832
  });
1803
1833
  return { id: userId, token };
1804
1834
  };
@@ -1855,110 +1885,528 @@ const createOAuth2Provider = (options) => {
1855
1885
  });
1856
1886
  };
1857
1887
 
1858
- class OidcAuthProvider {
1888
+ function createOidcRouter(options) {
1889
+ const { baseUrl, tokenIssuer } = options;
1890
+ const router = Router__default["default"]();
1891
+ const config = {
1892
+ issuer: baseUrl,
1893
+ token_endpoint: `${baseUrl}/v1/token`,
1894
+ userinfo_endpoint: `${baseUrl}/v1/userinfo`,
1895
+ jwks_uri: `${baseUrl}/.well-known/jwks.json`,
1896
+ response_types_supported: ["id_token"],
1897
+ subject_types_supported: ["public"],
1898
+ id_token_signing_alg_values_supported: ["RS256"],
1899
+ scopes_supported: ["openid"],
1900
+ token_endpoint_auth_methods_supported: [],
1901
+ claims_supported: ["sub"],
1902
+ grant_types_supported: []
1903
+ };
1904
+ router.get("/.well-known/openid-configuration", (_req, res) => {
1905
+ res.json(config);
1906
+ });
1907
+ router.get("/.well-known/jwks.json", async (_req, res) => {
1908
+ const { keys } = await tokenIssuer.listPublicKeys();
1909
+ res.json({ keys });
1910
+ });
1911
+ router.get("/v1/token", (_req, res) => {
1912
+ res.status(501).send("Not Implemented");
1913
+ });
1914
+ router.get("/v1/userinfo", (_req, res) => {
1915
+ res.status(501).send("Not Implemented");
1916
+ });
1917
+ return router;
1918
+ }
1919
+
1920
+ const CLOCK_MARGIN_S = 10;
1921
+ class IdentityClient {
1859
1922
  constructor(options) {
1860
- this.implementation = this.setupStrategy(options);
1861
- this.scope = options.scope;
1862
- this.prompt = options.prompt;
1863
- this.signInResolver = options.signInResolver;
1864
- this.authHandler = options.authHandler;
1865
- this.tokenIssuer = options.tokenIssuer;
1866
- this.catalogIdentityClient = options.catalogIdentityClient;
1867
- this.logger = options.logger;
1923
+ this.discovery = options.discovery;
1924
+ this.issuer = options.issuer;
1925
+ this.keyStore = new jose.JWKS.KeyStore();
1926
+ this.keyStoreUpdated = 0;
1868
1927
  }
1869
- async start(req) {
1870
- const { strategy } = await this.implementation;
1871
- const options = {
1872
- scope: req.scope || this.scope || "openid profile email",
1873
- state: encodeState(req.state)
1874
- };
1875
- const prompt = this.prompt || "none";
1876
- if (prompt !== "auto") {
1877
- options.prompt = prompt;
1928
+ async authenticate(token) {
1929
+ var _a;
1930
+ if (!token) {
1931
+ throw new errors.AuthenticationError("No token specified");
1878
1932
  }
1879
- return await executeRedirectStrategy(req, strategy, options);
1880
- }
1881
- async handler(req) {
1882
- const { strategy } = await this.implementation;
1883
- const { result, privateInfo } = await executeFrameHandlerStrategy(req, strategy);
1884
- return {
1885
- response: await this.handleResult(result),
1886
- refreshToken: privateInfo.refreshToken
1933
+ const key = await this.getKey(token);
1934
+ if (!key) {
1935
+ throw new errors.AuthenticationError("No signing key matching token found");
1936
+ }
1937
+ const decoded = jose.JWT.IdToken.verify(token, key, {
1938
+ algorithms: ["ES256"],
1939
+ audience: "backstage",
1940
+ issuer: this.issuer
1941
+ });
1942
+ if (!decoded.sub) {
1943
+ throw new errors.AuthenticationError("No user sub found in token");
1944
+ }
1945
+ const user = {
1946
+ id: decoded.sub,
1947
+ token,
1948
+ identity: {
1949
+ type: "user",
1950
+ userEntityRef: decoded.sub,
1951
+ ownershipEntityRefs: (_a = decoded.ent) != null ? _a : []
1952
+ }
1887
1953
  };
1954
+ return user;
1888
1955
  }
1889
- async refresh(req) {
1890
- const { client } = await this.implementation;
1891
- const tokenset = await client.refresh(req.refreshToken);
1892
- if (!tokenset.access_token) {
1893
- throw new Error("Refresh failed");
1956
+ static getBearerToken(authorizationHeader) {
1957
+ if (typeof authorizationHeader !== "string") {
1958
+ return void 0;
1894
1959
  }
1895
- const userinfo = await client.userinfo(tokenset.access_token);
1896
- return {
1897
- response: await this.handleResult({ tokenset, userinfo }),
1898
- refreshToken: tokenset.refresh_token
1899
- };
1960
+ const matches = authorizationHeader.match(/Bearer\s+(\S+)/i);
1961
+ return matches == null ? void 0 : matches[1];
1900
1962
  }
1901
- async setupStrategy(options) {
1902
- const issuer = await openidClient.Issuer.discover(options.metadataUrl);
1903
- const client = new issuer.Client({
1904
- access_type: "offline",
1905
- client_id: options.clientId,
1906
- client_secret: options.clientSecret,
1907
- redirect_uris: [options.callbackUrl],
1908
- response_types: ["code"],
1909
- id_token_signed_response_alg: options.tokenSignedResponseAlg || "RS256",
1910
- scope: options.scope || ""
1911
- });
1912
- const strategy = new openidClient.Strategy({
1913
- client,
1914
- passReqToCallback: false
1915
- }, (tokenset, userinfo, done) => {
1916
- if (typeof done !== "function") {
1917
- throw new Error("OIDC IdP must provide a userinfo_endpoint in the metadata response");
1918
- }
1919
- done(void 0, { tokenset, userinfo }, {
1920
- refreshToken: tokenset.refresh_token
1921
- });
1963
+ async getKey(rawJwtToken) {
1964
+ const { header, payload } = jose.JWT.decode(rawJwtToken, {
1965
+ complete: true
1922
1966
  });
1923
- strategy.error = console.error;
1924
- return { strategy, client };
1967
+ const keyStoreHasKey = !!this.keyStore.get({ kid: header.kid });
1968
+ const issuedAfterLastRefresh = (payload == null ? void 0 : payload.iat) && payload.iat > this.keyStoreUpdated - CLOCK_MARGIN_S;
1969
+ if (!keyStoreHasKey && issuedAfterLastRefresh) {
1970
+ await this.refreshKeyStore();
1971
+ }
1972
+ return this.keyStore.get({ kid: header.kid });
1925
1973
  }
1926
- async handleResult(result) {
1927
- const { profile } = await this.authHandler(result);
1928
- const response = {
1929
- providerInfo: {
1930
- idToken: result.tokenset.id_token,
1931
- accessToken: result.tokenset.access_token,
1932
- scope: result.tokenset.scope,
1933
- expiresInSeconds: result.tokenset.expires_in
1934
- },
1935
- profile
1936
- };
1937
- if (this.signInResolver) {
1938
- response.backstageIdentity = await this.signInResolver({
1939
- result,
1940
- profile
1941
- }, {
1942
- tokenIssuer: this.tokenIssuer,
1943
- catalogIdentityClient: this.catalogIdentityClient,
1944
- logger: this.logger
1945
- });
1974
+ async listPublicKeys() {
1975
+ const url = `${await this.discovery.getBaseUrl("auth")}/.well-known/jwks.json`;
1976
+ const response = await fetch__default["default"](url);
1977
+ if (!response.ok) {
1978
+ const payload = await response.text();
1979
+ const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
1980
+ throw new Error(message);
1946
1981
  }
1947
- return response;
1982
+ const publicKeys = await response.json();
1983
+ return publicKeys;
1984
+ }
1985
+ async refreshKeyStore() {
1986
+ const now = Date.now() / 1e3;
1987
+ const publicKeys = await this.listPublicKeys();
1988
+ this.keyStore = jose.JWKS.asKeyStore({
1989
+ keys: publicKeys.keys.map((key) => key)
1990
+ });
1991
+ this.keyStoreUpdated = now;
1948
1992
  }
1949
1993
  }
1950
- const oAuth2DefaultSignInResolver = async (info, ctx) => {
1951
- const { profile } = info;
1952
- if (!profile.email) {
1953
- throw new Error("Profile contained no email");
1994
+
1995
+ const MS_IN_S = 1e3;
1996
+ class TokenFactory {
1997
+ constructor(options) {
1998
+ this.issuer = options.issuer;
1999
+ this.logger = options.logger;
2000
+ this.keyStore = options.keyStore;
2001
+ this.keyDurationSeconds = options.keyDurationSeconds;
1954
2002
  }
1955
- const userId = profile.email.split("@")[0];
1956
- const token = await ctx.tokenIssuer.issueToken({
1957
- claims: { sub: userId, ent: [`user:default/${userId}`] }
1958
- });
1959
- return { id: userId, token };
1960
- };
1961
- const createOidcProvider = (options) => {
2003
+ async issueToken(params) {
2004
+ const key = await this.getKey();
2005
+ const iss = this.issuer;
2006
+ const sub = params.claims.sub;
2007
+ const ent = params.claims.ent;
2008
+ const aud = "backstage";
2009
+ const iat = Math.floor(Date.now() / MS_IN_S);
2010
+ const exp = iat + this.keyDurationSeconds;
2011
+ this.logger.info(`Issuing token for ${sub}, with entities ${ent != null ? ent : []}`);
2012
+ return jose.JWS.sign({ iss, sub, aud, iat, exp, ent }, key, {
2013
+ alg: key.alg,
2014
+ kid: key.kid
2015
+ });
2016
+ }
2017
+ async listPublicKeys() {
2018
+ const { items: keys } = await this.keyStore.listKeys();
2019
+ const validKeys = [];
2020
+ const expiredKeys = [];
2021
+ for (const key of keys) {
2022
+ const expireAt = luxon.DateTime.fromJSDate(key.createdAt).plus({
2023
+ seconds: 3 * this.keyDurationSeconds
2024
+ });
2025
+ if (expireAt < luxon.DateTime.local()) {
2026
+ expiredKeys.push(key);
2027
+ } else {
2028
+ validKeys.push(key);
2029
+ }
2030
+ }
2031
+ if (expiredKeys.length > 0) {
2032
+ const kids = expiredKeys.map(({ key }) => key.kid);
2033
+ this.logger.info(`Removing expired signing keys, '${kids.join("', '")}'`);
2034
+ this.keyStore.removeKeys(kids).catch((error) => {
2035
+ this.logger.error(`Failed to remove expired keys, ${error}`);
2036
+ });
2037
+ }
2038
+ return { keys: validKeys.map(({ key }) => key) };
2039
+ }
2040
+ async getKey() {
2041
+ if (this.privateKeyPromise) {
2042
+ if (this.keyExpiry && luxon.DateTime.fromJSDate(this.keyExpiry) > luxon.DateTime.local()) {
2043
+ return this.privateKeyPromise;
2044
+ }
2045
+ this.logger.info(`Signing key has expired, generating new key`);
2046
+ delete this.privateKeyPromise;
2047
+ }
2048
+ this.keyExpiry = luxon.DateTime.utc().plus({
2049
+ seconds: this.keyDurationSeconds
2050
+ }).toJSDate();
2051
+ const promise = (async () => {
2052
+ const key = await jose.JWK.generate("EC", "P-256", {
2053
+ use: "sig",
2054
+ kid: uuid.v4(),
2055
+ alg: "ES256"
2056
+ });
2057
+ this.logger.info(`Created new signing key ${key.kid}`);
2058
+ await this.keyStore.addKey(key.toJWK(false));
2059
+ return key;
2060
+ })();
2061
+ this.privateKeyPromise = promise;
2062
+ try {
2063
+ await promise;
2064
+ } catch (error) {
2065
+ this.logger.error(`Failed to generate new signing key, ${error}`);
2066
+ delete this.keyExpiry;
2067
+ delete this.privateKeyPromise;
2068
+ }
2069
+ return promise;
2070
+ }
2071
+ }
2072
+
2073
+ const migrationsDir = backendCommon.resolvePackagePath("@backstage/plugin-auth-backend", "migrations");
2074
+ const TABLE = "signing_keys";
2075
+ const parseDate = (date) => {
2076
+ const parsedDate = typeof date === "string" ? luxon.DateTime.fromSQL(date, { zone: "UTC" }) : luxon.DateTime.fromJSDate(date);
2077
+ if (!parsedDate.isValid) {
2078
+ throw new Error(`Failed to parse date, reason: ${parsedDate.invalidReason}, explanation: ${parsedDate.invalidExplanation}`);
2079
+ }
2080
+ return parsedDate.toJSDate();
2081
+ };
2082
+ class DatabaseKeyStore {
2083
+ static async create(options) {
2084
+ const { database } = options;
2085
+ await database.migrate.latest({
2086
+ directory: migrationsDir
2087
+ });
2088
+ return new DatabaseKeyStore(options);
2089
+ }
2090
+ constructor(options) {
2091
+ this.database = options.database;
2092
+ }
2093
+ async addKey(key) {
2094
+ await this.database(TABLE).insert({
2095
+ kid: key.kid,
2096
+ key: JSON.stringify(key)
2097
+ });
2098
+ }
2099
+ async listKeys() {
2100
+ const rows = await this.database(TABLE).select();
2101
+ return {
2102
+ items: rows.map((row) => ({
2103
+ key: JSON.parse(row.key),
2104
+ createdAt: parseDate(row.created_at)
2105
+ }))
2106
+ };
2107
+ }
2108
+ async removeKeys(kids) {
2109
+ await this.database(TABLE).delete().whereIn("kid", kids);
2110
+ }
2111
+ }
2112
+
2113
+ class MemoryKeyStore {
2114
+ constructor() {
2115
+ this.keys = /* @__PURE__ */ new Map();
2116
+ }
2117
+ async addKey(key) {
2118
+ this.keys.set(key.kid, {
2119
+ createdAt: luxon.DateTime.utc().toJSDate(),
2120
+ key: JSON.stringify(key)
2121
+ });
2122
+ }
2123
+ async removeKeys(kids) {
2124
+ for (const kid of kids) {
2125
+ this.keys.delete(kid);
2126
+ }
2127
+ }
2128
+ async listKeys() {
2129
+ return {
2130
+ items: Array.from(this.keys).map(([, { createdAt, key: keyStr }]) => ({
2131
+ createdAt,
2132
+ key: JSON.parse(keyStr)
2133
+ }))
2134
+ };
2135
+ }
2136
+ }
2137
+
2138
+ const DEFAULT_TIMEOUT_MS = 1e4;
2139
+ const DEFAULT_DOCUMENT_PATH = "sessions";
2140
+ class FirestoreKeyStore {
2141
+ constructor(database, path, timeout) {
2142
+ this.database = database;
2143
+ this.path = path;
2144
+ this.timeout = timeout;
2145
+ }
2146
+ static async create(settings) {
2147
+ const { path, timeout, ...firestoreSettings } = settings != null ? settings : {};
2148
+ const database = new firestore.Firestore(firestoreSettings);
2149
+ return new FirestoreKeyStore(database, path != null ? path : DEFAULT_DOCUMENT_PATH, timeout != null ? timeout : DEFAULT_TIMEOUT_MS);
2150
+ }
2151
+ static async verifyConnection(keyStore, logger) {
2152
+ try {
2153
+ await keyStore.verify();
2154
+ } catch (error) {
2155
+ if (process.env.NODE_ENV !== "development") {
2156
+ throw new Error(`Failed to connect to database: ${error.message}`);
2157
+ }
2158
+ logger == null ? void 0 : logger.warn(`Failed to connect to database: ${error.message}`);
2159
+ }
2160
+ }
2161
+ async addKey(key) {
2162
+ await this.withTimeout(this.database.collection(this.path).doc(key.kid).set({
2163
+ kid: key.kid,
2164
+ key: JSON.stringify(key)
2165
+ }));
2166
+ }
2167
+ async listKeys() {
2168
+ const keys = await this.withTimeout(this.database.collection(this.path).get());
2169
+ return {
2170
+ items: keys.docs.map((key) => ({
2171
+ key: key.data(),
2172
+ createdAt: key.createTime.toDate()
2173
+ }))
2174
+ };
2175
+ }
2176
+ async removeKeys(kids) {
2177
+ for (const kid of kids) {
2178
+ await this.withTimeout(this.database.collection(this.path).doc(kid).delete());
2179
+ }
2180
+ }
2181
+ async withTimeout(operation) {
2182
+ const timer = new Promise((_, reject) => setTimeout(() => {
2183
+ reject(new Error(`Operation timed out after ${this.timeout}ms`));
2184
+ }, this.timeout));
2185
+ return Promise.race([operation, timer]);
2186
+ }
2187
+ async verify() {
2188
+ await this.withTimeout(this.database.collection(this.path).limit(1).get());
2189
+ }
2190
+ }
2191
+
2192
+ class KeyStores {
2193
+ static async fromConfig(config, options) {
2194
+ var _a;
2195
+ const { logger, database } = options != null ? options : {};
2196
+ const ks = config.getOptionalConfig("auth.keyStore");
2197
+ const provider = (_a = ks == null ? void 0 : ks.getOptionalString("provider")) != null ? _a : "database";
2198
+ logger == null ? void 0 : logger.info(`Configuring "${provider}" as KeyStore provider`);
2199
+ if (provider === "database") {
2200
+ if (!database) {
2201
+ throw new Error("This KeyStore provider requires a database");
2202
+ }
2203
+ return await DatabaseKeyStore.create({
2204
+ database: await database.getClient()
2205
+ });
2206
+ }
2207
+ if (provider === "memory") {
2208
+ return new MemoryKeyStore();
2209
+ }
2210
+ if (provider === "firestore") {
2211
+ const settings = ks == null ? void 0 : ks.getConfig(provider);
2212
+ const keyStore = await FirestoreKeyStore.create(lodash.pickBy({
2213
+ projectId: settings == null ? void 0 : settings.getOptionalString("projectId"),
2214
+ keyFilename: settings == null ? void 0 : settings.getOptionalString("keyFilename"),
2215
+ host: settings == null ? void 0 : settings.getOptionalString("host"),
2216
+ port: settings == null ? void 0 : settings.getOptionalNumber("port"),
2217
+ ssl: settings == null ? void 0 : settings.getOptionalBoolean("ssl"),
2218
+ path: settings == null ? void 0 : settings.getOptionalString("path"),
2219
+ timeout: settings == null ? void 0 : settings.getOptionalNumber("timeout")
2220
+ }, (value) => value !== void 0));
2221
+ await FirestoreKeyStore.verifyConnection(keyStore, logger);
2222
+ return keyStore;
2223
+ }
2224
+ throw new Error(`Unknown KeyStore provider: ${provider}`);
2225
+ }
2226
+ }
2227
+
2228
+ const OAUTH2_PROXY_JWT_HEADER = "X-OAUTH2-PROXY-ID-TOKEN";
2229
+ class Oauth2ProxyAuthProvider {
2230
+ constructor(options) {
2231
+ this.catalogIdentityClient = options.catalogIdentityClient;
2232
+ this.logger = options.logger;
2233
+ this.tokenIssuer = options.tokenIssuer;
2234
+ this.signInResolver = options.signInResolver;
2235
+ this.authHandler = options.authHandler;
2236
+ }
2237
+ frameHandler() {
2238
+ return Promise.resolve(void 0);
2239
+ }
2240
+ async refresh(req, res) {
2241
+ try {
2242
+ const result = this.getResult(req);
2243
+ const response = await this.handleResult(result);
2244
+ res.json(response);
2245
+ } catch (e) {
2246
+ this.logger.error(`Exception occurred during ${OAUTH2_PROXY_JWT_HEADER} refresh`, e);
2247
+ res.status(401);
2248
+ res.end();
2249
+ }
2250
+ }
2251
+ start() {
2252
+ return Promise.resolve(void 0);
2253
+ }
2254
+ async handleResult(result) {
2255
+ const ctx = {
2256
+ logger: this.logger,
2257
+ tokenIssuer: this.tokenIssuer,
2258
+ catalogIdentityClient: this.catalogIdentityClient
2259
+ };
2260
+ const { profile } = await this.authHandler(result, ctx);
2261
+ const backstageSignInResult = await this.signInResolver({
2262
+ result,
2263
+ profile
2264
+ }, ctx);
2265
+ return {
2266
+ providerInfo: {
2267
+ accessToken: result.accessToken
2268
+ },
2269
+ backstageIdentity: prepareBackstageIdentityResponse(backstageSignInResult),
2270
+ profile
2271
+ };
2272
+ }
2273
+ getResult(req) {
2274
+ const authHeader = req.header(OAUTH2_PROXY_JWT_HEADER);
2275
+ const jwt = IdentityClient.getBearerToken(authHeader);
2276
+ if (!jwt) {
2277
+ throw new errors.AuthenticationError(`Missing or in incorrect format - Oauth2Proxy OIDC header: ${OAUTH2_PROXY_JWT_HEADER}`);
2278
+ }
2279
+ const decodedJWT = jose.JWT.decode(jwt);
2280
+ return {
2281
+ fullProfile: decodedJWT,
2282
+ accessToken: jwt
2283
+ };
2284
+ }
2285
+ }
2286
+ const createOauth2ProxyProvider = (options) => ({ catalogApi, logger, tokenIssuer }) => {
2287
+ const signInResolver = options.signIn.resolver;
2288
+ const authHandler = options.authHandler;
2289
+ const catalogIdentityClient = new CatalogIdentityClient({
2290
+ catalogApi,
2291
+ tokenIssuer
2292
+ });
2293
+ return new Oauth2ProxyAuthProvider({
2294
+ logger,
2295
+ signInResolver,
2296
+ authHandler,
2297
+ tokenIssuer,
2298
+ catalogIdentityClient
2299
+ });
2300
+ };
2301
+
2302
+ class OidcAuthProvider {
2303
+ constructor(options) {
2304
+ this.implementation = this.setupStrategy(options);
2305
+ this.scope = options.scope;
2306
+ this.prompt = options.prompt;
2307
+ this.signInResolver = options.signInResolver;
2308
+ this.authHandler = options.authHandler;
2309
+ this.tokenIssuer = options.tokenIssuer;
2310
+ this.catalogIdentityClient = options.catalogIdentityClient;
2311
+ this.logger = options.logger;
2312
+ }
2313
+ async start(req) {
2314
+ const { strategy } = await this.implementation;
2315
+ const options = {
2316
+ scope: req.scope || this.scope || "openid profile email",
2317
+ state: encodeState(req.state)
2318
+ };
2319
+ const prompt = this.prompt || "none";
2320
+ if (prompt !== "auto") {
2321
+ options.prompt = prompt;
2322
+ }
2323
+ return await executeRedirectStrategy(req, strategy, options);
2324
+ }
2325
+ async handler(req) {
2326
+ const { strategy } = await this.implementation;
2327
+ const { result, privateInfo } = await executeFrameHandlerStrategy(req, strategy);
2328
+ return {
2329
+ response: await this.handleResult(result),
2330
+ refreshToken: privateInfo.refreshToken
2331
+ };
2332
+ }
2333
+ async refresh(req) {
2334
+ const { client } = await this.implementation;
2335
+ const tokenset = await client.refresh(req.refreshToken);
2336
+ if (!tokenset.access_token) {
2337
+ throw new Error("Refresh failed");
2338
+ }
2339
+ const userinfo = await client.userinfo(tokenset.access_token);
2340
+ return {
2341
+ response: await this.handleResult({ tokenset, userinfo }),
2342
+ refreshToken: tokenset.refresh_token
2343
+ };
2344
+ }
2345
+ async setupStrategy(options) {
2346
+ const issuer = await openidClient.Issuer.discover(options.metadataUrl);
2347
+ const client = new issuer.Client({
2348
+ access_type: "offline",
2349
+ client_id: options.clientId,
2350
+ client_secret: options.clientSecret,
2351
+ redirect_uris: [options.callbackUrl],
2352
+ response_types: ["code"],
2353
+ id_token_signed_response_alg: options.tokenSignedResponseAlg || "RS256",
2354
+ scope: options.scope || ""
2355
+ });
2356
+ const strategy = new openidClient.Strategy({
2357
+ client,
2358
+ passReqToCallback: false
2359
+ }, (tokenset, userinfo, done) => {
2360
+ if (typeof done !== "function") {
2361
+ throw new Error("OIDC IdP must provide a userinfo_endpoint in the metadata response");
2362
+ }
2363
+ done(void 0, { tokenset, userinfo }, {
2364
+ refreshToken: tokenset.refresh_token
2365
+ });
2366
+ });
2367
+ strategy.error = console.error;
2368
+ return { strategy, client };
2369
+ }
2370
+ async handleResult(result) {
2371
+ const context = {
2372
+ logger: this.logger,
2373
+ catalogIdentityClient: this.catalogIdentityClient,
2374
+ tokenIssuer: this.tokenIssuer
2375
+ };
2376
+ const { profile } = await this.authHandler(result, context);
2377
+ const response = {
2378
+ providerInfo: {
2379
+ idToken: result.tokenset.id_token,
2380
+ accessToken: result.tokenset.access_token,
2381
+ scope: result.tokenset.scope,
2382
+ expiresInSeconds: result.tokenset.expires_in
2383
+ },
2384
+ profile
2385
+ };
2386
+ if (this.signInResolver) {
2387
+ response.backstageIdentity = await this.signInResolver({
2388
+ result,
2389
+ profile
2390
+ }, context);
2391
+ }
2392
+ return response;
2393
+ }
2394
+ }
2395
+ const oAuth2DefaultSignInResolver = async (info, ctx) => {
2396
+ const { profile } = info;
2397
+ if (!profile.email) {
2398
+ throw new Error("Profile contained no email");
2399
+ }
2400
+ const userId = profile.email.split("@")[0];
2401
+ const token = await ctx.tokenIssuer.issueToken({
2402
+ claims: {
2403
+ sub: `user:default/${userId}`,
2404
+ ent: [`user:default/${userId}`]
2405
+ }
2406
+ });
2407
+ return { id: userId, token };
2408
+ };
2409
+ const createOidcProvider = (options) => {
1962
2410
  return ({
1963
2411
  providerId,
1964
2412
  globalConfig,
@@ -2076,7 +2524,12 @@ class OktaAuthProvider {
2076
2524
  };
2077
2525
  }
2078
2526
  async handleResult(result) {
2079
- const { profile } = await this._authHandler(result);
2527
+ const context = {
2528
+ logger: this._logger,
2529
+ catalogIdentityClient: this._catalogIdentityClient,
2530
+ tokenIssuer: this._tokenIssuer
2531
+ };
2532
+ const { profile } = await this._authHandler(result, context);
2080
2533
  const response = {
2081
2534
  providerInfo: {
2082
2535
  idToken: result.params.id_token,
@@ -2090,11 +2543,7 @@ class OktaAuthProvider {
2090
2543
  response.backstageIdentity = await this._signInResolver({
2091
2544
  result,
2092
2545
  profile
2093
- }, {
2094
- tokenIssuer: this._tokenIssuer,
2095
- catalogIdentityClient: this._catalogIdentityClient,
2096
- logger: this._logger
2097
- });
2546
+ }, context);
2098
2547
  }
2099
2548
  return response;
2100
2549
  }
@@ -2117,245 +2566,14 @@ const oktaDefaultSignInResolver = async (info, ctx) => {
2117
2566
  const { profile } = info;
2118
2567
  if (!profile.email) {
2119
2568
  throw new Error("Okta profile contained no email");
2120
- }
2121
- const userId = profile.email.split("@")[0];
2122
- const token = await ctx.tokenIssuer.issueToken({
2123
- claims: { sub: userId, ent: [`user:default/${userId}`] }
2124
- });
2125
- return { id: userId, token };
2126
- };
2127
- const createOktaProvider = (_options) => {
2128
- return ({
2129
- providerId,
2130
- globalConfig,
2131
- config,
2132
- tokenIssuer,
2133
- catalogApi,
2134
- logger
2135
- }) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
2136
- var _a, _b;
2137
- const clientId = envConfig.getString("clientId");
2138
- const clientSecret = envConfig.getString("clientSecret");
2139
- const audience = envConfig.getString("audience");
2140
- const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
2141
- if (!audience.startsWith("https://")) {
2142
- throw new Error("URL for 'audience' must start with 'https://'.");
2143
- }
2144
- const catalogIdentityClient = new CatalogIdentityClient({
2145
- catalogApi,
2146
- tokenIssuer
2147
- });
2148
- const authHandler = (_options == null ? void 0 : _options.authHandler) ? _options.authHandler : async ({ fullProfile, params }) => ({
2149
- profile: makeProfileInfo(fullProfile, params.id_token)
2150
- });
2151
- const signInResolverFn = (_b = (_a = _options == null ? void 0 : _options.signIn) == null ? void 0 : _a.resolver) != null ? _b : oktaDefaultSignInResolver;
2152
- const signInResolver = (info) => signInResolverFn(info, {
2153
- catalogIdentityClient,
2154
- tokenIssuer,
2155
- logger
2156
- });
2157
- const provider = new OktaAuthProvider({
2158
- audience,
2159
- clientId,
2160
- clientSecret,
2161
- callbackUrl,
2162
- authHandler,
2163
- signInResolver,
2164
- tokenIssuer,
2165
- catalogIdentityClient,
2166
- logger
2167
- });
2168
- return OAuthAdapter.fromConfig(globalConfig, provider, {
2169
- disableRefresh: false,
2170
- providerId,
2171
- tokenIssuer
2172
- });
2173
- });
2174
- };
2175
-
2176
- class OneLoginProvider {
2177
- constructor(options) {
2178
- this.signInResolver = options.signInResolver;
2179
- this.authHandler = options.authHandler;
2180
- this.tokenIssuer = options.tokenIssuer;
2181
- this.catalogIdentityClient = options.catalogIdentityClient;
2182
- this.logger = options.logger;
2183
- this._strategy = new passportOneloginOauth.Strategy({
2184
- issuer: options.issuer,
2185
- clientID: options.clientId,
2186
- clientSecret: options.clientSecret,
2187
- callbackURL: options.callbackUrl,
2188
- passReqToCallback: false
2189
- }, (accessToken, refreshToken, params, fullProfile, done) => {
2190
- done(void 0, {
2191
- accessToken,
2192
- refreshToken,
2193
- params,
2194
- fullProfile
2195
- }, {
2196
- refreshToken
2197
- });
2198
- });
2199
- }
2200
- async start(req) {
2201
- return await executeRedirectStrategy(req, this._strategy, {
2202
- accessType: "offline",
2203
- prompt: "consent",
2204
- scope: "openid",
2205
- state: encodeState(req.state)
2206
- });
2207
- }
2208
- async handler(req) {
2209
- const { result, privateInfo } = await executeFrameHandlerStrategy(req, this._strategy);
2210
- return {
2211
- response: await this.handleResult(result),
2212
- refreshToken: privateInfo.refreshToken
2213
- };
2214
- }
2215
- async refresh(req) {
2216
- const { accessToken, refreshToken, params } = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
2217
- const fullProfile = await executeFetchUserProfileStrategy(this._strategy, accessToken);
2218
- return {
2219
- response: await this.handleResult({
2220
- fullProfile,
2221
- params,
2222
- accessToken
2223
- }),
2224
- refreshToken
2225
- };
2226
- }
2227
- async handleResult(result) {
2228
- const { profile } = await this.authHandler(result);
2229
- const response = {
2230
- providerInfo: {
2231
- idToken: result.params.id_token,
2232
- accessToken: result.accessToken,
2233
- scope: result.params.scope,
2234
- expiresInSeconds: result.params.expires_in
2235
- },
2236
- profile
2237
- };
2238
- if (this.signInResolver) {
2239
- response.backstageIdentity = await this.signInResolver({
2240
- result,
2241
- profile
2242
- }, {
2243
- tokenIssuer: this.tokenIssuer,
2244
- catalogIdentityClient: this.catalogIdentityClient,
2245
- logger: this.logger
2246
- });
2247
- }
2248
- return response;
2249
- }
2250
- }
2251
- const defaultSignInResolver = async (info) => {
2252
- const { profile } = info;
2253
- if (!profile.email) {
2254
- throw new Error("OIDC profile contained no email");
2255
- }
2256
- const id = profile.email.split("@")[0];
2257
- return { id, token: "" };
2258
- };
2259
- const createOneLoginProvider = (options) => {
2260
- return ({
2261
- providerId,
2262
- globalConfig,
2263
- config,
2264
- tokenIssuer,
2265
- catalogApi,
2266
- logger
2267
- }) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
2268
- var _a, _b;
2269
- const clientId = envConfig.getString("clientId");
2270
- const clientSecret = envConfig.getString("clientSecret");
2271
- const issuer = envConfig.getString("issuer");
2272
- const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
2273
- const catalogIdentityClient = new CatalogIdentityClient({
2274
- catalogApi,
2275
- tokenIssuer
2276
- });
2277
- const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ fullProfile, params }) => ({
2278
- profile: makeProfileInfo(fullProfile, params.id_token)
2279
- });
2280
- const signInResolver = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : defaultSignInResolver;
2281
- const provider = new OneLoginProvider({
2282
- clientId,
2283
- clientSecret,
2284
- callbackUrl,
2285
- issuer,
2286
- authHandler,
2287
- signInResolver,
2288
- tokenIssuer,
2289
- catalogIdentityClient,
2290
- logger
2291
- });
2292
- return OAuthAdapter.fromConfig(globalConfig, provider, {
2293
- disableRefresh: false,
2294
- providerId,
2295
- tokenIssuer
2296
- });
2297
- });
2298
- };
2299
-
2300
- class SamlAuthProvider {
2301
- constructor(options) {
2302
- this.appUrl = options.appUrl;
2303
- this.signInResolver = options.signInResolver;
2304
- this.authHandler = options.authHandler;
2305
- this.tokenIssuer = options.tokenIssuer;
2306
- this.catalogIdentityClient = options.catalogIdentityClient;
2307
- this.logger = options.logger;
2308
- this.strategy = new passportSaml.Strategy({ ...options }, (fullProfile, done) => {
2309
- done(void 0, { fullProfile });
2310
- });
2311
- }
2312
- async start(req, res) {
2313
- const { url } = await executeRedirectStrategy(req, this.strategy, {});
2314
- res.redirect(url);
2315
- }
2316
- async frameHandler(req, res) {
2317
- try {
2318
- const { result } = await executeFrameHandlerStrategy(req, this.strategy);
2319
- const { profile } = await this.authHandler(result);
2320
- const response = {
2321
- profile,
2322
- providerInfo: {}
2323
- };
2324
- if (this.signInResolver) {
2325
- const signInResponse = await this.signInResolver({
2326
- result,
2327
- profile
2328
- }, {
2329
- tokenIssuer: this.tokenIssuer,
2330
- catalogIdentityClient: this.catalogIdentityClient,
2331
- logger: this.logger
2332
- });
2333
- response.backstageIdentity = prepareBackstageIdentityResponse(signInResponse);
2334
- }
2335
- return postMessageResponse(res, this.appUrl, {
2336
- type: "authorization_response",
2337
- response
2338
- });
2339
- } catch (error) {
2340
- const { name, message } = errors.isError(error) ? error : new Error("Encountered invalid error");
2341
- return postMessageResponse(res, this.appUrl, {
2342
- type: "authorization_response",
2343
- error: { name, message }
2344
- });
2345
- }
2346
- }
2347
- async logout(_req, res) {
2348
- res.end();
2349
- }
2350
- }
2351
- const samlDefaultSignInResolver = async (info, ctx) => {
2352
- const id = info.result.fullProfile.nameID;
2569
+ }
2570
+ const userId = profile.email.split("@")[0];
2353
2571
  const token = await ctx.tokenIssuer.issueToken({
2354
- claims: { sub: id }
2572
+ claims: { sub: `user:default/${userId}`, ent: [`user:default/${userId}`] }
2355
2573
  });
2356
- return { id, token };
2574
+ return { id: userId, token };
2357
2575
  };
2358
- const createSamlProvider = (options) => {
2576
+ const createOktaProvider = (_options) => {
2359
2577
  return ({
2360
2578
  providerId,
2361
2579
  globalConfig,
@@ -2363,494 +2581,388 @@ const createSamlProvider = (options) => {
2363
2581
  tokenIssuer,
2364
2582
  catalogApi,
2365
2583
  logger
2366
- }) => {
2584
+ }) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
2367
2585
  var _a, _b;
2586
+ const clientId = envConfig.getString("clientId");
2587
+ const clientSecret = envConfig.getString("clientSecret");
2588
+ const audience = envConfig.getString("audience");
2589
+ const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
2590
+ if (!audience.startsWith("https://")) {
2591
+ throw new Error("URL for 'audience' must start with 'https://'.");
2592
+ }
2368
2593
  const catalogIdentityClient = new CatalogIdentityClient({
2369
2594
  catalogApi,
2370
2595
  tokenIssuer
2371
2596
  });
2372
- const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ fullProfile }) => ({
2373
- profile: {
2374
- email: fullProfile.email,
2375
- displayName: fullProfile.displayName
2376
- }
2597
+ const authHandler = (_options == null ? void 0 : _options.authHandler) ? _options.authHandler : async ({ fullProfile, params }) => ({
2598
+ profile: makeProfileInfo(fullProfile, params.id_token)
2377
2599
  });
2378
- const signInResolverFn = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : samlDefaultSignInResolver;
2600
+ const signInResolverFn = (_b = (_a = _options == null ? void 0 : _options.signIn) == null ? void 0 : _a.resolver) != null ? _b : oktaDefaultSignInResolver;
2379
2601
  const signInResolver = (info) => signInResolverFn(info, {
2380
2602
  catalogIdentityClient,
2381
2603
  tokenIssuer,
2382
2604
  logger
2383
2605
  });
2384
- return new SamlAuthProvider({
2385
- callbackUrl: `${globalConfig.baseUrl}/${providerId}/handler/frame`,
2386
- entryPoint: config.getString("entryPoint"),
2387
- logoutUrl: config.getOptionalString("logoutUrl"),
2388
- audience: config.getOptionalString("audience"),
2389
- issuer: config.getString("issuer"),
2390
- cert: config.getString("cert"),
2391
- privateKey: config.getOptionalString("privateKey"),
2392
- authnContext: config.getOptionalStringArray("authnContext"),
2393
- identifierFormat: config.getOptionalString("identifierFormat"),
2394
- decryptionPvk: config.getOptionalString("decryptionPvk"),
2395
- signatureAlgorithm: config.getOptionalString("signatureAlgorithm"),
2396
- digestAlgorithm: config.getOptionalString("digestAlgorithm"),
2397
- acceptedClockSkewMs: config.getOptionalNumber("acceptedClockSkewMs"),
2398
- tokenIssuer,
2399
- appUrl: globalConfig.appUrl,
2400
- authHandler,
2401
- signInResolver,
2402
- logger,
2403
- catalogIdentityClient
2404
- });
2405
- };
2406
- };
2407
-
2408
- const IAP_JWT_HEADER = "x-goog-iap-jwt-assertion";
2409
-
2410
- function createTokenValidator(audience, mockClient) {
2411
- const client = mockClient != null ? mockClient : new googleAuthLibrary.OAuth2Client();
2412
- return async function tokenValidator(token) {
2413
- const response = await client.getIapPublicKeys();
2414
- const ticket = await client.verifySignedJwtWithCertsAsync(token, response.pubkeys, audience, ["https://cloud.google.com/iap"]);
2415
- const payload = ticket.getPayload();
2416
- if (!payload) {
2417
- throw new TypeError("Token had no payload");
2418
- }
2419
- return payload;
2420
- };
2421
- }
2422
- async function parseRequestToken(jwtToken, tokenValidator) {
2423
- if (typeof jwtToken !== "string" || !jwtToken) {
2424
- throw new errors.AuthenticationError(`Missing Google IAP header: ${IAP_JWT_HEADER}`);
2425
- }
2426
- let payload;
2427
- try {
2428
- payload = await tokenValidator(jwtToken);
2429
- } catch (e) {
2430
- throw new errors.AuthenticationError(`Google IAP token verification failed, ${e}`);
2431
- }
2432
- if (!payload.sub || !payload.email) {
2433
- throw new errors.AuthenticationError("Google IAP token payload is missing sub and/or email claim");
2434
- }
2435
- return {
2436
- iapToken: {
2437
- ...payload,
2438
- sub: payload.sub,
2439
- email: payload.email
2440
- }
2441
- };
2442
- }
2443
- const defaultAuthHandler = async ({
2444
- iapToken
2445
- }) => ({ profile: { email: iapToken.email } });
2446
-
2447
- class GcpIapProvider {
2448
- constructor(options) {
2449
- this.authHandler = options.authHandler;
2450
- this.signInResolver = options.signInResolver;
2451
- this.tokenValidator = options.tokenValidator;
2452
- this.tokenIssuer = options.tokenIssuer;
2453
- this.catalogIdentityClient = options.catalogIdentityClient;
2454
- this.logger = options.logger;
2455
- }
2456
- async start() {
2457
- }
2458
- async frameHandler() {
2459
- }
2460
- async refresh(req, res) {
2461
- const result = await parseRequestToken(req.header(IAP_JWT_HEADER), this.tokenValidator);
2462
- const { profile } = await this.authHandler(result);
2463
- const backstageIdentity = await this.signInResolver({ profile, result }, {
2464
- tokenIssuer: this.tokenIssuer,
2465
- catalogIdentityClient: this.catalogIdentityClient,
2466
- logger: this.logger
2467
- });
2468
- const response = {
2469
- providerInfo: { iapToken: result.iapToken },
2470
- profile,
2471
- backstageIdentity: prepareBackstageIdentityResponse(backstageIdentity)
2472
- };
2473
- res.json(response);
2474
- }
2475
- }
2476
- function createGcpIapProvider(options) {
2477
- return ({ config, tokenIssuer, catalogApi, logger }) => {
2478
- var _a;
2479
- const audience = config.getString("audience");
2480
- const authHandler = (_a = options.authHandler) != null ? _a : defaultAuthHandler;
2481
- const signInResolver = options.signIn.resolver;
2482
- const tokenValidator = createTokenValidator(audience);
2483
- const catalogIdentityClient = new CatalogIdentityClient({
2484
- catalogApi,
2485
- tokenIssuer
2486
- });
2487
- return new GcpIapProvider({
2488
- authHandler,
2489
- signInResolver,
2490
- tokenValidator,
2491
- tokenIssuer,
2492
- catalogIdentityClient,
2493
- logger
2494
- });
2495
- };
2496
- }
2497
-
2498
- const factories = {
2499
- google: createGoogleProvider(),
2500
- github: createGithubProvider(),
2501
- gitlab: createGitlabProvider(),
2502
- saml: createSamlProvider(),
2503
- okta: createOktaProvider(),
2504
- auth0: createAuth0Provider(),
2505
- microsoft: createMicrosoftProvider(),
2506
- oauth2: createOAuth2Provider(),
2507
- oidc: createOidcProvider(),
2508
- onelogin: createOneLoginProvider(),
2509
- awsalb: createAwsAlbProvider(),
2510
- bitbucket: createBitbucketProvider(),
2511
- atlassian: createAtlassianProvider()
2512
- };
2513
-
2514
- function createOidcRouter(options) {
2515
- const { baseUrl, tokenIssuer } = options;
2516
- const router = Router__default["default"]();
2517
- const config = {
2518
- issuer: baseUrl,
2519
- token_endpoint: `${baseUrl}/v1/token`,
2520
- userinfo_endpoint: `${baseUrl}/v1/userinfo`,
2521
- jwks_uri: `${baseUrl}/.well-known/jwks.json`,
2522
- response_types_supported: ["id_token"],
2523
- subject_types_supported: ["public"],
2524
- id_token_signing_alg_values_supported: ["RS256"],
2525
- scopes_supported: ["openid"],
2526
- token_endpoint_auth_methods_supported: [],
2527
- claims_supported: ["sub"],
2528
- grant_types_supported: []
2529
- };
2530
- router.get("/.well-known/openid-configuration", (_req, res) => {
2531
- res.json(config);
2532
- });
2533
- router.get("/.well-known/jwks.json", async (_req, res) => {
2534
- const { keys } = await tokenIssuer.listPublicKeys();
2535
- res.json({ keys });
2536
- });
2537
- router.get("/v1/token", (_req, res) => {
2538
- res.status(501).send("Not Implemented");
2539
- });
2540
- router.get("/v1/userinfo", (_req, res) => {
2541
- res.status(501).send("Not Implemented");
2542
- });
2543
- return router;
2544
- }
2545
-
2546
- const CLOCK_MARGIN_S = 10;
2547
- class IdentityClient {
2548
- constructor(options) {
2549
- this.discovery = options.discovery;
2550
- this.issuer = options.issuer;
2551
- this.keyStore = new jose.JWKS.KeyStore();
2552
- this.keyStoreUpdated = 0;
2553
- }
2554
- async authenticate(token) {
2555
- var _a;
2556
- if (!token) {
2557
- throw new errors.AuthenticationError("No token specified");
2558
- }
2559
- const key = await this.getKey(token);
2560
- if (!key) {
2561
- throw new errors.AuthenticationError("No signing key matching token found");
2562
- }
2563
- const decoded = jose.JWT.IdToken.verify(token, key, {
2564
- algorithms: ["ES256"],
2565
- audience: "backstage",
2566
- issuer: this.issuer
2567
- });
2568
- if (!decoded.sub) {
2569
- throw new errors.AuthenticationError("No user sub found in token");
2570
- }
2571
- const user = {
2572
- id: decoded.sub,
2573
- token,
2574
- identity: {
2575
- type: "user",
2576
- userEntityRef: decoded.sub,
2577
- ownershipEntityRefs: (_a = decoded.ent) != null ? _a : []
2578
- }
2579
- };
2580
- return user;
2581
- }
2582
- static getBearerToken(authorizationHeader) {
2583
- if (typeof authorizationHeader !== "string") {
2584
- return void 0;
2585
- }
2586
- const matches = authorizationHeader.match(/Bearer\s+(\S+)/i);
2587
- return matches == null ? void 0 : matches[1];
2588
- }
2589
- async getKey(rawJwtToken) {
2590
- const { header, payload } = jose.JWT.decode(rawJwtToken, {
2591
- complete: true
2592
- });
2593
- const keyStoreHasKey = !!this.keyStore.get({ kid: header.kid });
2594
- const issuedAfterLastRefresh = (payload == null ? void 0 : payload.iat) && payload.iat > this.keyStoreUpdated - CLOCK_MARGIN_S;
2595
- if (!keyStoreHasKey && issuedAfterLastRefresh) {
2596
- await this.refreshKeyStore();
2597
- }
2598
- return this.keyStore.get({ kid: header.kid });
2599
- }
2600
- async listPublicKeys() {
2601
- const url = `${await this.discovery.getBaseUrl("auth")}/.well-known/jwks.json`;
2602
- const response = await fetch__default["default"](url);
2603
- if (!response.ok) {
2604
- const payload = await response.text();
2605
- const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
2606
- throw new Error(message);
2607
- }
2608
- const publicKeys = await response.json();
2609
- return publicKeys;
2610
- }
2611
- async refreshKeyStore() {
2612
- const now = Date.now() / 1e3;
2613
- const publicKeys = await this.listPublicKeys();
2614
- this.keyStore = jose.JWKS.asKeyStore({
2615
- keys: publicKeys.keys.map((key) => key)
2606
+ const provider = new OktaAuthProvider({
2607
+ audience,
2608
+ clientId,
2609
+ clientSecret,
2610
+ callbackUrl,
2611
+ authHandler,
2612
+ signInResolver,
2613
+ tokenIssuer,
2614
+ catalogIdentityClient,
2615
+ logger
2616
2616
  });
2617
- this.keyStoreUpdated = now;
2618
- }
2619
- }
2617
+ return OAuthAdapter.fromConfig(globalConfig, provider, {
2618
+ disableRefresh: false,
2619
+ providerId,
2620
+ tokenIssuer
2621
+ });
2622
+ });
2623
+ };
2620
2624
 
2621
- const MS_IN_S = 1e3;
2622
- class TokenFactory {
2625
+ class OneLoginProvider {
2623
2626
  constructor(options) {
2624
- this.issuer = options.issuer;
2627
+ this.signInResolver = options.signInResolver;
2628
+ this.authHandler = options.authHandler;
2629
+ this.tokenIssuer = options.tokenIssuer;
2630
+ this.catalogIdentityClient = options.catalogIdentityClient;
2625
2631
  this.logger = options.logger;
2626
- this.keyStore = options.keyStore;
2627
- this.keyDurationSeconds = options.keyDurationSeconds;
2632
+ this._strategy = new passportOneloginOauth.Strategy({
2633
+ issuer: options.issuer,
2634
+ clientID: options.clientId,
2635
+ clientSecret: options.clientSecret,
2636
+ callbackURL: options.callbackUrl,
2637
+ passReqToCallback: false
2638
+ }, (accessToken, refreshToken, params, fullProfile, done) => {
2639
+ done(void 0, {
2640
+ accessToken,
2641
+ refreshToken,
2642
+ params,
2643
+ fullProfile
2644
+ }, {
2645
+ refreshToken
2646
+ });
2647
+ });
2628
2648
  }
2629
- async issueToken(params) {
2630
- const key = await this.getKey();
2631
- const iss = this.issuer;
2632
- const sub = params.claims.sub;
2633
- const ent = params.claims.ent;
2634
- const aud = "backstage";
2635
- const iat = Math.floor(Date.now() / MS_IN_S);
2636
- const exp = iat + this.keyDurationSeconds;
2637
- this.logger.info(`Issuing token for ${sub}, with entities ${ent != null ? ent : []}`);
2638
- return jose.JWS.sign({ iss, sub, aud, iat, exp, ent }, key, {
2639
- alg: key.alg,
2640
- kid: key.kid
2649
+ async start(req) {
2650
+ return await executeRedirectStrategy(req, this._strategy, {
2651
+ accessType: "offline",
2652
+ prompt: "consent",
2653
+ scope: "openid",
2654
+ state: encodeState(req.state)
2641
2655
  });
2642
2656
  }
2643
- async listPublicKeys() {
2644
- const { items: keys } = await this.keyStore.listKeys();
2645
- const validKeys = [];
2646
- const expiredKeys = [];
2647
- for (const key of keys) {
2648
- const expireAt = luxon.DateTime.fromJSDate(key.createdAt).plus({
2649
- seconds: 3 * this.keyDurationSeconds
2650
- });
2651
- if (expireAt < luxon.DateTime.local()) {
2652
- expiredKeys.push(key);
2653
- } else {
2654
- validKeys.push(key);
2655
- }
2656
- }
2657
- if (expiredKeys.length > 0) {
2658
- const kids = expiredKeys.map(({ key }) => key.kid);
2659
- this.logger.info(`Removing expired signing keys, '${kids.join("', '")}'`);
2660
- this.keyStore.removeKeys(kids).catch((error) => {
2661
- this.logger.error(`Failed to remove expired keys, ${error}`);
2662
- });
2663
- }
2664
- return { keys: validKeys.map(({ key }) => key) };
2657
+ async handler(req) {
2658
+ const { result, privateInfo } = await executeFrameHandlerStrategy(req, this._strategy);
2659
+ return {
2660
+ response: await this.handleResult(result),
2661
+ refreshToken: privateInfo.refreshToken
2662
+ };
2665
2663
  }
2666
- async getKey() {
2667
- if (this.privateKeyPromise) {
2668
- if (this.keyExpiry && luxon.DateTime.fromJSDate(this.keyExpiry) > luxon.DateTime.local()) {
2669
- return this.privateKeyPromise;
2670
- }
2671
- this.logger.info(`Signing key has expired, generating new key`);
2672
- delete this.privateKeyPromise;
2673
- }
2674
- this.keyExpiry = luxon.DateTime.utc().plus({
2675
- seconds: this.keyDurationSeconds
2676
- }).toJSDate();
2677
- const promise = (async () => {
2678
- const key = await jose.JWK.generate("EC", "P-256", {
2679
- use: "sig",
2680
- kid: uuid.v4(),
2681
- alg: "ES256"
2682
- });
2683
- this.logger.info(`Created new signing key ${key.kid}`);
2684
- await this.keyStore.addKey(key.toJWK(false));
2685
- return key;
2686
- })();
2687
- this.privateKeyPromise = promise;
2688
- try {
2689
- await promise;
2690
- } catch (error) {
2691
- this.logger.error(`Failed to generate new signing key, ${error}`);
2692
- delete this.keyExpiry;
2693
- delete this.privateKeyPromise;
2664
+ async refresh(req) {
2665
+ const { accessToken, refreshToken, params } = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
2666
+ const fullProfile = await executeFetchUserProfileStrategy(this._strategy, accessToken);
2667
+ return {
2668
+ response: await this.handleResult({
2669
+ fullProfile,
2670
+ params,
2671
+ accessToken
2672
+ }),
2673
+ refreshToken
2674
+ };
2675
+ }
2676
+ async handleResult(result) {
2677
+ const context = {
2678
+ logger: this.logger,
2679
+ catalogIdentityClient: this.catalogIdentityClient,
2680
+ tokenIssuer: this.tokenIssuer
2681
+ };
2682
+ const { profile } = await this.authHandler(result, context);
2683
+ const response = {
2684
+ providerInfo: {
2685
+ idToken: result.params.id_token,
2686
+ accessToken: result.accessToken,
2687
+ scope: result.params.scope,
2688
+ expiresInSeconds: result.params.expires_in
2689
+ },
2690
+ profile
2691
+ };
2692
+ if (this.signInResolver) {
2693
+ response.backstageIdentity = await this.signInResolver({
2694
+ result,
2695
+ profile
2696
+ }, context);
2694
2697
  }
2695
- return promise;
2698
+ return response;
2696
2699
  }
2697
2700
  }
2698
-
2699
- const migrationsDir = backendCommon.resolvePackagePath("@backstage/plugin-auth-backend", "migrations");
2700
- const TABLE = "signing_keys";
2701
- const parseDate = (date) => {
2702
- const parsedDate = typeof date === "string" ? luxon.DateTime.fromSQL(date, { zone: "UTC" }) : luxon.DateTime.fromJSDate(date);
2703
- if (!parsedDate.isValid) {
2704
- throw new Error(`Failed to parse date, reason: ${parsedDate.invalidReason}, explanation: ${parsedDate.invalidExplanation}`);
2701
+ const defaultSignInResolver = async (info) => {
2702
+ const { profile } = info;
2703
+ if (!profile.email) {
2704
+ throw new Error("OIDC profile contained no email");
2705
2705
  }
2706
- return parsedDate.toJSDate();
2706
+ const id = profile.email.split("@")[0];
2707
+ return { id, token: "" };
2707
2708
  };
2708
- class DatabaseKeyStore {
2709
- static async create(options) {
2710
- const { database } = options;
2711
- await database.migrate.latest({
2712
- directory: migrationsDir
2709
+ const createOneLoginProvider = (options) => {
2710
+ return ({
2711
+ providerId,
2712
+ globalConfig,
2713
+ config,
2714
+ tokenIssuer,
2715
+ catalogApi,
2716
+ logger
2717
+ }) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
2718
+ var _a, _b;
2719
+ const clientId = envConfig.getString("clientId");
2720
+ const clientSecret = envConfig.getString("clientSecret");
2721
+ const issuer = envConfig.getString("issuer");
2722
+ const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
2723
+ const catalogIdentityClient = new CatalogIdentityClient({
2724
+ catalogApi,
2725
+ tokenIssuer
2713
2726
  });
2714
- return new DatabaseKeyStore(options);
2715
- }
2727
+ const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ fullProfile, params }) => ({
2728
+ profile: makeProfileInfo(fullProfile, params.id_token)
2729
+ });
2730
+ const signInResolver = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : defaultSignInResolver;
2731
+ const provider = new OneLoginProvider({
2732
+ clientId,
2733
+ clientSecret,
2734
+ callbackUrl,
2735
+ issuer,
2736
+ authHandler,
2737
+ signInResolver,
2738
+ tokenIssuer,
2739
+ catalogIdentityClient,
2740
+ logger
2741
+ });
2742
+ return OAuthAdapter.fromConfig(globalConfig, provider, {
2743
+ disableRefresh: false,
2744
+ providerId,
2745
+ tokenIssuer
2746
+ });
2747
+ });
2748
+ };
2749
+
2750
+ class SamlAuthProvider {
2716
2751
  constructor(options) {
2717
- this.database = options.database;
2718
- }
2719
- async addKey(key) {
2720
- await this.database(TABLE).insert({
2721
- kid: key.kid,
2722
- key: JSON.stringify(key)
2752
+ this.appUrl = options.appUrl;
2753
+ this.signInResolver = options.signInResolver;
2754
+ this.authHandler = options.authHandler;
2755
+ this.tokenIssuer = options.tokenIssuer;
2756
+ this.catalogIdentityClient = options.catalogIdentityClient;
2757
+ this.logger = options.logger;
2758
+ this.strategy = new passportSaml.Strategy({ ...options }, (fullProfile, done) => {
2759
+ done(void 0, { fullProfile });
2723
2760
  });
2724
2761
  }
2725
- async listKeys() {
2726
- const rows = await this.database(TABLE).select();
2727
- return {
2728
- items: rows.map((row) => ({
2729
- key: JSON.parse(row.key),
2730
- createdAt: parseDate(row.created_at)
2731
- }))
2732
- };
2762
+ async start(req, res) {
2763
+ const { url } = await executeRedirectStrategy(req, this.strategy, {});
2764
+ res.redirect(url);
2765
+ }
2766
+ async frameHandler(req, res) {
2767
+ try {
2768
+ const context = {
2769
+ logger: this.logger,
2770
+ catalogIdentityClient: this.catalogIdentityClient,
2771
+ tokenIssuer: this.tokenIssuer
2772
+ };
2773
+ const { result } = await executeFrameHandlerStrategy(req, this.strategy);
2774
+ const { profile } = await this.authHandler(result, context);
2775
+ const response = {
2776
+ profile,
2777
+ providerInfo: {}
2778
+ };
2779
+ if (this.signInResolver) {
2780
+ const signInResponse = await this.signInResolver({
2781
+ result,
2782
+ profile
2783
+ }, context);
2784
+ response.backstageIdentity = prepareBackstageIdentityResponse(signInResponse);
2785
+ }
2786
+ return postMessageResponse(res, this.appUrl, {
2787
+ type: "authorization_response",
2788
+ response
2789
+ });
2790
+ } catch (error) {
2791
+ const { name, message } = errors.isError(error) ? error : new Error("Encountered invalid error");
2792
+ return postMessageResponse(res, this.appUrl, {
2793
+ type: "authorization_response",
2794
+ error: { name, message }
2795
+ });
2796
+ }
2733
2797
  }
2734
- async removeKeys(kids) {
2735
- await this.database(TABLE).delete().whereIn("kid", kids);
2798
+ async logout(_req, res) {
2799
+ res.end();
2736
2800
  }
2737
2801
  }
2738
-
2739
- class MemoryKeyStore {
2740
- constructor() {
2741
- this.keys = /* @__PURE__ */ new Map();
2742
- }
2743
- async addKey(key) {
2744
- this.keys.set(key.kid, {
2745
- createdAt: luxon.DateTime.utc().toJSDate(),
2746
- key: JSON.stringify(key)
2802
+ const samlDefaultSignInResolver = async (info, ctx) => {
2803
+ const id = info.result.fullProfile.nameID;
2804
+ const token = await ctx.tokenIssuer.issueToken({
2805
+ claims: { sub: id }
2806
+ });
2807
+ return { id, token };
2808
+ };
2809
+ const createSamlProvider = (options) => {
2810
+ return ({
2811
+ providerId,
2812
+ globalConfig,
2813
+ config,
2814
+ tokenIssuer,
2815
+ catalogApi,
2816
+ logger
2817
+ }) => {
2818
+ var _a, _b;
2819
+ const catalogIdentityClient = new CatalogIdentityClient({
2820
+ catalogApi,
2821
+ tokenIssuer
2747
2822
  });
2748
- }
2749
- async removeKeys(kids) {
2750
- for (const kid of kids) {
2751
- this.keys.delete(kid);
2823
+ const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ fullProfile }) => ({
2824
+ profile: {
2825
+ email: fullProfile.email,
2826
+ displayName: fullProfile.displayName
2827
+ }
2828
+ });
2829
+ const signInResolverFn = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : samlDefaultSignInResolver;
2830
+ const signInResolver = (info) => signInResolverFn(info, {
2831
+ catalogIdentityClient,
2832
+ tokenIssuer,
2833
+ logger
2834
+ });
2835
+ return new SamlAuthProvider({
2836
+ callbackUrl: `${globalConfig.baseUrl}/${providerId}/handler/frame`,
2837
+ entryPoint: config.getString("entryPoint"),
2838
+ logoutUrl: config.getOptionalString("logoutUrl"),
2839
+ audience: config.getOptionalString("audience"),
2840
+ issuer: config.getString("issuer"),
2841
+ cert: config.getString("cert"),
2842
+ privateKey: config.getOptionalString("privateKey"),
2843
+ authnContext: config.getOptionalStringArray("authnContext"),
2844
+ identifierFormat: config.getOptionalString("identifierFormat"),
2845
+ decryptionPvk: config.getOptionalString("decryptionPvk"),
2846
+ signatureAlgorithm: config.getOptionalString("signatureAlgorithm"),
2847
+ digestAlgorithm: config.getOptionalString("digestAlgorithm"),
2848
+ acceptedClockSkewMs: config.getOptionalNumber("acceptedClockSkewMs"),
2849
+ tokenIssuer,
2850
+ appUrl: globalConfig.appUrl,
2851
+ authHandler,
2852
+ signInResolver,
2853
+ logger,
2854
+ catalogIdentityClient
2855
+ });
2856
+ };
2857
+ };
2858
+
2859
+ const IAP_JWT_HEADER = "x-goog-iap-jwt-assertion";
2860
+
2861
+ function createTokenValidator(audience, mockClient) {
2862
+ const client = mockClient != null ? mockClient : new googleAuthLibrary.OAuth2Client();
2863
+ return async function tokenValidator(token) {
2864
+ const response = await client.getIapPublicKeys();
2865
+ const ticket = await client.verifySignedJwtWithCertsAsync(token, response.pubkeys, audience, ["https://cloud.google.com/iap"]);
2866
+ const payload = ticket.getPayload();
2867
+ if (!payload) {
2868
+ throw new TypeError("Token had no payload");
2752
2869
  }
2870
+ return payload;
2871
+ };
2872
+ }
2873
+ async function parseRequestToken(jwtToken, tokenValidator) {
2874
+ if (typeof jwtToken !== "string" || !jwtToken) {
2875
+ throw new errors.AuthenticationError(`Missing Google IAP header: ${IAP_JWT_HEADER}`);
2753
2876
  }
2754
- async listKeys() {
2755
- return {
2756
- items: Array.from(this.keys).map(([, { createdAt, key: keyStr }]) => ({
2757
- createdAt,
2758
- key: JSON.parse(keyStr)
2759
- }))
2760
- };
2877
+ let payload;
2878
+ try {
2879
+ payload = await tokenValidator(jwtToken);
2880
+ } catch (e) {
2881
+ throw new errors.AuthenticationError(`Google IAP token verification failed, ${e}`);
2761
2882
  }
2883
+ if (!payload.sub || !payload.email) {
2884
+ throw new errors.AuthenticationError("Google IAP token payload is missing sub and/or email claim");
2885
+ }
2886
+ return {
2887
+ iapToken: {
2888
+ ...payload,
2889
+ sub: payload.sub,
2890
+ email: payload.email
2891
+ }
2892
+ };
2762
2893
  }
2894
+ const defaultAuthHandler = async ({
2895
+ iapToken
2896
+ }) => ({ profile: { email: iapToken.email } });
2763
2897
 
2764
- const DEFAULT_TIMEOUT_MS = 1e4;
2765
- const DEFAULT_DOCUMENT_PATH = "sessions";
2766
- class FirestoreKeyStore {
2767
- constructor(database, path, timeout) {
2768
- this.database = database;
2769
- this.path = path;
2770
- this.timeout = timeout;
2771
- }
2772
- static async create(settings) {
2773
- const { path, timeout, ...firestoreSettings } = settings != null ? settings : {};
2774
- const database = new firestore.Firestore(firestoreSettings);
2775
- return new FirestoreKeyStore(database, path != null ? path : DEFAULT_DOCUMENT_PATH, timeout != null ? timeout : DEFAULT_TIMEOUT_MS);
2898
+ class GcpIapProvider {
2899
+ constructor(options) {
2900
+ this.authHandler = options.authHandler;
2901
+ this.signInResolver = options.signInResolver;
2902
+ this.tokenValidator = options.tokenValidator;
2903
+ this.tokenIssuer = options.tokenIssuer;
2904
+ this.catalogIdentityClient = options.catalogIdentityClient;
2905
+ this.logger = options.logger;
2776
2906
  }
2777
- static async verifyConnection(keyStore, logger) {
2778
- try {
2779
- await keyStore.verify();
2780
- } catch (error) {
2781
- if (process.env.NODE_ENV !== "development") {
2782
- throw new Error(`Failed to connect to database: ${error.message}`);
2783
- }
2784
- logger == null ? void 0 : logger.warn(`Failed to connect to database: ${error.message}`);
2785
- }
2907
+ async start() {
2786
2908
  }
2787
- async addKey(key) {
2788
- await this.withTimeout(this.database.collection(this.path).doc(key.kid).set({
2789
- kid: key.kid,
2790
- key: JSON.stringify(key)
2791
- }));
2909
+ async frameHandler() {
2792
2910
  }
2793
- async listKeys() {
2794
- const keys = await this.withTimeout(this.database.collection(this.path).get());
2795
- return {
2796
- items: keys.docs.map((key) => ({
2797
- key: key.data(),
2798
- createdAt: key.createTime.toDate()
2799
- }))
2911
+ async refresh(req, res) {
2912
+ const result = await parseRequestToken(req.header(IAP_JWT_HEADER), this.tokenValidator);
2913
+ const context = {
2914
+ logger: this.logger,
2915
+ catalogIdentityClient: this.catalogIdentityClient,
2916
+ tokenIssuer: this.tokenIssuer
2800
2917
  };
2801
- }
2802
- async removeKeys(kids) {
2803
- for (const kid of kids) {
2804
- await this.withTimeout(this.database.collection(this.path).doc(kid).delete());
2805
- }
2806
- }
2807
- async withTimeout(operation) {
2808
- const timer = new Promise((_, reject) => setTimeout(() => {
2809
- reject(new Error(`Operation timed out after ${this.timeout}ms`));
2810
- }, this.timeout));
2811
- return Promise.race([operation, timer]);
2812
- }
2813
- async verify() {
2814
- await this.withTimeout(this.database.collection(this.path).limit(1).get());
2918
+ const { profile } = await this.authHandler(result, context);
2919
+ const backstageIdentity = await this.signInResolver({ profile, result }, context);
2920
+ const response = {
2921
+ providerInfo: { iapToken: result.iapToken },
2922
+ profile,
2923
+ backstageIdentity: prepareBackstageIdentityResponse(backstageIdentity)
2924
+ };
2925
+ res.json(response);
2815
2926
  }
2816
2927
  }
2817
-
2818
- class KeyStores {
2819
- static async fromConfig(config, options) {
2928
+ function createGcpIapProvider(options) {
2929
+ return ({ config, tokenIssuer, catalogApi, logger }) => {
2820
2930
  var _a;
2821
- const { logger, database } = options != null ? options : {};
2822
- const ks = config.getOptionalConfig("auth.keyStore");
2823
- const provider = (_a = ks == null ? void 0 : ks.getOptionalString("provider")) != null ? _a : "database";
2824
- logger == null ? void 0 : logger.info(`Configuring "${provider}" as KeyStore provider`);
2825
- if (provider === "database") {
2826
- if (!database) {
2827
- throw new Error("This KeyStore provider requires a database");
2828
- }
2829
- return await DatabaseKeyStore.create({
2830
- database: await database.getClient()
2831
- });
2832
- }
2833
- if (provider === "memory") {
2834
- return new MemoryKeyStore();
2835
- }
2836
- if (provider === "firestore") {
2837
- const settings = ks == null ? void 0 : ks.getConfig(provider);
2838
- const keyStore = await FirestoreKeyStore.create(lodash.pickBy({
2839
- projectId: settings == null ? void 0 : settings.getOptionalString("projectId"),
2840
- keyFilename: settings == null ? void 0 : settings.getOptionalString("keyFilename"),
2841
- host: settings == null ? void 0 : settings.getOptionalString("host"),
2842
- port: settings == null ? void 0 : settings.getOptionalNumber("port"),
2843
- ssl: settings == null ? void 0 : settings.getOptionalBoolean("ssl"),
2844
- path: settings == null ? void 0 : settings.getOptionalString("path"),
2845
- timeout: settings == null ? void 0 : settings.getOptionalNumber("timeout")
2846
- }, (value) => value !== void 0));
2847
- await FirestoreKeyStore.verifyConnection(keyStore, logger);
2848
- return keyStore;
2849
- }
2850
- throw new Error(`Unknown KeyStore provider: ${provider}`);
2851
- }
2931
+ const audience = config.getString("audience");
2932
+ const authHandler = (_a = options.authHandler) != null ? _a : defaultAuthHandler;
2933
+ const signInResolver = options.signIn.resolver;
2934
+ const tokenValidator = createTokenValidator(audience);
2935
+ const catalogIdentityClient = new CatalogIdentityClient({
2936
+ catalogApi,
2937
+ tokenIssuer
2938
+ });
2939
+ return new GcpIapProvider({
2940
+ authHandler,
2941
+ signInResolver,
2942
+ tokenValidator,
2943
+ tokenIssuer,
2944
+ catalogIdentityClient,
2945
+ logger
2946
+ });
2947
+ };
2852
2948
  }
2853
2949
 
2950
+ const factories = {
2951
+ google: createGoogleProvider(),
2952
+ github: createGithubProvider(),
2953
+ gitlab: createGitlabProvider(),
2954
+ saml: createSamlProvider(),
2955
+ okta: createOktaProvider(),
2956
+ auth0: createAuth0Provider(),
2957
+ microsoft: createMicrosoftProvider(),
2958
+ oauth2: createOAuth2Provider(),
2959
+ oidc: createOidcProvider(),
2960
+ onelogin: createOneLoginProvider(),
2961
+ awsalb: createAwsAlbProvider(),
2962
+ bitbucket: createBitbucketProvider(),
2963
+ atlassian: createAtlassianProvider()
2964
+ };
2965
+
2854
2966
  async function createRouter(options) {
2855
2967
  const { logger, config, discovery, database, providerFactories } = options;
2856
2968
  const router = Router__default["default"]();
@@ -2873,7 +2985,7 @@ async function createRouter(options) {
2873
2985
  secret,
2874
2986
  saveUninitialized: false,
2875
2987
  resave: false,
2876
- cookie: { secure: enforceCookieSSL }
2988
+ cookie: { secure: enforceCookieSSL ? "auto" : false }
2877
2989
  }));
2878
2990
  router.use(passport__default["default"].initialize());
2879
2991
  router.use(passport__default["default"].session());
@@ -2969,6 +3081,7 @@ exports.createGitlabProvider = createGitlabProvider;
2969
3081
  exports.createGoogleProvider = createGoogleProvider;
2970
3082
  exports.createMicrosoftProvider = createMicrosoftProvider;
2971
3083
  exports.createOAuth2Provider = createOAuth2Provider;
3084
+ exports.createOauth2ProxyProvider = createOauth2ProxyProvider;
2972
3085
  exports.createOidcProvider = createOidcProvider;
2973
3086
  exports.createOktaProvider = createOktaProvider;
2974
3087
  exports.createOneLoginProvider = createOneLoginProvider;