@backstage/plugin-auth-backend 0.9.0-next.1 → 0.10.1
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/CHANGELOG.md +139 -0
- package/dist/index.cjs.js +729 -778
- package/dist/index.cjs.js.map +1 -1
- package/migrations/20210326100300_timestamptz.js +2 -2
- package/package.json +24 -21
- package/dist/index.d.ts +0 -1002
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
|
|
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
|
|
153
|
-
|
|
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
|
|
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 {
|
|
@@ -257,7 +257,7 @@ function prepareBackstageIdentityResponse(result) {
|
|
|
257
257
|
const { sub, ent } = parseJwtPayload(result.token);
|
|
258
258
|
const userEntityRef = catalogModel.stringifyEntityRef(catalogModel.parseEntityRef(sub, {
|
|
259
259
|
defaultKind: "user",
|
|
260
|
-
defaultNamespace: catalogModel.
|
|
260
|
+
defaultNamespace: catalogModel.DEFAULT_NAMESPACE
|
|
261
261
|
}));
|
|
262
262
|
return {
|
|
263
263
|
...{
|
|
@@ -317,14 +317,18 @@ class OAuthAdapter {
|
|
|
317
317
|
static fromConfig(config, handlers, options) {
|
|
318
318
|
var _a;
|
|
319
319
|
const { origin: appOrigin } = new url.URL(config.appUrl);
|
|
320
|
-
const
|
|
321
|
-
const
|
|
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
|
+
});
|
|
322
326
|
return new OAuthAdapter(handlers, {
|
|
323
327
|
...options,
|
|
324
328
|
appOrigin,
|
|
325
|
-
cookieDomain,
|
|
326
|
-
cookiePath,
|
|
327
|
-
secure,
|
|
329
|
+
cookieDomain: cookieConfig.domain,
|
|
330
|
+
cookiePath: cookieConfig.path,
|
|
331
|
+
secure: cookieConfig.secure,
|
|
328
332
|
isOriginAllowed: config.isOriginAllowed
|
|
329
333
|
});
|
|
330
334
|
}
|
|
@@ -430,7 +434,7 @@ class OAuthAdapter {
|
|
|
430
434
|
}
|
|
431
435
|
const userEntityRef = catalogModel.stringifyEntityRef(catalogModel.parseEntityRef(identity.id, {
|
|
432
436
|
defaultKind: "user",
|
|
433
|
-
defaultNamespace: catalogModel.
|
|
437
|
+
defaultNamespace: catalogModel.DEFAULT_NAMESPACE
|
|
434
438
|
}));
|
|
435
439
|
const token = await this.options.tokenIssuer.issueToken({
|
|
436
440
|
claims: { sub: userEntityRef }
|
|
@@ -713,7 +717,8 @@ const createAtlassianProvider = (options) => {
|
|
|
713
717
|
const clientId = envConfig.getString("clientId");
|
|
714
718
|
const clientSecret = envConfig.getString("clientSecret");
|
|
715
719
|
const scopes = envConfig.getString("scopes");
|
|
716
|
-
const
|
|
720
|
+
const customCallbackUrl = envConfig.getOptionalString("callbackUrl");
|
|
721
|
+
const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
717
722
|
const catalogIdentityClient = new CatalogIdentityClient({
|
|
718
723
|
catalogApi,
|
|
719
724
|
tokenManager
|
|
@@ -731,9 +736,9 @@ const createAtlassianProvider = (options) => {
|
|
|
731
736
|
tokenIssuer
|
|
732
737
|
});
|
|
733
738
|
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
734
|
-
disableRefresh: true,
|
|
735
739
|
providerId,
|
|
736
|
-
tokenIssuer
|
|
740
|
+
tokenIssuer,
|
|
741
|
+
callbackUrl
|
|
737
742
|
});
|
|
738
743
|
});
|
|
739
744
|
};
|
|
@@ -849,7 +854,8 @@ const createAuth0Provider = (options) => {
|
|
|
849
854
|
const clientId = envConfig.getString("clientId");
|
|
850
855
|
const clientSecret = envConfig.getString("clientSecret");
|
|
851
856
|
const domain = envConfig.getString("domain");
|
|
852
|
-
const
|
|
857
|
+
const customCallbackUrl = envConfig.getOptionalString("callbackUrl");
|
|
858
|
+
const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
853
859
|
const catalogIdentityClient = new CatalogIdentityClient({
|
|
854
860
|
catalogApi,
|
|
855
861
|
tokenManager
|
|
@@ -872,7 +878,8 @@ const createAuth0Provider = (options) => {
|
|
|
872
878
|
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
873
879
|
disableRefresh: true,
|
|
874
880
|
providerId,
|
|
875
|
-
tokenIssuer
|
|
881
|
+
tokenIssuer,
|
|
882
|
+
callbackUrl
|
|
876
883
|
});
|
|
877
884
|
});
|
|
878
885
|
};
|
|
@@ -1123,7 +1130,8 @@ const createBitbucketProvider = (options) => {
|
|
|
1123
1130
|
var _a;
|
|
1124
1131
|
const clientId = envConfig.getString("clientId");
|
|
1125
1132
|
const clientSecret = envConfig.getString("clientSecret");
|
|
1126
|
-
const
|
|
1133
|
+
const customCallbackUrl = envConfig.getOptionalString("callbackUrl");
|
|
1134
|
+
const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
1127
1135
|
const catalogIdentityClient = new CatalogIdentityClient({
|
|
1128
1136
|
catalogApi,
|
|
1129
1137
|
tokenManager
|
|
@@ -1144,7 +1152,8 @@ const createBitbucketProvider = (options) => {
|
|
|
1144
1152
|
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
1145
1153
|
disableRefresh: false,
|
|
1146
1154
|
providerId,
|
|
1147
|
-
tokenIssuer
|
|
1155
|
+
tokenIssuer,
|
|
1156
|
+
callbackUrl
|
|
1148
1157
|
});
|
|
1149
1158
|
});
|
|
1150
1159
|
};
|
|
@@ -1418,7 +1427,8 @@ const createGitlabProvider = (options) => {
|
|
|
1418
1427
|
const clientSecret = envConfig.getString("clientSecret");
|
|
1419
1428
|
const audience = envConfig.getOptionalString("audience");
|
|
1420
1429
|
const baseUrl = audience || "https://gitlab.com";
|
|
1421
|
-
const
|
|
1430
|
+
const customCallbackUrl = envConfig.getOptionalString("callbackUrl");
|
|
1431
|
+
const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
1422
1432
|
const catalogIdentityClient = new CatalogIdentityClient({
|
|
1423
1433
|
catalogApi,
|
|
1424
1434
|
tokenManager
|
|
@@ -1444,7 +1454,8 @@ const createGitlabProvider = (options) => {
|
|
|
1444
1454
|
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
1445
1455
|
disableRefresh: false,
|
|
1446
1456
|
providerId,
|
|
1447
|
-
tokenIssuer
|
|
1457
|
+
tokenIssuer,
|
|
1458
|
+
callbackUrl
|
|
1448
1459
|
});
|
|
1449
1460
|
});
|
|
1450
1461
|
};
|
|
@@ -1573,7 +1584,8 @@ const createGoogleProvider = (options) => {
|
|
|
1573
1584
|
var _a, _b;
|
|
1574
1585
|
const clientId = envConfig.getString("clientId");
|
|
1575
1586
|
const clientSecret = envConfig.getString("clientSecret");
|
|
1576
|
-
const
|
|
1587
|
+
const customCallbackUrl = envConfig.getOptionalString("callbackUrl");
|
|
1588
|
+
const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
1577
1589
|
const catalogIdentityClient = new CatalogIdentityClient({
|
|
1578
1590
|
catalogApi,
|
|
1579
1591
|
tokenManager
|
|
@@ -1600,7 +1612,8 @@ const createGoogleProvider = (options) => {
|
|
|
1600
1612
|
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
1601
1613
|
disableRefresh: false,
|
|
1602
1614
|
providerId,
|
|
1603
|
-
tokenIssuer
|
|
1615
|
+
tokenIssuer,
|
|
1616
|
+
callbackUrl
|
|
1604
1617
|
});
|
|
1605
1618
|
});
|
|
1606
1619
|
};
|
|
@@ -1732,7 +1745,8 @@ const createMicrosoftProvider = (options) => {
|
|
|
1732
1745
|
const clientId = envConfig.getString("clientId");
|
|
1733
1746
|
const clientSecret = envConfig.getString("clientSecret");
|
|
1734
1747
|
const tenantId = envConfig.getString("tenantId");
|
|
1735
|
-
const
|
|
1748
|
+
const customCallbackUrl = envConfig.getOptionalString("callbackUrl");
|
|
1749
|
+
const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
1736
1750
|
const authorizationUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`;
|
|
1737
1751
|
const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
|
|
1738
1752
|
const catalogIdentityClient = new CatalogIdentityClient({
|
|
@@ -1763,7 +1777,8 @@ const createMicrosoftProvider = (options) => {
|
|
|
1763
1777
|
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
1764
1778
|
disableRefresh: false,
|
|
1765
1779
|
providerId,
|
|
1766
|
-
tokenIssuer
|
|
1780
|
+
tokenIssuer,
|
|
1781
|
+
callbackUrl
|
|
1767
1782
|
});
|
|
1768
1783
|
});
|
|
1769
1784
|
};
|
|
@@ -1877,7 +1892,8 @@ const createOAuth2Provider = (options) => {
|
|
|
1877
1892
|
var _a, _b, _c;
|
|
1878
1893
|
const clientId = envConfig.getString("clientId");
|
|
1879
1894
|
const clientSecret = envConfig.getString("clientSecret");
|
|
1880
|
-
const
|
|
1895
|
+
const customCallbackUrl = envConfig.getOptionalString("callbackUrl");
|
|
1896
|
+
const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
1881
1897
|
const authorizationUrl = envConfig.getString("authorizationUrl");
|
|
1882
1898
|
const tokenUrl = envConfig.getString("tokenUrl");
|
|
1883
1899
|
const scope = envConfig.getOptionalString("scope");
|
|
@@ -1913,633 +1929,449 @@ const createOAuth2Provider = (options) => {
|
|
|
1913
1929
|
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
1914
1930
|
disableRefresh,
|
|
1915
1931
|
providerId,
|
|
1916
|
-
tokenIssuer
|
|
1932
|
+
tokenIssuer,
|
|
1933
|
+
callbackUrl
|
|
1917
1934
|
});
|
|
1918
1935
|
});
|
|
1919
1936
|
};
|
|
1920
1937
|
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
const router = Router__default["default"]();
|
|
1924
|
-
const config = {
|
|
1925
|
-
issuer: baseUrl,
|
|
1926
|
-
token_endpoint: `${baseUrl}/v1/token`,
|
|
1927
|
-
userinfo_endpoint: `${baseUrl}/v1/userinfo`,
|
|
1928
|
-
jwks_uri: `${baseUrl}/.well-known/jwks.json`,
|
|
1929
|
-
response_types_supported: ["id_token"],
|
|
1930
|
-
subject_types_supported: ["public"],
|
|
1931
|
-
id_token_signing_alg_values_supported: ["RS256"],
|
|
1932
|
-
scopes_supported: ["openid"],
|
|
1933
|
-
token_endpoint_auth_methods_supported: [],
|
|
1934
|
-
claims_supported: ["sub"],
|
|
1935
|
-
grant_types_supported: []
|
|
1936
|
-
};
|
|
1937
|
-
router.get("/.well-known/openid-configuration", (_req, res) => {
|
|
1938
|
-
res.json(config);
|
|
1939
|
-
});
|
|
1940
|
-
router.get("/.well-known/jwks.json", async (_req, res) => {
|
|
1941
|
-
const { keys } = await tokenIssuer.listPublicKeys();
|
|
1942
|
-
res.json({ keys });
|
|
1943
|
-
});
|
|
1944
|
-
router.get("/v1/token", (_req, res) => {
|
|
1945
|
-
res.status(501).send("Not Implemented");
|
|
1946
|
-
});
|
|
1947
|
-
router.get("/v1/userinfo", (_req, res) => {
|
|
1948
|
-
res.status(501).send("Not Implemented");
|
|
1949
|
-
});
|
|
1950
|
-
return router;
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
const CLOCK_MARGIN_S = 10;
|
|
1954
|
-
class IdentityClient {
|
|
1938
|
+
const OAUTH2_PROXY_JWT_HEADER = "X-OAUTH2-PROXY-ID-TOKEN";
|
|
1939
|
+
class Oauth2ProxyAuthProvider {
|
|
1955
1940
|
constructor(options) {
|
|
1956
|
-
this.
|
|
1957
|
-
this.
|
|
1958
|
-
this.
|
|
1959
|
-
this.
|
|
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;
|
|
1960
1946
|
}
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
if (!token) {
|
|
1964
|
-
throw new errors.AuthenticationError("No token specified");
|
|
1965
|
-
}
|
|
1966
|
-
const key = await this.getKey(token);
|
|
1967
|
-
if (!key) {
|
|
1968
|
-
throw new errors.AuthenticationError("No signing key matching token found");
|
|
1969
|
-
}
|
|
1970
|
-
const decoded = jose.JWT.IdToken.verify(token, key, {
|
|
1971
|
-
algorithms: ["ES256"],
|
|
1972
|
-
audience: "backstage",
|
|
1973
|
-
issuer: this.issuer
|
|
1974
|
-
});
|
|
1975
|
-
if (!decoded.sub) {
|
|
1976
|
-
throw new errors.AuthenticationError("No user sub found in token");
|
|
1977
|
-
}
|
|
1978
|
-
const user = {
|
|
1979
|
-
id: decoded.sub,
|
|
1980
|
-
token,
|
|
1981
|
-
identity: {
|
|
1982
|
-
type: "user",
|
|
1983
|
-
userEntityRef: decoded.sub,
|
|
1984
|
-
ownershipEntityRefs: (_a = decoded.ent) != null ? _a : []
|
|
1985
|
-
}
|
|
1986
|
-
};
|
|
1987
|
-
return user;
|
|
1947
|
+
frameHandler() {
|
|
1948
|
+
return Promise.resolve(void 0);
|
|
1988
1949
|
}
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
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();
|
|
1992
1959
|
}
|
|
1993
|
-
const matches = authorizationHeader.match(/Bearer\s+(\S+)/i);
|
|
1994
|
-
return matches == null ? void 0 : matches[1];
|
|
1995
1960
|
}
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
complete: true
|
|
1999
|
-
});
|
|
2000
|
-
const keyStoreHasKey = !!this.keyStore.get({ kid: header.kid });
|
|
2001
|
-
const issuedAfterLastRefresh = (payload == null ? void 0 : payload.iat) && payload.iat > this.keyStoreUpdated - CLOCK_MARGIN_S;
|
|
2002
|
-
if (!keyStoreHasKey && issuedAfterLastRefresh) {
|
|
2003
|
-
await this.refreshKeyStore();
|
|
2004
|
-
}
|
|
2005
|
-
return this.keyStore.get({ kid: header.kid });
|
|
1961
|
+
start() {
|
|
1962
|
+
return Promise.resolve(void 0);
|
|
2006
1963
|
}
|
|
2007
|
-
async
|
|
2008
|
-
const
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
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
|
+
};
|
|
2017
1982
|
}
|
|
2018
|
-
|
|
2019
|
-
const
|
|
2020
|
-
const
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
}
|
|
2024
|
-
|
|
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
|
+
};
|
|
2025
1994
|
}
|
|
2026
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
|
+
};
|
|
2027
2011
|
|
|
2028
|
-
|
|
2029
|
-
class TokenFactory {
|
|
2012
|
+
class OidcAuthProvider {
|
|
2030
2013
|
constructor(options) {
|
|
2031
|
-
this.
|
|
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;
|
|
2032
2021
|
this.logger = options.logger;
|
|
2033
|
-
this.keyStore = options.keyStore;
|
|
2034
|
-
this.keyDurationSeconds = options.keyDurationSeconds;
|
|
2035
|
-
}
|
|
2036
|
-
async issueToken(params) {
|
|
2037
|
-
const key = await this.getKey();
|
|
2038
|
-
const iss = this.issuer;
|
|
2039
|
-
const sub = params.claims.sub;
|
|
2040
|
-
const ent = params.claims.ent;
|
|
2041
|
-
const aud = "backstage";
|
|
2042
|
-
const iat = Math.floor(Date.now() / MS_IN_S);
|
|
2043
|
-
const exp = iat + this.keyDurationSeconds;
|
|
2044
|
-
this.logger.info(`Issuing token for ${sub}, with entities ${ent != null ? ent : []}`);
|
|
2045
|
-
return jose.JWS.sign({ iss, sub, aud, iat, exp, ent }, key, {
|
|
2046
|
-
alg: key.alg,
|
|
2047
|
-
kid: key.kid
|
|
2048
|
-
});
|
|
2049
2022
|
}
|
|
2050
|
-
async
|
|
2051
|
-
const {
|
|
2052
|
-
const
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
expiredKeys.push(key);
|
|
2060
|
-
} else {
|
|
2061
|
-
validKeys.push(key);
|
|
2062
|
-
}
|
|
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;
|
|
2063
2032
|
}
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2033
|
+
return await executeRedirectStrategy(req, strategy, options);
|
|
2034
|
+
}
|
|
2035
|
+
async handler(req) {
|
|
2036
|
+
const { strategy } = await this.implementation;
|
|
2037
|
+
const { result, privateInfo } = await executeFrameHandlerStrategy(req, strategy);
|
|
2038
|
+
return {
|
|
2039
|
+
response: await this.handleResult(result),
|
|
2040
|
+
refreshToken: privateInfo.refreshToken
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
async refresh(req) {
|
|
2044
|
+
const { client } = await this.implementation;
|
|
2045
|
+
const tokenset = await client.refresh(req.refreshToken);
|
|
2046
|
+
if (!tokenset.access_token) {
|
|
2047
|
+
throw new Error("Refresh failed");
|
|
2070
2048
|
}
|
|
2071
|
-
|
|
2049
|
+
const userinfo = await client.userinfo(tokenset.access_token);
|
|
2050
|
+
return {
|
|
2051
|
+
response: await this.handleResult({ tokenset, userinfo }),
|
|
2052
|
+
refreshToken: tokenset.refresh_token
|
|
2053
|
+
};
|
|
2072
2054
|
}
|
|
2073
|
-
async
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2055
|
+
async setupStrategy(options) {
|
|
2056
|
+
const issuer = await openidClient.Issuer.discover(options.metadataUrl);
|
|
2057
|
+
const client = new issuer.Client({
|
|
2058
|
+
access_type: "offline",
|
|
2059
|
+
client_id: options.clientId,
|
|
2060
|
+
client_secret: options.clientSecret,
|
|
2061
|
+
redirect_uris: [options.callbackUrl],
|
|
2062
|
+
response_types: ["code"],
|
|
2063
|
+
id_token_signed_response_alg: options.tokenSignedResponseAlg || "RS256",
|
|
2064
|
+
scope: options.scope || ""
|
|
2065
|
+
});
|
|
2066
|
+
const strategy = new openidClient.Strategy({
|
|
2067
|
+
client,
|
|
2068
|
+
passReqToCallback: false
|
|
2069
|
+
}, (tokenset, userinfo, done) => {
|
|
2070
|
+
if (typeof done !== "function") {
|
|
2071
|
+
throw new Error("OIDC IdP must provide a userinfo_endpoint in the metadata response");
|
|
2077
2072
|
}
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
}
|
|
2081
|
-
this.keyExpiry = luxon.DateTime.utc().plus({
|
|
2082
|
-
seconds: this.keyDurationSeconds
|
|
2083
|
-
}).toJSDate();
|
|
2084
|
-
const promise = (async () => {
|
|
2085
|
-
const key = await jose.JWK.generate("EC", "P-256", {
|
|
2086
|
-
use: "sig",
|
|
2087
|
-
kid: uuid.v4(),
|
|
2088
|
-
alg: "ES256"
|
|
2073
|
+
done(void 0, { tokenset, userinfo }, {
|
|
2074
|
+
refreshToken: tokenset.refresh_token
|
|
2089
2075
|
});
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
this.
|
|
2099
|
-
|
|
2100
|
-
|
|
2076
|
+
});
|
|
2077
|
+
strategy.error = console.error;
|
|
2078
|
+
return { strategy, client };
|
|
2079
|
+
}
|
|
2080
|
+
async handleResult(result) {
|
|
2081
|
+
const context = {
|
|
2082
|
+
logger: this.logger,
|
|
2083
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
2084
|
+
tokenIssuer: this.tokenIssuer
|
|
2085
|
+
};
|
|
2086
|
+
const { profile } = await this.authHandler(result, context);
|
|
2087
|
+
const response = {
|
|
2088
|
+
providerInfo: {
|
|
2089
|
+
idToken: result.tokenset.id_token,
|
|
2090
|
+
accessToken: result.tokenset.access_token,
|
|
2091
|
+
scope: result.tokenset.scope,
|
|
2092
|
+
expiresInSeconds: result.tokenset.expires_in
|
|
2093
|
+
},
|
|
2094
|
+
profile
|
|
2095
|
+
};
|
|
2096
|
+
if (this.signInResolver) {
|
|
2097
|
+
response.backstageIdentity = await this.signInResolver({
|
|
2098
|
+
result,
|
|
2099
|
+
profile
|
|
2100
|
+
}, context);
|
|
2101
2101
|
}
|
|
2102
|
-
return
|
|
2102
|
+
return response;
|
|
2103
2103
|
}
|
|
2104
2104
|
}
|
|
2105
|
-
|
|
2106
|
-
const
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
const parsedDate = typeof date === "string" ? luxon.DateTime.fromSQL(date, { zone: "UTC" }) : luxon.DateTime.fromJSDate(date);
|
|
2110
|
-
if (!parsedDate.isValid) {
|
|
2111
|
-
throw new Error(`Failed to parse date, reason: ${parsedDate.invalidReason}, explanation: ${parsedDate.invalidExplanation}`);
|
|
2105
|
+
const oAuth2DefaultSignInResolver = async (info, ctx) => {
|
|
2106
|
+
const { profile } = info;
|
|
2107
|
+
if (!profile.email) {
|
|
2108
|
+
throw new Error("Profile contained no email");
|
|
2112
2109
|
}
|
|
2113
|
-
|
|
2110
|
+
const userId = profile.email.split("@")[0];
|
|
2111
|
+
const token = await ctx.tokenIssuer.issueToken({
|
|
2112
|
+
claims: {
|
|
2113
|
+
sub: `user:default/${userId}`,
|
|
2114
|
+
ent: [`user:default/${userId}`]
|
|
2115
|
+
}
|
|
2116
|
+
});
|
|
2117
|
+
return { id: userId, token };
|
|
2114
2118
|
};
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2119
|
+
const createOidcProvider = (options) => {
|
|
2120
|
+
return ({
|
|
2121
|
+
providerId,
|
|
2122
|
+
globalConfig,
|
|
2123
|
+
config,
|
|
2124
|
+
tokenIssuer,
|
|
2125
|
+
tokenManager,
|
|
2126
|
+
catalogApi,
|
|
2127
|
+
logger
|
|
2128
|
+
}) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
|
|
2129
|
+
var _a, _b;
|
|
2130
|
+
const clientId = envConfig.getString("clientId");
|
|
2131
|
+
const clientSecret = envConfig.getString("clientSecret");
|
|
2132
|
+
const customCallbackUrl = envConfig.getOptionalString("callbackUrl");
|
|
2133
|
+
const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
2134
|
+
const metadataUrl = envConfig.getString("metadataUrl");
|
|
2135
|
+
const tokenSignedResponseAlg = envConfig.getOptionalString("tokenSignedResponseAlg");
|
|
2136
|
+
const scope = envConfig.getOptionalString("scope");
|
|
2137
|
+
const prompt = envConfig.getOptionalString("prompt");
|
|
2138
|
+
const catalogIdentityClient = new CatalogIdentityClient({
|
|
2139
|
+
catalogApi,
|
|
2140
|
+
tokenManager
|
|
2120
2141
|
});
|
|
2121
|
-
|
|
2122
|
-
|
|
2142
|
+
const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ userinfo }) => ({
|
|
2143
|
+
profile: {
|
|
2144
|
+
displayName: userinfo.name,
|
|
2145
|
+
email: userinfo.email,
|
|
2146
|
+
picture: userinfo.picture
|
|
2147
|
+
}
|
|
2148
|
+
});
|
|
2149
|
+
const signInResolverFn = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : oAuth2DefaultSignInResolver;
|
|
2150
|
+
const signInResolver = (info) => signInResolverFn(info, {
|
|
2151
|
+
catalogIdentityClient,
|
|
2152
|
+
tokenIssuer,
|
|
2153
|
+
logger
|
|
2154
|
+
});
|
|
2155
|
+
const provider = new OidcAuthProvider({
|
|
2156
|
+
clientId,
|
|
2157
|
+
clientSecret,
|
|
2158
|
+
callbackUrl,
|
|
2159
|
+
tokenSignedResponseAlg,
|
|
2160
|
+
metadataUrl,
|
|
2161
|
+
scope,
|
|
2162
|
+
prompt,
|
|
2163
|
+
signInResolver,
|
|
2164
|
+
authHandler,
|
|
2165
|
+
logger,
|
|
2166
|
+
tokenIssuer,
|
|
2167
|
+
catalogIdentityClient
|
|
2168
|
+
});
|
|
2169
|
+
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
2170
|
+
disableRefresh: false,
|
|
2171
|
+
providerId,
|
|
2172
|
+
tokenIssuer,
|
|
2173
|
+
callbackUrl
|
|
2174
|
+
});
|
|
2175
|
+
});
|
|
2176
|
+
};
|
|
2177
|
+
|
|
2178
|
+
class OktaAuthProvider {
|
|
2123
2179
|
constructor(options) {
|
|
2124
|
-
this.
|
|
2180
|
+
this._store = {
|
|
2181
|
+
store(_req, cb) {
|
|
2182
|
+
cb(null, null);
|
|
2183
|
+
},
|
|
2184
|
+
verify(_req, _state, cb) {
|
|
2185
|
+
cb(null, true);
|
|
2186
|
+
}
|
|
2187
|
+
};
|
|
2188
|
+
this._signInResolver = options.signInResolver;
|
|
2189
|
+
this._authHandler = options.authHandler;
|
|
2190
|
+
this._tokenIssuer = options.tokenIssuer;
|
|
2191
|
+
this._catalogIdentityClient = options.catalogIdentityClient;
|
|
2192
|
+
this._logger = options.logger;
|
|
2193
|
+
this._strategy = new passportOktaOauth.Strategy({
|
|
2194
|
+
clientID: options.clientId,
|
|
2195
|
+
clientSecret: options.clientSecret,
|
|
2196
|
+
callbackURL: options.callbackUrl,
|
|
2197
|
+
audience: options.audience,
|
|
2198
|
+
passReqToCallback: false,
|
|
2199
|
+
store: this._store,
|
|
2200
|
+
response_type: "code"
|
|
2201
|
+
}, (accessToken, refreshToken, params, fullProfile, done) => {
|
|
2202
|
+
done(void 0, {
|
|
2203
|
+
accessToken,
|
|
2204
|
+
refreshToken,
|
|
2205
|
+
params,
|
|
2206
|
+
fullProfile
|
|
2207
|
+
}, {
|
|
2208
|
+
refreshToken
|
|
2209
|
+
});
|
|
2210
|
+
});
|
|
2125
2211
|
}
|
|
2126
|
-
async
|
|
2127
|
-
await this.
|
|
2128
|
-
|
|
2129
|
-
|
|
2212
|
+
async start(req) {
|
|
2213
|
+
return await executeRedirectStrategy(req, this._strategy, {
|
|
2214
|
+
accessType: "offline",
|
|
2215
|
+
prompt: "consent",
|
|
2216
|
+
scope: req.scope,
|
|
2217
|
+
state: encodeState(req.state)
|
|
2130
2218
|
});
|
|
2131
2219
|
}
|
|
2132
|
-
async
|
|
2133
|
-
const
|
|
2220
|
+
async handler(req) {
|
|
2221
|
+
const { result, privateInfo } = await executeFrameHandlerStrategy(req, this._strategy);
|
|
2134
2222
|
return {
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
createdAt: parseDate(row.created_at)
|
|
2138
|
-
}))
|
|
2223
|
+
response: await this.handleResult(result),
|
|
2224
|
+
refreshToken: privateInfo.refreshToken
|
|
2139
2225
|
};
|
|
2140
2226
|
}
|
|
2141
|
-
async
|
|
2142
|
-
await this.
|
|
2143
|
-
|
|
2144
|
-
}
|
|
2145
|
-
|
|
2146
|
-
class MemoryKeyStore {
|
|
2147
|
-
constructor() {
|
|
2148
|
-
this.keys = /* @__PURE__ */ new Map();
|
|
2149
|
-
}
|
|
2150
|
-
async addKey(key) {
|
|
2151
|
-
this.keys.set(key.kid, {
|
|
2152
|
-
createdAt: luxon.DateTime.utc().toJSDate(),
|
|
2153
|
-
key: JSON.stringify(key)
|
|
2154
|
-
});
|
|
2155
|
-
}
|
|
2156
|
-
async removeKeys(kids) {
|
|
2157
|
-
for (const kid of kids) {
|
|
2158
|
-
this.keys.delete(kid);
|
|
2159
|
-
}
|
|
2160
|
-
}
|
|
2161
|
-
async listKeys() {
|
|
2227
|
+
async refresh(req) {
|
|
2228
|
+
const { accessToken, refreshToken, params } = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
|
|
2229
|
+
const fullProfile = await executeFetchUserProfileStrategy(this._strategy, accessToken);
|
|
2162
2230
|
return {
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2231
|
+
response: await this.handleResult({
|
|
2232
|
+
fullProfile,
|
|
2233
|
+
params,
|
|
2234
|
+
accessToken
|
|
2235
|
+
}),
|
|
2236
|
+
refreshToken
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
async handleResult(result) {
|
|
2240
|
+
const context = {
|
|
2241
|
+
logger: this._logger,
|
|
2242
|
+
catalogIdentityClient: this._catalogIdentityClient,
|
|
2243
|
+
tokenIssuer: this._tokenIssuer
|
|
2244
|
+
};
|
|
2245
|
+
const { profile } = await this._authHandler(result, context);
|
|
2246
|
+
const response = {
|
|
2247
|
+
providerInfo: {
|
|
2248
|
+
idToken: result.params.id_token,
|
|
2249
|
+
accessToken: result.accessToken,
|
|
2250
|
+
scope: result.params.scope,
|
|
2251
|
+
expiresInSeconds: result.params.expires_in
|
|
2252
|
+
},
|
|
2253
|
+
profile
|
|
2167
2254
|
};
|
|
2255
|
+
if (this._signInResolver) {
|
|
2256
|
+
response.backstageIdentity = await this._signInResolver({
|
|
2257
|
+
result,
|
|
2258
|
+
profile
|
|
2259
|
+
}, context);
|
|
2260
|
+
}
|
|
2261
|
+
return response;
|
|
2168
2262
|
}
|
|
2169
2263
|
}
|
|
2170
|
-
|
|
2171
|
-
const
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
constructor(database, path, timeout) {
|
|
2175
|
-
this.database = database;
|
|
2176
|
-
this.path = path;
|
|
2177
|
-
this.timeout = timeout;
|
|
2264
|
+
const oktaEmailSignInResolver = async (info, ctx) => {
|
|
2265
|
+
const { profile } = info;
|
|
2266
|
+
if (!profile.email) {
|
|
2267
|
+
throw new Error("Okta profile contained no email");
|
|
2178
2268
|
}
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
return new FirestoreKeyStore(database, path != null ? path : DEFAULT_DOCUMENT_PATH, timeout != null ? timeout : DEFAULT_TIMEOUT_MS);
|
|
2183
|
-
}
|
|
2184
|
-
static async verifyConnection(keyStore, logger) {
|
|
2185
|
-
try {
|
|
2186
|
-
await keyStore.verify();
|
|
2187
|
-
} catch (error) {
|
|
2188
|
-
if (process.env.NODE_ENV !== "development") {
|
|
2189
|
-
throw new Error(`Failed to connect to database: ${error.message}`);
|
|
2190
|
-
}
|
|
2191
|
-
logger == null ? void 0 : logger.warn(`Failed to connect to database: ${error.message}`);
|
|
2192
|
-
}
|
|
2193
|
-
}
|
|
2194
|
-
async addKey(key) {
|
|
2195
|
-
await this.withTimeout(this.database.collection(this.path).doc(key.kid).set({
|
|
2196
|
-
kid: key.kid,
|
|
2197
|
-
key: JSON.stringify(key)
|
|
2198
|
-
}));
|
|
2199
|
-
}
|
|
2200
|
-
async listKeys() {
|
|
2201
|
-
const keys = await this.withTimeout(this.database.collection(this.path).get());
|
|
2202
|
-
return {
|
|
2203
|
-
items: keys.docs.map((key) => ({
|
|
2204
|
-
key: key.data(),
|
|
2205
|
-
createdAt: key.createTime.toDate()
|
|
2206
|
-
}))
|
|
2207
|
-
};
|
|
2208
|
-
}
|
|
2209
|
-
async removeKeys(kids) {
|
|
2210
|
-
for (const kid of kids) {
|
|
2211
|
-
await this.withTimeout(this.database.collection(this.path).doc(kid).delete());
|
|
2212
|
-
}
|
|
2213
|
-
}
|
|
2214
|
-
async withTimeout(operation) {
|
|
2215
|
-
const timer = new Promise((_, reject) => setTimeout(() => {
|
|
2216
|
-
reject(new Error(`Operation timed out after ${this.timeout}ms`));
|
|
2217
|
-
}, this.timeout));
|
|
2218
|
-
return Promise.race([operation, timer]);
|
|
2219
|
-
}
|
|
2220
|
-
async verify() {
|
|
2221
|
-
await this.withTimeout(this.database.collection(this.path).limit(1).get());
|
|
2222
|
-
}
|
|
2223
|
-
}
|
|
2224
|
-
|
|
2225
|
-
class KeyStores {
|
|
2226
|
-
static async fromConfig(config, options) {
|
|
2227
|
-
var _a;
|
|
2228
|
-
const { logger, database } = options != null ? options : {};
|
|
2229
|
-
const ks = config.getOptionalConfig("auth.keyStore");
|
|
2230
|
-
const provider = (_a = ks == null ? void 0 : ks.getOptionalString("provider")) != null ? _a : "database";
|
|
2231
|
-
logger == null ? void 0 : logger.info(`Configuring "${provider}" as KeyStore provider`);
|
|
2232
|
-
if (provider === "database") {
|
|
2233
|
-
if (!database) {
|
|
2234
|
-
throw new Error("This KeyStore provider requires a database");
|
|
2235
|
-
}
|
|
2236
|
-
return await DatabaseKeyStore.create({
|
|
2237
|
-
database: await database.getClient()
|
|
2238
|
-
});
|
|
2239
|
-
}
|
|
2240
|
-
if (provider === "memory") {
|
|
2241
|
-
return new MemoryKeyStore();
|
|
2242
|
-
}
|
|
2243
|
-
if (provider === "firestore") {
|
|
2244
|
-
const settings = ks == null ? void 0 : ks.getConfig(provider);
|
|
2245
|
-
const keyStore = await FirestoreKeyStore.create(lodash.pickBy({
|
|
2246
|
-
projectId: settings == null ? void 0 : settings.getOptionalString("projectId"),
|
|
2247
|
-
keyFilename: settings == null ? void 0 : settings.getOptionalString("keyFilename"),
|
|
2248
|
-
host: settings == null ? void 0 : settings.getOptionalString("host"),
|
|
2249
|
-
port: settings == null ? void 0 : settings.getOptionalNumber("port"),
|
|
2250
|
-
ssl: settings == null ? void 0 : settings.getOptionalBoolean("ssl"),
|
|
2251
|
-
path: settings == null ? void 0 : settings.getOptionalString("path"),
|
|
2252
|
-
timeout: settings == null ? void 0 : settings.getOptionalNumber("timeout")
|
|
2253
|
-
}, (value) => value !== void 0));
|
|
2254
|
-
await FirestoreKeyStore.verifyConnection(keyStore, logger);
|
|
2255
|
-
return keyStore;
|
|
2256
|
-
}
|
|
2257
|
-
throw new Error(`Unknown KeyStore provider: ${provider}`);
|
|
2258
|
-
}
|
|
2259
|
-
}
|
|
2260
|
-
|
|
2261
|
-
const OAUTH2_PROXY_JWT_HEADER = "X-OAUTH2-PROXY-ID-TOKEN";
|
|
2262
|
-
class Oauth2ProxyAuthProvider {
|
|
2263
|
-
constructor(options) {
|
|
2264
|
-
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
2265
|
-
this.logger = options.logger;
|
|
2266
|
-
this.tokenIssuer = options.tokenIssuer;
|
|
2267
|
-
this.signInResolver = options.signInResolver;
|
|
2268
|
-
this.authHandler = options.authHandler;
|
|
2269
|
-
}
|
|
2270
|
-
frameHandler() {
|
|
2271
|
-
return Promise.resolve(void 0);
|
|
2272
|
-
}
|
|
2273
|
-
async refresh(req, res) {
|
|
2274
|
-
try {
|
|
2275
|
-
const result = this.getResult(req);
|
|
2276
|
-
const response = await this.handleResult(result);
|
|
2277
|
-
res.json(response);
|
|
2278
|
-
} catch (e) {
|
|
2279
|
-
this.logger.error(`Exception occurred during ${OAUTH2_PROXY_JWT_HEADER} refresh`, e);
|
|
2280
|
-
res.status(401);
|
|
2281
|
-
res.end();
|
|
2282
|
-
}
|
|
2283
|
-
}
|
|
2284
|
-
start() {
|
|
2285
|
-
return Promise.resolve(void 0);
|
|
2286
|
-
}
|
|
2287
|
-
async handleResult(result) {
|
|
2288
|
-
const ctx = {
|
|
2289
|
-
logger: this.logger,
|
|
2290
|
-
tokenIssuer: this.tokenIssuer,
|
|
2291
|
-
catalogIdentityClient: this.catalogIdentityClient
|
|
2292
|
-
};
|
|
2293
|
-
const { profile } = await this.authHandler(result, ctx);
|
|
2294
|
-
const backstageSignInResult = await this.signInResolver({
|
|
2295
|
-
result,
|
|
2296
|
-
profile
|
|
2297
|
-
}, ctx);
|
|
2298
|
-
return {
|
|
2299
|
-
providerInfo: {
|
|
2300
|
-
accessToken: result.accessToken
|
|
2301
|
-
},
|
|
2302
|
-
backstageIdentity: prepareBackstageIdentityResponse(backstageSignInResult),
|
|
2303
|
-
profile
|
|
2304
|
-
};
|
|
2305
|
-
}
|
|
2306
|
-
getResult(req) {
|
|
2307
|
-
const authHeader = req.header(OAUTH2_PROXY_JWT_HEADER);
|
|
2308
|
-
const jwt = IdentityClient.getBearerToken(authHeader);
|
|
2309
|
-
if (!jwt) {
|
|
2310
|
-
throw new errors.AuthenticationError(`Missing or in incorrect format - Oauth2Proxy OIDC header: ${OAUTH2_PROXY_JWT_HEADER}`);
|
|
2269
|
+
const entity = await ctx.catalogIdentityClient.findUser({
|
|
2270
|
+
annotations: {
|
|
2271
|
+
"okta.com/email": profile.email
|
|
2311
2272
|
}
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2273
|
+
});
|
|
2274
|
+
const claims = getEntityClaims(entity);
|
|
2275
|
+
const token = await ctx.tokenIssuer.issueToken({ claims });
|
|
2276
|
+
return { id: entity.metadata.name, entity, token };
|
|
2277
|
+
};
|
|
2278
|
+
const oktaDefaultSignInResolver = async (info, ctx) => {
|
|
2279
|
+
const { profile } = info;
|
|
2280
|
+
if (!profile.email) {
|
|
2281
|
+
throw new Error("Okta profile contained no email");
|
|
2317
2282
|
}
|
|
2318
|
-
|
|
2319
|
-
const
|
|
2320
|
-
|
|
2321
|
-
const authHandler = options.authHandler;
|
|
2322
|
-
const catalogIdentityClient = new CatalogIdentityClient({
|
|
2323
|
-
catalogApi,
|
|
2324
|
-
tokenManager
|
|
2283
|
+
const userId = profile.email.split("@")[0];
|
|
2284
|
+
const token = await ctx.tokenIssuer.issueToken({
|
|
2285
|
+
claims: { sub: `user:default/${userId}`, ent: [`user:default/${userId}`] }
|
|
2325
2286
|
});
|
|
2326
|
-
return
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2287
|
+
return { id: userId, token };
|
|
2288
|
+
};
|
|
2289
|
+
const createOktaProvider = (_options) => {
|
|
2290
|
+
return ({
|
|
2291
|
+
providerId,
|
|
2292
|
+
globalConfig,
|
|
2293
|
+
config,
|
|
2330
2294
|
tokenIssuer,
|
|
2331
|
-
|
|
2295
|
+
tokenManager,
|
|
2296
|
+
catalogApi,
|
|
2297
|
+
logger
|
|
2298
|
+
}) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
|
|
2299
|
+
var _a, _b;
|
|
2300
|
+
const clientId = envConfig.getString("clientId");
|
|
2301
|
+
const clientSecret = envConfig.getString("clientSecret");
|
|
2302
|
+
const audience = envConfig.getString("audience");
|
|
2303
|
+
const customCallbackUrl = envConfig.getOptionalString("callbackUrl");
|
|
2304
|
+
const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
2305
|
+
if (!audience.startsWith("https://")) {
|
|
2306
|
+
throw new Error("URL for 'audience' must start with 'https://'.");
|
|
2307
|
+
}
|
|
2308
|
+
const catalogIdentityClient = new CatalogIdentityClient({
|
|
2309
|
+
catalogApi,
|
|
2310
|
+
tokenManager
|
|
2311
|
+
});
|
|
2312
|
+
const authHandler = (_options == null ? void 0 : _options.authHandler) ? _options.authHandler : async ({ fullProfile, params }) => ({
|
|
2313
|
+
profile: makeProfileInfo(fullProfile, params.id_token)
|
|
2314
|
+
});
|
|
2315
|
+
const signInResolverFn = (_b = (_a = _options == null ? void 0 : _options.signIn) == null ? void 0 : _a.resolver) != null ? _b : oktaDefaultSignInResolver;
|
|
2316
|
+
const signInResolver = (info) => signInResolverFn(info, {
|
|
2317
|
+
catalogIdentityClient,
|
|
2318
|
+
tokenIssuer,
|
|
2319
|
+
logger
|
|
2320
|
+
});
|
|
2321
|
+
const provider = new OktaAuthProvider({
|
|
2322
|
+
audience,
|
|
2323
|
+
clientId,
|
|
2324
|
+
clientSecret,
|
|
2325
|
+
callbackUrl,
|
|
2326
|
+
authHandler,
|
|
2327
|
+
signInResolver,
|
|
2328
|
+
tokenIssuer,
|
|
2329
|
+
catalogIdentityClient,
|
|
2330
|
+
logger
|
|
2331
|
+
});
|
|
2332
|
+
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
2333
|
+
disableRefresh: false,
|
|
2334
|
+
providerId,
|
|
2335
|
+
tokenIssuer,
|
|
2336
|
+
callbackUrl
|
|
2337
|
+
});
|
|
2332
2338
|
});
|
|
2333
2339
|
};
|
|
2334
2340
|
|
|
2335
|
-
class
|
|
2341
|
+
class OneLoginProvider {
|
|
2336
2342
|
constructor(options) {
|
|
2337
|
-
this.implementation = this.setupStrategy(options);
|
|
2338
|
-
this.scope = options.scope;
|
|
2339
|
-
this.prompt = options.prompt;
|
|
2340
2343
|
this.signInResolver = options.signInResolver;
|
|
2341
2344
|
this.authHandler = options.authHandler;
|
|
2342
2345
|
this.tokenIssuer = options.tokenIssuer;
|
|
2343
2346
|
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
2344
2347
|
this.logger = options.logger;
|
|
2348
|
+
this._strategy = new passportOneloginOauth.Strategy({
|
|
2349
|
+
issuer: options.issuer,
|
|
2350
|
+
clientID: options.clientId,
|
|
2351
|
+
clientSecret: options.clientSecret,
|
|
2352
|
+
callbackURL: options.callbackUrl,
|
|
2353
|
+
passReqToCallback: false
|
|
2354
|
+
}, (accessToken, refreshToken, params, fullProfile, done) => {
|
|
2355
|
+
done(void 0, {
|
|
2356
|
+
accessToken,
|
|
2357
|
+
refreshToken,
|
|
2358
|
+
params,
|
|
2359
|
+
fullProfile
|
|
2360
|
+
}, {
|
|
2361
|
+
refreshToken
|
|
2362
|
+
});
|
|
2363
|
+
});
|
|
2345
2364
|
}
|
|
2346
2365
|
async start(req) {
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2366
|
+
return await executeRedirectStrategy(req, this._strategy, {
|
|
2367
|
+
accessType: "offline",
|
|
2368
|
+
prompt: "consent",
|
|
2369
|
+
scope: "openid",
|
|
2350
2370
|
state: encodeState(req.state)
|
|
2351
|
-
};
|
|
2352
|
-
const prompt = this.prompt || "none";
|
|
2353
|
-
if (prompt !== "auto") {
|
|
2354
|
-
options.prompt = prompt;
|
|
2355
|
-
}
|
|
2356
|
-
return await executeRedirectStrategy(req, strategy, options);
|
|
2371
|
+
});
|
|
2357
2372
|
}
|
|
2358
2373
|
async handler(req) {
|
|
2359
|
-
const {
|
|
2360
|
-
const { result, privateInfo } = await executeFrameHandlerStrategy(req, strategy);
|
|
2361
|
-
return {
|
|
2362
|
-
response: await this.handleResult(result),
|
|
2363
|
-
refreshToken: privateInfo.refreshToken
|
|
2364
|
-
};
|
|
2365
|
-
}
|
|
2366
|
-
async refresh(req) {
|
|
2367
|
-
const { client } = await this.implementation;
|
|
2368
|
-
const tokenset = await client.refresh(req.refreshToken);
|
|
2369
|
-
if (!tokenset.access_token) {
|
|
2370
|
-
throw new Error("Refresh failed");
|
|
2371
|
-
}
|
|
2372
|
-
const userinfo = await client.userinfo(tokenset.access_token);
|
|
2373
|
-
return {
|
|
2374
|
-
response: await this.handleResult({ tokenset, userinfo }),
|
|
2375
|
-
refreshToken: tokenset.refresh_token
|
|
2376
|
-
};
|
|
2377
|
-
}
|
|
2378
|
-
async setupStrategy(options) {
|
|
2379
|
-
const issuer = await openidClient.Issuer.discover(options.metadataUrl);
|
|
2380
|
-
const client = new issuer.Client({
|
|
2381
|
-
access_type: "offline",
|
|
2382
|
-
client_id: options.clientId,
|
|
2383
|
-
client_secret: options.clientSecret,
|
|
2384
|
-
redirect_uris: [options.callbackUrl],
|
|
2385
|
-
response_types: ["code"],
|
|
2386
|
-
id_token_signed_response_alg: options.tokenSignedResponseAlg || "RS256",
|
|
2387
|
-
scope: options.scope || ""
|
|
2388
|
-
});
|
|
2389
|
-
const strategy = new openidClient.Strategy({
|
|
2390
|
-
client,
|
|
2391
|
-
passReqToCallback: false
|
|
2392
|
-
}, (tokenset, userinfo, done) => {
|
|
2393
|
-
if (typeof done !== "function") {
|
|
2394
|
-
throw new Error("OIDC IdP must provide a userinfo_endpoint in the metadata response");
|
|
2395
|
-
}
|
|
2396
|
-
done(void 0, { tokenset, userinfo }, {
|
|
2397
|
-
refreshToken: tokenset.refresh_token
|
|
2398
|
-
});
|
|
2399
|
-
});
|
|
2400
|
-
strategy.error = console.error;
|
|
2401
|
-
return { strategy, client };
|
|
2402
|
-
}
|
|
2403
|
-
async handleResult(result) {
|
|
2404
|
-
const context = {
|
|
2405
|
-
logger: this.logger,
|
|
2406
|
-
catalogIdentityClient: this.catalogIdentityClient,
|
|
2407
|
-
tokenIssuer: this.tokenIssuer
|
|
2408
|
-
};
|
|
2409
|
-
const { profile } = await this.authHandler(result, context);
|
|
2410
|
-
const response = {
|
|
2411
|
-
providerInfo: {
|
|
2412
|
-
idToken: result.tokenset.id_token,
|
|
2413
|
-
accessToken: result.tokenset.access_token,
|
|
2414
|
-
scope: result.tokenset.scope,
|
|
2415
|
-
expiresInSeconds: result.tokenset.expires_in
|
|
2416
|
-
},
|
|
2417
|
-
profile
|
|
2418
|
-
};
|
|
2419
|
-
if (this.signInResolver) {
|
|
2420
|
-
response.backstageIdentity = await this.signInResolver({
|
|
2421
|
-
result,
|
|
2422
|
-
profile
|
|
2423
|
-
}, context);
|
|
2424
|
-
}
|
|
2425
|
-
return response;
|
|
2426
|
-
}
|
|
2427
|
-
}
|
|
2428
|
-
const oAuth2DefaultSignInResolver = async (info, ctx) => {
|
|
2429
|
-
const { profile } = info;
|
|
2430
|
-
if (!profile.email) {
|
|
2431
|
-
throw new Error("Profile contained no email");
|
|
2432
|
-
}
|
|
2433
|
-
const userId = profile.email.split("@")[0];
|
|
2434
|
-
const token = await ctx.tokenIssuer.issueToken({
|
|
2435
|
-
claims: {
|
|
2436
|
-
sub: `user:default/${userId}`,
|
|
2437
|
-
ent: [`user:default/${userId}`]
|
|
2438
|
-
}
|
|
2439
|
-
});
|
|
2440
|
-
return { id: userId, token };
|
|
2441
|
-
};
|
|
2442
|
-
const createOidcProvider = (options) => {
|
|
2443
|
-
return ({
|
|
2444
|
-
providerId,
|
|
2445
|
-
globalConfig,
|
|
2446
|
-
config,
|
|
2447
|
-
tokenIssuer,
|
|
2448
|
-
tokenManager,
|
|
2449
|
-
catalogApi,
|
|
2450
|
-
logger
|
|
2451
|
-
}) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
|
|
2452
|
-
var _a, _b;
|
|
2453
|
-
const clientId = envConfig.getString("clientId");
|
|
2454
|
-
const clientSecret = envConfig.getString("clientSecret");
|
|
2455
|
-
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
2456
|
-
const metadataUrl = envConfig.getString("metadataUrl");
|
|
2457
|
-
const tokenSignedResponseAlg = envConfig.getOptionalString("tokenSignedResponseAlg");
|
|
2458
|
-
const scope = envConfig.getOptionalString("scope");
|
|
2459
|
-
const prompt = envConfig.getOptionalString("prompt");
|
|
2460
|
-
const catalogIdentityClient = new CatalogIdentityClient({
|
|
2461
|
-
catalogApi,
|
|
2462
|
-
tokenManager
|
|
2463
|
-
});
|
|
2464
|
-
const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ userinfo }) => ({
|
|
2465
|
-
profile: {
|
|
2466
|
-
displayName: userinfo.name,
|
|
2467
|
-
email: userinfo.email,
|
|
2468
|
-
picture: userinfo.picture
|
|
2469
|
-
}
|
|
2470
|
-
});
|
|
2471
|
-
const signInResolverFn = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : oAuth2DefaultSignInResolver;
|
|
2472
|
-
const signInResolver = (info) => signInResolverFn(info, {
|
|
2473
|
-
catalogIdentityClient,
|
|
2474
|
-
tokenIssuer,
|
|
2475
|
-
logger
|
|
2476
|
-
});
|
|
2477
|
-
const provider = new OidcAuthProvider({
|
|
2478
|
-
clientId,
|
|
2479
|
-
clientSecret,
|
|
2480
|
-
callbackUrl,
|
|
2481
|
-
tokenSignedResponseAlg,
|
|
2482
|
-
metadataUrl,
|
|
2483
|
-
scope,
|
|
2484
|
-
prompt,
|
|
2485
|
-
signInResolver,
|
|
2486
|
-
authHandler,
|
|
2487
|
-
logger,
|
|
2488
|
-
tokenIssuer,
|
|
2489
|
-
catalogIdentityClient
|
|
2490
|
-
});
|
|
2491
|
-
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
2492
|
-
disableRefresh: false,
|
|
2493
|
-
providerId,
|
|
2494
|
-
tokenIssuer
|
|
2495
|
-
});
|
|
2496
|
-
});
|
|
2497
|
-
};
|
|
2498
|
-
|
|
2499
|
-
class OktaAuthProvider {
|
|
2500
|
-
constructor(options) {
|
|
2501
|
-
this._store = {
|
|
2502
|
-
store(_req, cb) {
|
|
2503
|
-
cb(null, null);
|
|
2504
|
-
},
|
|
2505
|
-
verify(_req, _state, cb) {
|
|
2506
|
-
cb(null, true);
|
|
2507
|
-
}
|
|
2508
|
-
};
|
|
2509
|
-
this._signInResolver = options.signInResolver;
|
|
2510
|
-
this._authHandler = options.authHandler;
|
|
2511
|
-
this._tokenIssuer = options.tokenIssuer;
|
|
2512
|
-
this._catalogIdentityClient = options.catalogIdentityClient;
|
|
2513
|
-
this._logger = options.logger;
|
|
2514
|
-
this._strategy = new passportOktaOauth.Strategy({
|
|
2515
|
-
clientID: options.clientId,
|
|
2516
|
-
clientSecret: options.clientSecret,
|
|
2517
|
-
callbackURL: options.callbackUrl,
|
|
2518
|
-
audience: options.audience,
|
|
2519
|
-
passReqToCallback: false,
|
|
2520
|
-
store: this._store,
|
|
2521
|
-
response_type: "code"
|
|
2522
|
-
}, (accessToken, refreshToken, params, fullProfile, done) => {
|
|
2523
|
-
done(void 0, {
|
|
2524
|
-
accessToken,
|
|
2525
|
-
refreshToken,
|
|
2526
|
-
params,
|
|
2527
|
-
fullProfile
|
|
2528
|
-
}, {
|
|
2529
|
-
refreshToken
|
|
2530
|
-
});
|
|
2531
|
-
});
|
|
2532
|
-
}
|
|
2533
|
-
async start(req) {
|
|
2534
|
-
return await executeRedirectStrategy(req, this._strategy, {
|
|
2535
|
-
accessType: "offline",
|
|
2536
|
-
prompt: "consent",
|
|
2537
|
-
scope: req.scope,
|
|
2538
|
-
state: encodeState(req.state)
|
|
2539
|
-
});
|
|
2540
|
-
}
|
|
2541
|
-
async handler(req) {
|
|
2542
|
-
const { result, privateInfo } = await executeFrameHandlerStrategy(req, this._strategy);
|
|
2374
|
+
const { result, privateInfo } = await executeFrameHandlerStrategy(req, this._strategy);
|
|
2543
2375
|
return {
|
|
2544
2376
|
response: await this.handleResult(result),
|
|
2545
2377
|
refreshToken: privateInfo.refreshToken
|
|
@@ -2559,11 +2391,11 @@ class OktaAuthProvider {
|
|
|
2559
2391
|
}
|
|
2560
2392
|
async handleResult(result) {
|
|
2561
2393
|
const context = {
|
|
2562
|
-
logger: this.
|
|
2563
|
-
catalogIdentityClient: this.
|
|
2564
|
-
tokenIssuer: this.
|
|
2394
|
+
logger: this.logger,
|
|
2395
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
2396
|
+
tokenIssuer: this.tokenIssuer
|
|
2565
2397
|
};
|
|
2566
|
-
const { profile } = await this.
|
|
2398
|
+
const { profile } = await this.authHandler(result, context);
|
|
2567
2399
|
const response = {
|
|
2568
2400
|
providerInfo: {
|
|
2569
2401
|
idToken: result.params.id_token,
|
|
@@ -2573,8 +2405,8 @@ class OktaAuthProvider {
|
|
|
2573
2405
|
},
|
|
2574
2406
|
profile
|
|
2575
2407
|
};
|
|
2576
|
-
if (this.
|
|
2577
|
-
response.backstageIdentity = await this.
|
|
2408
|
+
if (this.signInResolver) {
|
|
2409
|
+
response.backstageIdentity = await this.signInResolver({
|
|
2578
2410
|
result,
|
|
2579
2411
|
profile
|
|
2580
2412
|
}, context);
|
|
@@ -2582,32 +2414,15 @@ class OktaAuthProvider {
|
|
|
2582
2414
|
return response;
|
|
2583
2415
|
}
|
|
2584
2416
|
}
|
|
2585
|
-
const
|
|
2586
|
-
const { profile } = info;
|
|
2587
|
-
if (!profile.email) {
|
|
2588
|
-
throw new Error("Okta profile contained no email");
|
|
2589
|
-
}
|
|
2590
|
-
const entity = await ctx.catalogIdentityClient.findUser({
|
|
2591
|
-
annotations: {
|
|
2592
|
-
"okta.com/email": profile.email
|
|
2593
|
-
}
|
|
2594
|
-
});
|
|
2595
|
-
const claims = getEntityClaims(entity);
|
|
2596
|
-
const token = await ctx.tokenIssuer.issueToken({ claims });
|
|
2597
|
-
return { id: entity.metadata.name, entity, token };
|
|
2598
|
-
};
|
|
2599
|
-
const oktaDefaultSignInResolver = async (info, ctx) => {
|
|
2417
|
+
const defaultSignInResolver = async (info) => {
|
|
2600
2418
|
const { profile } = info;
|
|
2601
2419
|
if (!profile.email) {
|
|
2602
|
-
throw new Error("
|
|
2420
|
+
throw new Error("OIDC profile contained no email");
|
|
2603
2421
|
}
|
|
2604
|
-
const
|
|
2605
|
-
|
|
2606
|
-
claims: { sub: `user:default/${userId}`, ent: [`user:default/${userId}`] }
|
|
2607
|
-
});
|
|
2608
|
-
return { id: userId, token };
|
|
2422
|
+
const id = profile.email.split("@")[0];
|
|
2423
|
+
return { id, token: "" };
|
|
2609
2424
|
};
|
|
2610
|
-
const
|
|
2425
|
+
const createOneLoginProvider = (options) => {
|
|
2611
2426
|
return ({
|
|
2612
2427
|
providerId,
|
|
2613
2428
|
globalConfig,
|
|
@@ -2620,29 +2435,22 @@ const createOktaProvider = (_options) => {
|
|
|
2620
2435
|
var _a, _b;
|
|
2621
2436
|
const clientId = envConfig.getString("clientId");
|
|
2622
2437
|
const clientSecret = envConfig.getString("clientSecret");
|
|
2623
|
-
const
|
|
2624
|
-
const
|
|
2625
|
-
|
|
2626
|
-
throw new Error("URL for 'audience' must start with 'https://'.");
|
|
2627
|
-
}
|
|
2438
|
+
const issuer = envConfig.getString("issuer");
|
|
2439
|
+
const customCallbackUrl = envConfig.getOptionalString("callbackUrl");
|
|
2440
|
+
const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
2628
2441
|
const catalogIdentityClient = new CatalogIdentityClient({
|
|
2629
2442
|
catalogApi,
|
|
2630
2443
|
tokenManager
|
|
2631
2444
|
});
|
|
2632
|
-
const authHandler = (
|
|
2445
|
+
const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ fullProfile, params }) => ({
|
|
2633
2446
|
profile: makeProfileInfo(fullProfile, params.id_token)
|
|
2634
2447
|
});
|
|
2635
|
-
const
|
|
2636
|
-
const
|
|
2637
|
-
catalogIdentityClient,
|
|
2638
|
-
tokenIssuer,
|
|
2639
|
-
logger
|
|
2640
|
-
});
|
|
2641
|
-
const provider = new OktaAuthProvider({
|
|
2642
|
-
audience,
|
|
2448
|
+
const signInResolver = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : defaultSignInResolver;
|
|
2449
|
+
const provider = new OneLoginProvider({
|
|
2643
2450
|
clientId,
|
|
2644
2451
|
clientSecret,
|
|
2645
2452
|
callbackUrl,
|
|
2453
|
+
issuer,
|
|
2646
2454
|
authHandler,
|
|
2647
2455
|
signInResolver,
|
|
2648
2456
|
tokenIssuer,
|
|
@@ -2652,147 +2460,22 @@ const createOktaProvider = (_options) => {
|
|
|
2652
2460
|
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
2653
2461
|
disableRefresh: false,
|
|
2654
2462
|
providerId,
|
|
2655
|
-
tokenIssuer
|
|
2463
|
+
tokenIssuer,
|
|
2464
|
+
callbackUrl
|
|
2656
2465
|
});
|
|
2657
2466
|
});
|
|
2658
2467
|
};
|
|
2659
2468
|
|
|
2660
|
-
class
|
|
2469
|
+
class SamlAuthProvider {
|
|
2661
2470
|
constructor(options) {
|
|
2471
|
+
this.appUrl = options.appUrl;
|
|
2662
2472
|
this.signInResolver = options.signInResolver;
|
|
2663
2473
|
this.authHandler = options.authHandler;
|
|
2664
2474
|
this.tokenIssuer = options.tokenIssuer;
|
|
2665
2475
|
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
2666
2476
|
this.logger = options.logger;
|
|
2667
|
-
this.
|
|
2668
|
-
|
|
2669
|
-
clientID: options.clientId,
|
|
2670
|
-
clientSecret: options.clientSecret,
|
|
2671
|
-
callbackURL: options.callbackUrl,
|
|
2672
|
-
passReqToCallback: false
|
|
2673
|
-
}, (accessToken, refreshToken, params, fullProfile, done) => {
|
|
2674
|
-
done(void 0, {
|
|
2675
|
-
accessToken,
|
|
2676
|
-
refreshToken,
|
|
2677
|
-
params,
|
|
2678
|
-
fullProfile
|
|
2679
|
-
}, {
|
|
2680
|
-
refreshToken
|
|
2681
|
-
});
|
|
2682
|
-
});
|
|
2683
|
-
}
|
|
2684
|
-
async start(req) {
|
|
2685
|
-
return await executeRedirectStrategy(req, this._strategy, {
|
|
2686
|
-
accessType: "offline",
|
|
2687
|
-
prompt: "consent",
|
|
2688
|
-
scope: "openid",
|
|
2689
|
-
state: encodeState(req.state)
|
|
2690
|
-
});
|
|
2691
|
-
}
|
|
2692
|
-
async handler(req) {
|
|
2693
|
-
const { result, privateInfo } = await executeFrameHandlerStrategy(req, this._strategy);
|
|
2694
|
-
return {
|
|
2695
|
-
response: await this.handleResult(result),
|
|
2696
|
-
refreshToken: privateInfo.refreshToken
|
|
2697
|
-
};
|
|
2698
|
-
}
|
|
2699
|
-
async refresh(req) {
|
|
2700
|
-
const { accessToken, refreshToken, params } = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
|
|
2701
|
-
const fullProfile = await executeFetchUserProfileStrategy(this._strategy, accessToken);
|
|
2702
|
-
return {
|
|
2703
|
-
response: await this.handleResult({
|
|
2704
|
-
fullProfile,
|
|
2705
|
-
params,
|
|
2706
|
-
accessToken
|
|
2707
|
-
}),
|
|
2708
|
-
refreshToken
|
|
2709
|
-
};
|
|
2710
|
-
}
|
|
2711
|
-
async handleResult(result) {
|
|
2712
|
-
const context = {
|
|
2713
|
-
logger: this.logger,
|
|
2714
|
-
catalogIdentityClient: this.catalogIdentityClient,
|
|
2715
|
-
tokenIssuer: this.tokenIssuer
|
|
2716
|
-
};
|
|
2717
|
-
const { profile } = await this.authHandler(result, context);
|
|
2718
|
-
const response = {
|
|
2719
|
-
providerInfo: {
|
|
2720
|
-
idToken: result.params.id_token,
|
|
2721
|
-
accessToken: result.accessToken,
|
|
2722
|
-
scope: result.params.scope,
|
|
2723
|
-
expiresInSeconds: result.params.expires_in
|
|
2724
|
-
},
|
|
2725
|
-
profile
|
|
2726
|
-
};
|
|
2727
|
-
if (this.signInResolver) {
|
|
2728
|
-
response.backstageIdentity = await this.signInResolver({
|
|
2729
|
-
result,
|
|
2730
|
-
profile
|
|
2731
|
-
}, context);
|
|
2732
|
-
}
|
|
2733
|
-
return response;
|
|
2734
|
-
}
|
|
2735
|
-
}
|
|
2736
|
-
const defaultSignInResolver = async (info) => {
|
|
2737
|
-
const { profile } = info;
|
|
2738
|
-
if (!profile.email) {
|
|
2739
|
-
throw new Error("OIDC profile contained no email");
|
|
2740
|
-
}
|
|
2741
|
-
const id = profile.email.split("@")[0];
|
|
2742
|
-
return { id, token: "" };
|
|
2743
|
-
};
|
|
2744
|
-
const createOneLoginProvider = (options) => {
|
|
2745
|
-
return ({
|
|
2746
|
-
providerId,
|
|
2747
|
-
globalConfig,
|
|
2748
|
-
config,
|
|
2749
|
-
tokenIssuer,
|
|
2750
|
-
tokenManager,
|
|
2751
|
-
catalogApi,
|
|
2752
|
-
logger
|
|
2753
|
-
}) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
|
|
2754
|
-
var _a, _b;
|
|
2755
|
-
const clientId = envConfig.getString("clientId");
|
|
2756
|
-
const clientSecret = envConfig.getString("clientSecret");
|
|
2757
|
-
const issuer = envConfig.getString("issuer");
|
|
2758
|
-
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
2759
|
-
const catalogIdentityClient = new CatalogIdentityClient({
|
|
2760
|
-
catalogApi,
|
|
2761
|
-
tokenManager
|
|
2762
|
-
});
|
|
2763
|
-
const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ fullProfile, params }) => ({
|
|
2764
|
-
profile: makeProfileInfo(fullProfile, params.id_token)
|
|
2765
|
-
});
|
|
2766
|
-
const signInResolver = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : defaultSignInResolver;
|
|
2767
|
-
const provider = new OneLoginProvider({
|
|
2768
|
-
clientId,
|
|
2769
|
-
clientSecret,
|
|
2770
|
-
callbackUrl,
|
|
2771
|
-
issuer,
|
|
2772
|
-
authHandler,
|
|
2773
|
-
signInResolver,
|
|
2774
|
-
tokenIssuer,
|
|
2775
|
-
catalogIdentityClient,
|
|
2776
|
-
logger
|
|
2777
|
-
});
|
|
2778
|
-
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
2779
|
-
disableRefresh: false,
|
|
2780
|
-
providerId,
|
|
2781
|
-
tokenIssuer
|
|
2782
|
-
});
|
|
2783
|
-
});
|
|
2784
|
-
};
|
|
2785
|
-
|
|
2786
|
-
class SamlAuthProvider {
|
|
2787
|
-
constructor(options) {
|
|
2788
|
-
this.appUrl = options.appUrl;
|
|
2789
|
-
this.signInResolver = options.signInResolver;
|
|
2790
|
-
this.authHandler = options.authHandler;
|
|
2791
|
-
this.tokenIssuer = options.tokenIssuer;
|
|
2792
|
-
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
2793
|
-
this.logger = options.logger;
|
|
2794
|
-
this.strategy = new passportSaml.Strategy({ ...options }, (fullProfile, done) => {
|
|
2795
|
-
done(void 0, { fullProfile });
|
|
2477
|
+
this.strategy = new passportSaml.Strategy({ ...options }, (fullProfile, done) => {
|
|
2478
|
+
done(void 0, { fullProfile });
|
|
2796
2479
|
});
|
|
2797
2480
|
}
|
|
2798
2481
|
async start(req, res) {
|
|
@@ -3000,6 +2683,271 @@ const factories = {
|
|
|
3000
2683
|
atlassian: createAtlassianProvider()
|
|
3001
2684
|
};
|
|
3002
2685
|
|
|
2686
|
+
function createOidcRouter(options) {
|
|
2687
|
+
const { baseUrl, tokenIssuer } = options;
|
|
2688
|
+
const router = Router__default["default"]();
|
|
2689
|
+
const config = {
|
|
2690
|
+
issuer: baseUrl,
|
|
2691
|
+
token_endpoint: `${baseUrl}/v1/token`,
|
|
2692
|
+
userinfo_endpoint: `${baseUrl}/v1/userinfo`,
|
|
2693
|
+
jwks_uri: `${baseUrl}/.well-known/jwks.json`,
|
|
2694
|
+
response_types_supported: ["id_token"],
|
|
2695
|
+
subject_types_supported: ["public"],
|
|
2696
|
+
id_token_signing_alg_values_supported: ["RS256"],
|
|
2697
|
+
scopes_supported: ["openid"],
|
|
2698
|
+
token_endpoint_auth_methods_supported: [],
|
|
2699
|
+
claims_supported: ["sub"],
|
|
2700
|
+
grant_types_supported: []
|
|
2701
|
+
};
|
|
2702
|
+
router.get("/.well-known/openid-configuration", (_req, res) => {
|
|
2703
|
+
res.json(config);
|
|
2704
|
+
});
|
|
2705
|
+
router.get("/.well-known/jwks.json", async (_req, res) => {
|
|
2706
|
+
const { keys } = await tokenIssuer.listPublicKeys();
|
|
2707
|
+
res.json({ keys });
|
|
2708
|
+
});
|
|
2709
|
+
router.get("/v1/token", (_req, res) => {
|
|
2710
|
+
res.status(501).send("Not Implemented");
|
|
2711
|
+
});
|
|
2712
|
+
router.get("/v1/userinfo", (_req, res) => {
|
|
2713
|
+
res.status(501).send("Not Implemented");
|
|
2714
|
+
});
|
|
2715
|
+
return router;
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
const MS_IN_S = 1e3;
|
|
2719
|
+
class TokenFactory {
|
|
2720
|
+
constructor(options) {
|
|
2721
|
+
this.issuer = options.issuer;
|
|
2722
|
+
this.logger = options.logger;
|
|
2723
|
+
this.keyStore = options.keyStore;
|
|
2724
|
+
this.keyDurationSeconds = options.keyDurationSeconds;
|
|
2725
|
+
}
|
|
2726
|
+
async issueToken(params) {
|
|
2727
|
+
const key = await this.getKey();
|
|
2728
|
+
const iss = this.issuer;
|
|
2729
|
+
const sub = params.claims.sub;
|
|
2730
|
+
const ent = params.claims.ent;
|
|
2731
|
+
const aud = "backstage";
|
|
2732
|
+
const iat = Math.floor(Date.now() / MS_IN_S);
|
|
2733
|
+
const exp = iat + this.keyDurationSeconds;
|
|
2734
|
+
this.logger.info(`Issuing token for ${sub}, with entities ${ent != null ? ent : []}`);
|
|
2735
|
+
return jose.JWS.sign({ iss, sub, aud, iat, exp, ent }, key, {
|
|
2736
|
+
alg: key.alg,
|
|
2737
|
+
kid: key.kid
|
|
2738
|
+
});
|
|
2739
|
+
}
|
|
2740
|
+
async listPublicKeys() {
|
|
2741
|
+
const { items: keys } = await this.keyStore.listKeys();
|
|
2742
|
+
const validKeys = [];
|
|
2743
|
+
const expiredKeys = [];
|
|
2744
|
+
for (const key of keys) {
|
|
2745
|
+
const expireAt = luxon.DateTime.fromJSDate(key.createdAt).plus({
|
|
2746
|
+
seconds: 3 * this.keyDurationSeconds
|
|
2747
|
+
});
|
|
2748
|
+
if (expireAt < luxon.DateTime.local()) {
|
|
2749
|
+
expiredKeys.push(key);
|
|
2750
|
+
} else {
|
|
2751
|
+
validKeys.push(key);
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
if (expiredKeys.length > 0) {
|
|
2755
|
+
const kids = expiredKeys.map(({ key }) => key.kid);
|
|
2756
|
+
this.logger.info(`Removing expired signing keys, '${kids.join("', '")}'`);
|
|
2757
|
+
this.keyStore.removeKeys(kids).catch((error) => {
|
|
2758
|
+
this.logger.error(`Failed to remove expired keys, ${error}`);
|
|
2759
|
+
});
|
|
2760
|
+
}
|
|
2761
|
+
return { keys: validKeys.map(({ key }) => key) };
|
|
2762
|
+
}
|
|
2763
|
+
async getKey() {
|
|
2764
|
+
if (this.privateKeyPromise) {
|
|
2765
|
+
if (this.keyExpiry && luxon.DateTime.fromJSDate(this.keyExpiry) > luxon.DateTime.local()) {
|
|
2766
|
+
return this.privateKeyPromise;
|
|
2767
|
+
}
|
|
2768
|
+
this.logger.info(`Signing key has expired, generating new key`);
|
|
2769
|
+
delete this.privateKeyPromise;
|
|
2770
|
+
}
|
|
2771
|
+
this.keyExpiry = luxon.DateTime.utc().plus({
|
|
2772
|
+
seconds: this.keyDurationSeconds
|
|
2773
|
+
}).toJSDate();
|
|
2774
|
+
const promise = (async () => {
|
|
2775
|
+
const key = await jose.JWK.generate("EC", "P-256", {
|
|
2776
|
+
use: "sig",
|
|
2777
|
+
kid: uuid.v4(),
|
|
2778
|
+
alg: "ES256"
|
|
2779
|
+
});
|
|
2780
|
+
this.logger.info(`Created new signing key ${key.kid}`);
|
|
2781
|
+
await this.keyStore.addKey(key.toJWK(false));
|
|
2782
|
+
return key;
|
|
2783
|
+
})();
|
|
2784
|
+
this.privateKeyPromise = promise;
|
|
2785
|
+
try {
|
|
2786
|
+
await promise;
|
|
2787
|
+
} catch (error) {
|
|
2788
|
+
this.logger.error(`Failed to generate new signing key, ${error}`);
|
|
2789
|
+
delete this.keyExpiry;
|
|
2790
|
+
delete this.privateKeyPromise;
|
|
2791
|
+
}
|
|
2792
|
+
return promise;
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
const migrationsDir = backendCommon.resolvePackagePath("@backstage/plugin-auth-backend", "migrations");
|
|
2797
|
+
const TABLE = "signing_keys";
|
|
2798
|
+
const parseDate = (date) => {
|
|
2799
|
+
const parsedDate = typeof date === "string" ? luxon.DateTime.fromSQL(date, { zone: "UTC" }) : luxon.DateTime.fromJSDate(date);
|
|
2800
|
+
if (!parsedDate.isValid) {
|
|
2801
|
+
throw new Error(`Failed to parse date, reason: ${parsedDate.invalidReason}, explanation: ${parsedDate.invalidExplanation}`);
|
|
2802
|
+
}
|
|
2803
|
+
return parsedDate.toJSDate();
|
|
2804
|
+
};
|
|
2805
|
+
class DatabaseKeyStore {
|
|
2806
|
+
static async create(options) {
|
|
2807
|
+
const { database } = options;
|
|
2808
|
+
await database.migrate.latest({
|
|
2809
|
+
directory: migrationsDir
|
|
2810
|
+
});
|
|
2811
|
+
return new DatabaseKeyStore(options);
|
|
2812
|
+
}
|
|
2813
|
+
constructor(options) {
|
|
2814
|
+
this.database = options.database;
|
|
2815
|
+
}
|
|
2816
|
+
async addKey(key) {
|
|
2817
|
+
await this.database(TABLE).insert({
|
|
2818
|
+
kid: key.kid,
|
|
2819
|
+
key: JSON.stringify(key)
|
|
2820
|
+
});
|
|
2821
|
+
}
|
|
2822
|
+
async listKeys() {
|
|
2823
|
+
const rows = await this.database(TABLE).select();
|
|
2824
|
+
return {
|
|
2825
|
+
items: rows.map((row) => ({
|
|
2826
|
+
key: JSON.parse(row.key),
|
|
2827
|
+
createdAt: parseDate(row.created_at)
|
|
2828
|
+
}))
|
|
2829
|
+
};
|
|
2830
|
+
}
|
|
2831
|
+
async removeKeys(kids) {
|
|
2832
|
+
await this.database(TABLE).delete().whereIn("kid", kids);
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
class MemoryKeyStore {
|
|
2837
|
+
constructor() {
|
|
2838
|
+
this.keys = /* @__PURE__ */ new Map();
|
|
2839
|
+
}
|
|
2840
|
+
async addKey(key) {
|
|
2841
|
+
this.keys.set(key.kid, {
|
|
2842
|
+
createdAt: luxon.DateTime.utc().toJSDate(),
|
|
2843
|
+
key: JSON.stringify(key)
|
|
2844
|
+
});
|
|
2845
|
+
}
|
|
2846
|
+
async removeKeys(kids) {
|
|
2847
|
+
for (const kid of kids) {
|
|
2848
|
+
this.keys.delete(kid);
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
async listKeys() {
|
|
2852
|
+
return {
|
|
2853
|
+
items: Array.from(this.keys).map(([, { createdAt, key: keyStr }]) => ({
|
|
2854
|
+
createdAt,
|
|
2855
|
+
key: JSON.parse(keyStr)
|
|
2856
|
+
}))
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
2862
|
+
const DEFAULT_DOCUMENT_PATH = "sessions";
|
|
2863
|
+
class FirestoreKeyStore {
|
|
2864
|
+
constructor(database, path, timeout) {
|
|
2865
|
+
this.database = database;
|
|
2866
|
+
this.path = path;
|
|
2867
|
+
this.timeout = timeout;
|
|
2868
|
+
}
|
|
2869
|
+
static async create(settings) {
|
|
2870
|
+
const { path, timeout, ...firestoreSettings } = settings != null ? settings : {};
|
|
2871
|
+
const database = new firestore.Firestore(firestoreSettings);
|
|
2872
|
+
return new FirestoreKeyStore(database, path != null ? path : DEFAULT_DOCUMENT_PATH, timeout != null ? timeout : DEFAULT_TIMEOUT_MS);
|
|
2873
|
+
}
|
|
2874
|
+
static async verifyConnection(keyStore, logger) {
|
|
2875
|
+
try {
|
|
2876
|
+
await keyStore.verify();
|
|
2877
|
+
} catch (error) {
|
|
2878
|
+
if (process.env.NODE_ENV !== "development") {
|
|
2879
|
+
throw new Error(`Failed to connect to database: ${error.message}`);
|
|
2880
|
+
}
|
|
2881
|
+
logger == null ? void 0 : logger.warn(`Failed to connect to database: ${error.message}`);
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
async addKey(key) {
|
|
2885
|
+
await this.withTimeout(this.database.collection(this.path).doc(key.kid).set({
|
|
2886
|
+
kid: key.kid,
|
|
2887
|
+
key: JSON.stringify(key)
|
|
2888
|
+
}));
|
|
2889
|
+
}
|
|
2890
|
+
async listKeys() {
|
|
2891
|
+
const keys = await this.withTimeout(this.database.collection(this.path).get());
|
|
2892
|
+
return {
|
|
2893
|
+
items: keys.docs.map((key) => ({
|
|
2894
|
+
key: key.data(),
|
|
2895
|
+
createdAt: key.createTime.toDate()
|
|
2896
|
+
}))
|
|
2897
|
+
};
|
|
2898
|
+
}
|
|
2899
|
+
async removeKeys(kids) {
|
|
2900
|
+
for (const kid of kids) {
|
|
2901
|
+
await this.withTimeout(this.database.collection(this.path).doc(kid).delete());
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
async withTimeout(operation) {
|
|
2905
|
+
const timer = new Promise((_, reject) => setTimeout(() => {
|
|
2906
|
+
reject(new Error(`Operation timed out after ${this.timeout}ms`));
|
|
2907
|
+
}, this.timeout));
|
|
2908
|
+
return Promise.race([operation, timer]);
|
|
2909
|
+
}
|
|
2910
|
+
async verify() {
|
|
2911
|
+
await this.withTimeout(this.database.collection(this.path).limit(1).get());
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
class KeyStores {
|
|
2916
|
+
static async fromConfig(config, options) {
|
|
2917
|
+
var _a;
|
|
2918
|
+
const { logger, database } = options != null ? options : {};
|
|
2919
|
+
const ks = config.getOptionalConfig("auth.keyStore");
|
|
2920
|
+
const provider = (_a = ks == null ? void 0 : ks.getOptionalString("provider")) != null ? _a : "database";
|
|
2921
|
+
logger == null ? void 0 : logger.info(`Configuring "${provider}" as KeyStore provider`);
|
|
2922
|
+
if (provider === "database") {
|
|
2923
|
+
if (!database) {
|
|
2924
|
+
throw new Error("This KeyStore provider requires a database");
|
|
2925
|
+
}
|
|
2926
|
+
return await DatabaseKeyStore.create({
|
|
2927
|
+
database: await database.getClient()
|
|
2928
|
+
});
|
|
2929
|
+
}
|
|
2930
|
+
if (provider === "memory") {
|
|
2931
|
+
return new MemoryKeyStore();
|
|
2932
|
+
}
|
|
2933
|
+
if (provider === "firestore") {
|
|
2934
|
+
const settings = ks == null ? void 0 : ks.getConfig(provider);
|
|
2935
|
+
const keyStore = await FirestoreKeyStore.create(lodash.pickBy({
|
|
2936
|
+
projectId: settings == null ? void 0 : settings.getOptionalString("projectId"),
|
|
2937
|
+
keyFilename: settings == null ? void 0 : settings.getOptionalString("keyFilename"),
|
|
2938
|
+
host: settings == null ? void 0 : settings.getOptionalString("host"),
|
|
2939
|
+
port: settings == null ? void 0 : settings.getOptionalNumber("port"),
|
|
2940
|
+
ssl: settings == null ? void 0 : settings.getOptionalBoolean("ssl"),
|
|
2941
|
+
path: settings == null ? void 0 : settings.getOptionalString("path"),
|
|
2942
|
+
timeout: settings == null ? void 0 : settings.getOptionalNumber("timeout")
|
|
2943
|
+
}, (value) => value !== void 0));
|
|
2944
|
+
await FirestoreKeyStore.verifyConnection(keyStore, logger);
|
|
2945
|
+
return keyStore;
|
|
2946
|
+
}
|
|
2947
|
+
throw new Error(`Unknown KeyStore provider: ${provider}`);
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
|
|
3003
2951
|
async function createRouter(options) {
|
|
3004
2952
|
const {
|
|
3005
2953
|
logger,
|
|
@@ -3051,7 +2999,11 @@ async function createRouter(options) {
|
|
|
3051
2999
|
try {
|
|
3052
3000
|
const provider = providerFactory({
|
|
3053
3001
|
providerId,
|
|
3054
|
-
globalConfig: {
|
|
3002
|
+
globalConfig: {
|
|
3003
|
+
baseUrl: authUrl,
|
|
3004
|
+
appUrl,
|
|
3005
|
+
isOriginAllowed
|
|
3006
|
+
},
|
|
3055
3007
|
config: providersConfig.getConfig(providerId),
|
|
3056
3008
|
logger,
|
|
3057
3009
|
tokenManager,
|
|
@@ -3111,7 +3063,6 @@ function createOriginFilter(config) {
|
|
|
3111
3063
|
}
|
|
3112
3064
|
|
|
3113
3065
|
exports.CatalogIdentityClient = CatalogIdentityClient;
|
|
3114
|
-
exports.IdentityClient = IdentityClient;
|
|
3115
3066
|
exports.OAuthAdapter = OAuthAdapter;
|
|
3116
3067
|
exports.OAuthEnvironmentHandler = OAuthEnvironmentHandler;
|
|
3117
3068
|
exports.bitbucketUserIdSignInResolver = bitbucketUserIdSignInResolver;
|