@backstage/plugin-auth-backend 0.9.0 → 0.10.2
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 +116 -0
- package/dist/index.cjs.js +661 -730
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +27 -121
- package/migrations/20210326100300_timestamptz.js +2 -2
- package/package.json +24 -21
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 }
|
|
@@ -732,7 +736,6 @@ const createAtlassianProvider = (options) => {
|
|
|
732
736
|
tokenIssuer
|
|
733
737
|
});
|
|
734
738
|
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
735
|
-
disableRefresh: true,
|
|
736
739
|
providerId,
|
|
737
740
|
tokenIssuer,
|
|
738
741
|
callbackUrl
|
|
@@ -1932,487 +1935,147 @@ const createOAuth2Provider = (options) => {
|
|
|
1932
1935
|
});
|
|
1933
1936
|
};
|
|
1934
1937
|
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
const router = Router__default["default"]();
|
|
1938
|
-
const config = {
|
|
1939
|
-
issuer: baseUrl,
|
|
1940
|
-
token_endpoint: `${baseUrl}/v1/token`,
|
|
1941
|
-
userinfo_endpoint: `${baseUrl}/v1/userinfo`,
|
|
1942
|
-
jwks_uri: `${baseUrl}/.well-known/jwks.json`,
|
|
1943
|
-
response_types_supported: ["id_token"],
|
|
1944
|
-
subject_types_supported: ["public"],
|
|
1945
|
-
id_token_signing_alg_values_supported: ["RS256"],
|
|
1946
|
-
scopes_supported: ["openid"],
|
|
1947
|
-
token_endpoint_auth_methods_supported: [],
|
|
1948
|
-
claims_supported: ["sub"],
|
|
1949
|
-
grant_types_supported: []
|
|
1950
|
-
};
|
|
1951
|
-
router.get("/.well-known/openid-configuration", (_req, res) => {
|
|
1952
|
-
res.json(config);
|
|
1953
|
-
});
|
|
1954
|
-
router.get("/.well-known/jwks.json", async (_req, res) => {
|
|
1955
|
-
const { keys } = await tokenIssuer.listPublicKeys();
|
|
1956
|
-
res.json({ keys });
|
|
1957
|
-
});
|
|
1958
|
-
router.get("/v1/token", (_req, res) => {
|
|
1959
|
-
res.status(501).send("Not Implemented");
|
|
1960
|
-
});
|
|
1961
|
-
router.get("/v1/userinfo", (_req, res) => {
|
|
1962
|
-
res.status(501).send("Not Implemented");
|
|
1963
|
-
});
|
|
1964
|
-
return router;
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
const CLOCK_MARGIN_S = 10;
|
|
1968
|
-
class IdentityClient {
|
|
1938
|
+
const OAUTH2_PROXY_JWT_HEADER = "X-OAUTH2-PROXY-ID-TOKEN";
|
|
1939
|
+
class Oauth2ProxyAuthProvider {
|
|
1969
1940
|
constructor(options) {
|
|
1970
|
-
this.
|
|
1971
|
-
this.
|
|
1972
|
-
this.
|
|
1973
|
-
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;
|
|
1974
1946
|
}
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
if (!token) {
|
|
1978
|
-
throw new errors.AuthenticationError("No token specified");
|
|
1979
|
-
}
|
|
1980
|
-
const key = await this.getKey(token);
|
|
1981
|
-
if (!key) {
|
|
1982
|
-
throw new errors.AuthenticationError("No signing key matching token found");
|
|
1983
|
-
}
|
|
1984
|
-
const decoded = jose.JWT.IdToken.verify(token, key, {
|
|
1985
|
-
algorithms: ["ES256"],
|
|
1986
|
-
audience: "backstage",
|
|
1987
|
-
issuer: this.issuer
|
|
1988
|
-
});
|
|
1989
|
-
if (!decoded.sub) {
|
|
1990
|
-
throw new errors.AuthenticationError("No user sub found in token");
|
|
1991
|
-
}
|
|
1992
|
-
const user = {
|
|
1993
|
-
id: decoded.sub,
|
|
1994
|
-
token,
|
|
1995
|
-
identity: {
|
|
1996
|
-
type: "user",
|
|
1997
|
-
userEntityRef: decoded.sub,
|
|
1998
|
-
ownershipEntityRefs: (_a = decoded.ent) != null ? _a : []
|
|
1999
|
-
}
|
|
2000
|
-
};
|
|
2001
|
-
return user;
|
|
1947
|
+
frameHandler() {
|
|
1948
|
+
return Promise.resolve(void 0);
|
|
2002
1949
|
}
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
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();
|
|
2006
1959
|
}
|
|
2007
|
-
const matches = authorizationHeader.match(/Bearer\s+(\S+)/i);
|
|
2008
|
-
return matches == null ? void 0 : matches[1];
|
|
2009
1960
|
}
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
complete: true
|
|
2013
|
-
});
|
|
2014
|
-
const keyStoreHasKey = !!this.keyStore.get({ kid: header.kid });
|
|
2015
|
-
const issuedAfterLastRefresh = (payload == null ? void 0 : payload.iat) && payload.iat > this.keyStoreUpdated - CLOCK_MARGIN_S;
|
|
2016
|
-
if (!keyStoreHasKey && issuedAfterLastRefresh) {
|
|
2017
|
-
await this.refreshKeyStore();
|
|
2018
|
-
}
|
|
2019
|
-
return this.keyStore.get({ kid: header.kid });
|
|
1961
|
+
start() {
|
|
1962
|
+
return Promise.resolve(void 0);
|
|
2020
1963
|
}
|
|
2021
|
-
async
|
|
2022
|
-
const
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
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
|
+
};
|
|
2031
1982
|
}
|
|
2032
|
-
|
|
2033
|
-
const
|
|
2034
|
-
const
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
}
|
|
2038
|
-
|
|
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
|
+
};
|
|
2039
1994
|
}
|
|
2040
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
|
+
};
|
|
2041
2011
|
|
|
2042
|
-
|
|
2043
|
-
class TokenFactory {
|
|
2012
|
+
class OidcAuthProvider {
|
|
2044
2013
|
constructor(options) {
|
|
2045
|
-
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;
|
|
2046
2021
|
this.logger = options.logger;
|
|
2047
|
-
this.keyStore = options.keyStore;
|
|
2048
|
-
this.keyDurationSeconds = options.keyDurationSeconds;
|
|
2049
|
-
}
|
|
2050
|
-
async issueToken(params) {
|
|
2051
|
-
const key = await this.getKey();
|
|
2052
|
-
const iss = this.issuer;
|
|
2053
|
-
const sub = params.claims.sub;
|
|
2054
|
-
const ent = params.claims.ent;
|
|
2055
|
-
const aud = "backstage";
|
|
2056
|
-
const iat = Math.floor(Date.now() / MS_IN_S);
|
|
2057
|
-
const exp = iat + this.keyDurationSeconds;
|
|
2058
|
-
this.logger.info(`Issuing token for ${sub}, with entities ${ent != null ? ent : []}`);
|
|
2059
|
-
return jose.JWS.sign({ iss, sub, aud, iat, exp, ent }, key, {
|
|
2060
|
-
alg: key.alg,
|
|
2061
|
-
kid: key.kid
|
|
2062
|
-
});
|
|
2063
2022
|
}
|
|
2064
|
-
async
|
|
2065
|
-
const {
|
|
2066
|
-
const
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
expiredKeys.push(key);
|
|
2074
|
-
} else {
|
|
2075
|
-
validKeys.push(key);
|
|
2076
|
-
}
|
|
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;
|
|
2077
2032
|
}
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
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");
|
|
2084
2048
|
}
|
|
2085
|
-
|
|
2049
|
+
const userinfo = await client.userinfo(tokenset.access_token);
|
|
2050
|
+
return {
|
|
2051
|
+
response: await this.handleResult({ tokenset, userinfo }),
|
|
2052
|
+
refreshToken: tokenset.refresh_token
|
|
2053
|
+
};
|
|
2086
2054
|
}
|
|
2087
|
-
async
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
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");
|
|
2091
2072
|
}
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
}
|
|
2095
|
-
this.keyExpiry = luxon.DateTime.utc().plus({
|
|
2096
|
-
seconds: this.keyDurationSeconds
|
|
2097
|
-
}).toJSDate();
|
|
2098
|
-
const promise = (async () => {
|
|
2099
|
-
const key = await jose.JWK.generate("EC", "P-256", {
|
|
2100
|
-
use: "sig",
|
|
2101
|
-
kid: uuid.v4(),
|
|
2102
|
-
alg: "ES256"
|
|
2073
|
+
done(void 0, { tokenset, userinfo }, {
|
|
2074
|
+
refreshToken: tokenset.refresh_token
|
|
2103
2075
|
});
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
})();
|
|
2108
|
-
this.privateKeyPromise = promise;
|
|
2109
|
-
try {
|
|
2110
|
-
await promise;
|
|
2111
|
-
} catch (error) {
|
|
2112
|
-
this.logger.error(`Failed to generate new signing key, ${error}`);
|
|
2113
|
-
delete this.keyExpiry;
|
|
2114
|
-
delete this.privateKeyPromise;
|
|
2115
|
-
}
|
|
2116
|
-
return promise;
|
|
2117
|
-
}
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
const migrationsDir = backendCommon.resolvePackagePath("@backstage/plugin-auth-backend", "migrations");
|
|
2121
|
-
const TABLE = "signing_keys";
|
|
2122
|
-
const parseDate = (date) => {
|
|
2123
|
-
const parsedDate = typeof date === "string" ? luxon.DateTime.fromSQL(date, { zone: "UTC" }) : luxon.DateTime.fromJSDate(date);
|
|
2124
|
-
if (!parsedDate.isValid) {
|
|
2125
|
-
throw new Error(`Failed to parse date, reason: ${parsedDate.invalidReason}, explanation: ${parsedDate.invalidExplanation}`);
|
|
2126
|
-
}
|
|
2127
|
-
return parsedDate.toJSDate();
|
|
2128
|
-
};
|
|
2129
|
-
class DatabaseKeyStore {
|
|
2130
|
-
static async create(options) {
|
|
2131
|
-
const { database } = options;
|
|
2132
|
-
await database.migrate.latest({
|
|
2133
|
-
directory: migrationsDir
|
|
2134
|
-
});
|
|
2135
|
-
return new DatabaseKeyStore(options);
|
|
2136
|
-
}
|
|
2137
|
-
constructor(options) {
|
|
2138
|
-
this.database = options.database;
|
|
2139
|
-
}
|
|
2140
|
-
async addKey(key) {
|
|
2141
|
-
await this.database(TABLE).insert({
|
|
2142
|
-
kid: key.kid,
|
|
2143
|
-
key: JSON.stringify(key)
|
|
2144
|
-
});
|
|
2145
|
-
}
|
|
2146
|
-
async listKeys() {
|
|
2147
|
-
const rows = await this.database(TABLE).select();
|
|
2148
|
-
return {
|
|
2149
|
-
items: rows.map((row) => ({
|
|
2150
|
-
key: JSON.parse(row.key),
|
|
2151
|
-
createdAt: parseDate(row.created_at)
|
|
2152
|
-
}))
|
|
2153
|
-
};
|
|
2154
|
-
}
|
|
2155
|
-
async removeKeys(kids) {
|
|
2156
|
-
await this.database(TABLE).delete().whereIn("kid", kids);
|
|
2157
|
-
}
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
|
-
class MemoryKeyStore {
|
|
2161
|
-
constructor() {
|
|
2162
|
-
this.keys = /* @__PURE__ */ new Map();
|
|
2163
|
-
}
|
|
2164
|
-
async addKey(key) {
|
|
2165
|
-
this.keys.set(key.kid, {
|
|
2166
|
-
createdAt: luxon.DateTime.utc().toJSDate(),
|
|
2167
|
-
key: JSON.stringify(key)
|
|
2168
|
-
});
|
|
2169
|
-
}
|
|
2170
|
-
async removeKeys(kids) {
|
|
2171
|
-
for (const kid of kids) {
|
|
2172
|
-
this.keys.delete(kid);
|
|
2173
|
-
}
|
|
2174
|
-
}
|
|
2175
|
-
async listKeys() {
|
|
2176
|
-
return {
|
|
2177
|
-
items: Array.from(this.keys).map(([, { createdAt, key: keyStr }]) => ({
|
|
2178
|
-
createdAt,
|
|
2179
|
-
key: JSON.parse(keyStr)
|
|
2180
|
-
}))
|
|
2181
|
-
};
|
|
2182
|
-
}
|
|
2183
|
-
}
|
|
2184
|
-
|
|
2185
|
-
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
2186
|
-
const DEFAULT_DOCUMENT_PATH = "sessions";
|
|
2187
|
-
class FirestoreKeyStore {
|
|
2188
|
-
constructor(database, path, timeout) {
|
|
2189
|
-
this.database = database;
|
|
2190
|
-
this.path = path;
|
|
2191
|
-
this.timeout = timeout;
|
|
2192
|
-
}
|
|
2193
|
-
static async create(settings) {
|
|
2194
|
-
const { path, timeout, ...firestoreSettings } = settings != null ? settings : {};
|
|
2195
|
-
const database = new firestore.Firestore(firestoreSettings);
|
|
2196
|
-
return new FirestoreKeyStore(database, path != null ? path : DEFAULT_DOCUMENT_PATH, timeout != null ? timeout : DEFAULT_TIMEOUT_MS);
|
|
2197
|
-
}
|
|
2198
|
-
static async verifyConnection(keyStore, logger) {
|
|
2199
|
-
try {
|
|
2200
|
-
await keyStore.verify();
|
|
2201
|
-
} catch (error) {
|
|
2202
|
-
if (process.env.NODE_ENV !== "development") {
|
|
2203
|
-
throw new Error(`Failed to connect to database: ${error.message}`);
|
|
2204
|
-
}
|
|
2205
|
-
logger == null ? void 0 : logger.warn(`Failed to connect to database: ${error.message}`);
|
|
2206
|
-
}
|
|
2207
|
-
}
|
|
2208
|
-
async addKey(key) {
|
|
2209
|
-
await this.withTimeout(this.database.collection(this.path).doc(key.kid).set({
|
|
2210
|
-
kid: key.kid,
|
|
2211
|
-
key: JSON.stringify(key)
|
|
2212
|
-
}));
|
|
2213
|
-
}
|
|
2214
|
-
async listKeys() {
|
|
2215
|
-
const keys = await this.withTimeout(this.database.collection(this.path).get());
|
|
2216
|
-
return {
|
|
2217
|
-
items: keys.docs.map((key) => ({
|
|
2218
|
-
key: key.data(),
|
|
2219
|
-
createdAt: key.createTime.toDate()
|
|
2220
|
-
}))
|
|
2221
|
-
};
|
|
2222
|
-
}
|
|
2223
|
-
async removeKeys(kids) {
|
|
2224
|
-
for (const kid of kids) {
|
|
2225
|
-
await this.withTimeout(this.database.collection(this.path).doc(kid).delete());
|
|
2226
|
-
}
|
|
2227
|
-
}
|
|
2228
|
-
async withTimeout(operation) {
|
|
2229
|
-
const timer = new Promise((_, reject) => setTimeout(() => {
|
|
2230
|
-
reject(new Error(`Operation timed out after ${this.timeout}ms`));
|
|
2231
|
-
}, this.timeout));
|
|
2232
|
-
return Promise.race([operation, timer]);
|
|
2233
|
-
}
|
|
2234
|
-
async verify() {
|
|
2235
|
-
await this.withTimeout(this.database.collection(this.path).limit(1).get());
|
|
2236
|
-
}
|
|
2237
|
-
}
|
|
2238
|
-
|
|
2239
|
-
class KeyStores {
|
|
2240
|
-
static async fromConfig(config, options) {
|
|
2241
|
-
var _a;
|
|
2242
|
-
const { logger, database } = options != null ? options : {};
|
|
2243
|
-
const ks = config.getOptionalConfig("auth.keyStore");
|
|
2244
|
-
const provider = (_a = ks == null ? void 0 : ks.getOptionalString("provider")) != null ? _a : "database";
|
|
2245
|
-
logger == null ? void 0 : logger.info(`Configuring "${provider}" as KeyStore provider`);
|
|
2246
|
-
if (provider === "database") {
|
|
2247
|
-
if (!database) {
|
|
2248
|
-
throw new Error("This KeyStore provider requires a database");
|
|
2249
|
-
}
|
|
2250
|
-
return await DatabaseKeyStore.create({
|
|
2251
|
-
database: await database.getClient()
|
|
2252
|
-
});
|
|
2253
|
-
}
|
|
2254
|
-
if (provider === "memory") {
|
|
2255
|
-
return new MemoryKeyStore();
|
|
2256
|
-
}
|
|
2257
|
-
if (provider === "firestore") {
|
|
2258
|
-
const settings = ks == null ? void 0 : ks.getConfig(provider);
|
|
2259
|
-
const keyStore = await FirestoreKeyStore.create(lodash.pickBy({
|
|
2260
|
-
projectId: settings == null ? void 0 : settings.getOptionalString("projectId"),
|
|
2261
|
-
keyFilename: settings == null ? void 0 : settings.getOptionalString("keyFilename"),
|
|
2262
|
-
host: settings == null ? void 0 : settings.getOptionalString("host"),
|
|
2263
|
-
port: settings == null ? void 0 : settings.getOptionalNumber("port"),
|
|
2264
|
-
ssl: settings == null ? void 0 : settings.getOptionalBoolean("ssl"),
|
|
2265
|
-
path: settings == null ? void 0 : settings.getOptionalString("path"),
|
|
2266
|
-
timeout: settings == null ? void 0 : settings.getOptionalNumber("timeout")
|
|
2267
|
-
}, (value) => value !== void 0));
|
|
2268
|
-
await FirestoreKeyStore.verifyConnection(keyStore, logger);
|
|
2269
|
-
return keyStore;
|
|
2270
|
-
}
|
|
2271
|
-
throw new Error(`Unknown KeyStore provider: ${provider}`);
|
|
2272
|
-
}
|
|
2273
|
-
}
|
|
2274
|
-
|
|
2275
|
-
const OAUTH2_PROXY_JWT_HEADER = "X-OAUTH2-PROXY-ID-TOKEN";
|
|
2276
|
-
class Oauth2ProxyAuthProvider {
|
|
2277
|
-
constructor(options) {
|
|
2278
|
-
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
2279
|
-
this.logger = options.logger;
|
|
2280
|
-
this.tokenIssuer = options.tokenIssuer;
|
|
2281
|
-
this.signInResolver = options.signInResolver;
|
|
2282
|
-
this.authHandler = options.authHandler;
|
|
2283
|
-
}
|
|
2284
|
-
frameHandler() {
|
|
2285
|
-
return Promise.resolve(void 0);
|
|
2286
|
-
}
|
|
2287
|
-
async refresh(req, res) {
|
|
2288
|
-
try {
|
|
2289
|
-
const result = this.getResult(req);
|
|
2290
|
-
const response = await this.handleResult(result);
|
|
2291
|
-
res.json(response);
|
|
2292
|
-
} catch (e) {
|
|
2293
|
-
this.logger.error(`Exception occurred during ${OAUTH2_PROXY_JWT_HEADER} refresh`, e);
|
|
2294
|
-
res.status(401);
|
|
2295
|
-
res.end();
|
|
2296
|
-
}
|
|
2297
|
-
}
|
|
2298
|
-
start() {
|
|
2299
|
-
return Promise.resolve(void 0);
|
|
2300
|
-
}
|
|
2301
|
-
async handleResult(result) {
|
|
2302
|
-
const ctx = {
|
|
2303
|
-
logger: this.logger,
|
|
2304
|
-
tokenIssuer: this.tokenIssuer,
|
|
2305
|
-
catalogIdentityClient: this.catalogIdentityClient
|
|
2306
|
-
};
|
|
2307
|
-
const { profile } = await this.authHandler(result, ctx);
|
|
2308
|
-
const backstageSignInResult = await this.signInResolver({
|
|
2309
|
-
result,
|
|
2310
|
-
profile
|
|
2311
|
-
}, ctx);
|
|
2312
|
-
return {
|
|
2313
|
-
providerInfo: {
|
|
2314
|
-
accessToken: result.accessToken
|
|
2315
|
-
},
|
|
2316
|
-
backstageIdentity: prepareBackstageIdentityResponse(backstageSignInResult),
|
|
2317
|
-
profile
|
|
2318
|
-
};
|
|
2319
|
-
}
|
|
2320
|
-
getResult(req) {
|
|
2321
|
-
const authHeader = req.header(OAUTH2_PROXY_JWT_HEADER);
|
|
2322
|
-
const jwt = IdentityClient.getBearerToken(authHeader);
|
|
2323
|
-
if (!jwt) {
|
|
2324
|
-
throw new errors.AuthenticationError(`Missing or in incorrect format - Oauth2Proxy OIDC header: ${OAUTH2_PROXY_JWT_HEADER}`);
|
|
2325
|
-
}
|
|
2326
|
-
const decodedJWT = jose.JWT.decode(jwt);
|
|
2327
|
-
return {
|
|
2328
|
-
fullProfile: decodedJWT,
|
|
2329
|
-
accessToken: jwt
|
|
2330
|
-
};
|
|
2331
|
-
}
|
|
2332
|
-
}
|
|
2333
|
-
const createOauth2ProxyProvider = (options) => ({ catalogApi, logger, tokenIssuer, tokenManager }) => {
|
|
2334
|
-
const signInResolver = options.signIn.resolver;
|
|
2335
|
-
const authHandler = options.authHandler;
|
|
2336
|
-
const catalogIdentityClient = new CatalogIdentityClient({
|
|
2337
|
-
catalogApi,
|
|
2338
|
-
tokenManager
|
|
2339
|
-
});
|
|
2340
|
-
return new Oauth2ProxyAuthProvider({
|
|
2341
|
-
logger,
|
|
2342
|
-
signInResolver,
|
|
2343
|
-
authHandler,
|
|
2344
|
-
tokenIssuer,
|
|
2345
|
-
catalogIdentityClient
|
|
2346
|
-
});
|
|
2347
|
-
};
|
|
2348
|
-
|
|
2349
|
-
class OidcAuthProvider {
|
|
2350
|
-
constructor(options) {
|
|
2351
|
-
this.implementation = this.setupStrategy(options);
|
|
2352
|
-
this.scope = options.scope;
|
|
2353
|
-
this.prompt = options.prompt;
|
|
2354
|
-
this.signInResolver = options.signInResolver;
|
|
2355
|
-
this.authHandler = options.authHandler;
|
|
2356
|
-
this.tokenIssuer = options.tokenIssuer;
|
|
2357
|
-
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
2358
|
-
this.logger = options.logger;
|
|
2359
|
-
}
|
|
2360
|
-
async start(req) {
|
|
2361
|
-
const { strategy } = await this.implementation;
|
|
2362
|
-
const options = {
|
|
2363
|
-
scope: req.scope || this.scope || "openid profile email",
|
|
2364
|
-
state: encodeState(req.state)
|
|
2365
|
-
};
|
|
2366
|
-
const prompt = this.prompt || "none";
|
|
2367
|
-
if (prompt !== "auto") {
|
|
2368
|
-
options.prompt = prompt;
|
|
2369
|
-
}
|
|
2370
|
-
return await executeRedirectStrategy(req, strategy, options);
|
|
2371
|
-
}
|
|
2372
|
-
async handler(req) {
|
|
2373
|
-
const { strategy } = await this.implementation;
|
|
2374
|
-
const { result, privateInfo } = await executeFrameHandlerStrategy(req, strategy);
|
|
2375
|
-
return {
|
|
2376
|
-
response: await this.handleResult(result),
|
|
2377
|
-
refreshToken: privateInfo.refreshToken
|
|
2378
|
-
};
|
|
2379
|
-
}
|
|
2380
|
-
async refresh(req) {
|
|
2381
|
-
const { client } = await this.implementation;
|
|
2382
|
-
const tokenset = await client.refresh(req.refreshToken);
|
|
2383
|
-
if (!tokenset.access_token) {
|
|
2384
|
-
throw new Error("Refresh failed");
|
|
2385
|
-
}
|
|
2386
|
-
const userinfo = await client.userinfo(tokenset.access_token);
|
|
2387
|
-
return {
|
|
2388
|
-
response: await this.handleResult({ tokenset, userinfo }),
|
|
2389
|
-
refreshToken: tokenset.refresh_token
|
|
2390
|
-
};
|
|
2391
|
-
}
|
|
2392
|
-
async setupStrategy(options) {
|
|
2393
|
-
const issuer = await openidClient.Issuer.discover(options.metadataUrl);
|
|
2394
|
-
const client = new issuer.Client({
|
|
2395
|
-
access_type: "offline",
|
|
2396
|
-
client_id: options.clientId,
|
|
2397
|
-
client_secret: options.clientSecret,
|
|
2398
|
-
redirect_uris: [options.callbackUrl],
|
|
2399
|
-
response_types: ["code"],
|
|
2400
|
-
id_token_signed_response_alg: options.tokenSignedResponseAlg || "RS256",
|
|
2401
|
-
scope: options.scope || ""
|
|
2402
|
-
});
|
|
2403
|
-
const strategy = new openidClient.Strategy({
|
|
2404
|
-
client,
|
|
2405
|
-
passReqToCallback: false
|
|
2406
|
-
}, (tokenset, userinfo, done) => {
|
|
2407
|
-
if (typeof done !== "function") {
|
|
2408
|
-
throw new Error("OIDC IdP must provide a userinfo_endpoint in the metadata response");
|
|
2409
|
-
}
|
|
2410
|
-
done(void 0, { tokenset, userinfo }, {
|
|
2411
|
-
refreshToken: tokenset.refresh_token
|
|
2412
|
-
});
|
|
2413
|
-
});
|
|
2414
|
-
strategy.error = console.error;
|
|
2415
|
-
return { strategy, client };
|
|
2076
|
+
});
|
|
2077
|
+
strategy.error = console.error;
|
|
2078
|
+
return { strategy, client };
|
|
2416
2079
|
}
|
|
2417
2080
|
async handleResult(result) {
|
|
2418
2081
|
const context = {
|
|
@@ -2734,292 +2397,557 @@ class OneLoginProvider {
|
|
|
2734
2397
|
};
|
|
2735
2398
|
const { profile } = await this.authHandler(result, context);
|
|
2736
2399
|
const response = {
|
|
2737
|
-
providerInfo: {
|
|
2738
|
-
idToken: result.params.id_token,
|
|
2739
|
-
accessToken: result.accessToken,
|
|
2740
|
-
scope: result.params.scope,
|
|
2741
|
-
expiresInSeconds: result.params.expires_in
|
|
2742
|
-
},
|
|
2743
|
-
profile
|
|
2400
|
+
providerInfo: {
|
|
2401
|
+
idToken: result.params.id_token,
|
|
2402
|
+
accessToken: result.accessToken,
|
|
2403
|
+
scope: result.params.scope,
|
|
2404
|
+
expiresInSeconds: result.params.expires_in
|
|
2405
|
+
},
|
|
2406
|
+
profile
|
|
2407
|
+
};
|
|
2408
|
+
if (this.signInResolver) {
|
|
2409
|
+
response.backstageIdentity = await this.signInResolver({
|
|
2410
|
+
result,
|
|
2411
|
+
profile
|
|
2412
|
+
}, context);
|
|
2413
|
+
}
|
|
2414
|
+
return response;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
const defaultSignInResolver = async (info) => {
|
|
2418
|
+
const { profile } = info;
|
|
2419
|
+
if (!profile.email) {
|
|
2420
|
+
throw new Error("OIDC profile contained no email");
|
|
2421
|
+
}
|
|
2422
|
+
const id = profile.email.split("@")[0];
|
|
2423
|
+
return { id, token: "" };
|
|
2424
|
+
};
|
|
2425
|
+
const createOneLoginProvider = (options) => {
|
|
2426
|
+
return ({
|
|
2427
|
+
providerId,
|
|
2428
|
+
globalConfig,
|
|
2429
|
+
config,
|
|
2430
|
+
tokenIssuer,
|
|
2431
|
+
tokenManager,
|
|
2432
|
+
catalogApi,
|
|
2433
|
+
logger
|
|
2434
|
+
}) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
|
|
2435
|
+
var _a, _b;
|
|
2436
|
+
const clientId = envConfig.getString("clientId");
|
|
2437
|
+
const clientSecret = envConfig.getString("clientSecret");
|
|
2438
|
+
const issuer = envConfig.getString("issuer");
|
|
2439
|
+
const customCallbackUrl = envConfig.getOptionalString("callbackUrl");
|
|
2440
|
+
const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
2441
|
+
const catalogIdentityClient = new CatalogIdentityClient({
|
|
2442
|
+
catalogApi,
|
|
2443
|
+
tokenManager
|
|
2444
|
+
});
|
|
2445
|
+
const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ fullProfile, params }) => ({
|
|
2446
|
+
profile: makeProfileInfo(fullProfile, params.id_token)
|
|
2447
|
+
});
|
|
2448
|
+
const signInResolver = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : defaultSignInResolver;
|
|
2449
|
+
const provider = new OneLoginProvider({
|
|
2450
|
+
clientId,
|
|
2451
|
+
clientSecret,
|
|
2452
|
+
callbackUrl,
|
|
2453
|
+
issuer,
|
|
2454
|
+
authHandler,
|
|
2455
|
+
signInResolver,
|
|
2456
|
+
tokenIssuer,
|
|
2457
|
+
catalogIdentityClient,
|
|
2458
|
+
logger
|
|
2459
|
+
});
|
|
2460
|
+
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
2461
|
+
disableRefresh: false,
|
|
2462
|
+
providerId,
|
|
2463
|
+
tokenIssuer,
|
|
2464
|
+
callbackUrl
|
|
2465
|
+
});
|
|
2466
|
+
});
|
|
2467
|
+
};
|
|
2468
|
+
|
|
2469
|
+
class SamlAuthProvider {
|
|
2470
|
+
constructor(options) {
|
|
2471
|
+
this.appUrl = options.appUrl;
|
|
2472
|
+
this.signInResolver = options.signInResolver;
|
|
2473
|
+
this.authHandler = options.authHandler;
|
|
2474
|
+
this.tokenIssuer = options.tokenIssuer;
|
|
2475
|
+
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
2476
|
+
this.logger = options.logger;
|
|
2477
|
+
this.strategy = new passportSaml.Strategy({ ...options }, (fullProfile, done) => {
|
|
2478
|
+
done(void 0, { fullProfile });
|
|
2479
|
+
});
|
|
2480
|
+
}
|
|
2481
|
+
async start(req, res) {
|
|
2482
|
+
const { url } = await executeRedirectStrategy(req, this.strategy, {});
|
|
2483
|
+
res.redirect(url);
|
|
2484
|
+
}
|
|
2485
|
+
async frameHandler(req, res) {
|
|
2486
|
+
try {
|
|
2487
|
+
const context = {
|
|
2488
|
+
logger: this.logger,
|
|
2489
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
2490
|
+
tokenIssuer: this.tokenIssuer
|
|
2491
|
+
};
|
|
2492
|
+
const { result } = await executeFrameHandlerStrategy(req, this.strategy);
|
|
2493
|
+
const { profile } = await this.authHandler(result, context);
|
|
2494
|
+
const response = {
|
|
2495
|
+
profile,
|
|
2496
|
+
providerInfo: {}
|
|
2497
|
+
};
|
|
2498
|
+
if (this.signInResolver) {
|
|
2499
|
+
const signInResponse = await this.signInResolver({
|
|
2500
|
+
result,
|
|
2501
|
+
profile
|
|
2502
|
+
}, context);
|
|
2503
|
+
response.backstageIdentity = prepareBackstageIdentityResponse(signInResponse);
|
|
2504
|
+
}
|
|
2505
|
+
return postMessageResponse(res, this.appUrl, {
|
|
2506
|
+
type: "authorization_response",
|
|
2507
|
+
response
|
|
2508
|
+
});
|
|
2509
|
+
} catch (error) {
|
|
2510
|
+
const { name, message } = errors.isError(error) ? error : new Error("Encountered invalid error");
|
|
2511
|
+
return postMessageResponse(res, this.appUrl, {
|
|
2512
|
+
type: "authorization_response",
|
|
2513
|
+
error: { name, message }
|
|
2514
|
+
});
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
async logout(_req, res) {
|
|
2518
|
+
res.end();
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
const samlDefaultSignInResolver = async (info, ctx) => {
|
|
2522
|
+
const id = info.result.fullProfile.nameID;
|
|
2523
|
+
const token = await ctx.tokenIssuer.issueToken({
|
|
2524
|
+
claims: { sub: id }
|
|
2525
|
+
});
|
|
2526
|
+
return { id, token };
|
|
2527
|
+
};
|
|
2528
|
+
const createSamlProvider = (options) => {
|
|
2529
|
+
return ({
|
|
2530
|
+
providerId,
|
|
2531
|
+
globalConfig,
|
|
2532
|
+
config,
|
|
2533
|
+
tokenIssuer,
|
|
2534
|
+
tokenManager,
|
|
2535
|
+
catalogApi,
|
|
2536
|
+
logger
|
|
2537
|
+
}) => {
|
|
2538
|
+
var _a, _b;
|
|
2539
|
+
const catalogIdentityClient = new CatalogIdentityClient({
|
|
2540
|
+
catalogApi,
|
|
2541
|
+
tokenManager
|
|
2542
|
+
});
|
|
2543
|
+
const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ fullProfile }) => ({
|
|
2544
|
+
profile: {
|
|
2545
|
+
email: fullProfile.email,
|
|
2546
|
+
displayName: fullProfile.displayName
|
|
2547
|
+
}
|
|
2548
|
+
});
|
|
2549
|
+
const signInResolverFn = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : samlDefaultSignInResolver;
|
|
2550
|
+
const signInResolver = (info) => signInResolverFn(info, {
|
|
2551
|
+
catalogIdentityClient,
|
|
2552
|
+
tokenIssuer,
|
|
2553
|
+
logger
|
|
2554
|
+
});
|
|
2555
|
+
return new SamlAuthProvider({
|
|
2556
|
+
callbackUrl: `${globalConfig.baseUrl}/${providerId}/handler/frame`,
|
|
2557
|
+
entryPoint: config.getString("entryPoint"),
|
|
2558
|
+
logoutUrl: config.getOptionalString("logoutUrl"),
|
|
2559
|
+
audience: config.getOptionalString("audience"),
|
|
2560
|
+
issuer: config.getString("issuer"),
|
|
2561
|
+
cert: config.getString("cert"),
|
|
2562
|
+
privateKey: config.getOptionalString("privateKey"),
|
|
2563
|
+
authnContext: config.getOptionalStringArray("authnContext"),
|
|
2564
|
+
identifierFormat: config.getOptionalString("identifierFormat"),
|
|
2565
|
+
decryptionPvk: config.getOptionalString("decryptionPvk"),
|
|
2566
|
+
signatureAlgorithm: config.getOptionalString("signatureAlgorithm"),
|
|
2567
|
+
digestAlgorithm: config.getOptionalString("digestAlgorithm"),
|
|
2568
|
+
acceptedClockSkewMs: config.getOptionalNumber("acceptedClockSkewMs"),
|
|
2569
|
+
tokenIssuer,
|
|
2570
|
+
appUrl: globalConfig.appUrl,
|
|
2571
|
+
authHandler,
|
|
2572
|
+
signInResolver,
|
|
2573
|
+
logger,
|
|
2574
|
+
catalogIdentityClient
|
|
2575
|
+
});
|
|
2576
|
+
};
|
|
2577
|
+
};
|
|
2578
|
+
|
|
2579
|
+
const IAP_JWT_HEADER = "x-goog-iap-jwt-assertion";
|
|
2580
|
+
|
|
2581
|
+
function createTokenValidator(audience, mockClient) {
|
|
2582
|
+
const client = mockClient != null ? mockClient : new googleAuthLibrary.OAuth2Client();
|
|
2583
|
+
return async function tokenValidator(token) {
|
|
2584
|
+
const response = await client.getIapPublicKeys();
|
|
2585
|
+
const ticket = await client.verifySignedJwtWithCertsAsync(token, response.pubkeys, audience, ["https://cloud.google.com/iap"]);
|
|
2586
|
+
const payload = ticket.getPayload();
|
|
2587
|
+
if (!payload) {
|
|
2588
|
+
throw new TypeError("Token had no payload");
|
|
2589
|
+
}
|
|
2590
|
+
return payload;
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
async function parseRequestToken(jwtToken, tokenValidator) {
|
|
2594
|
+
if (typeof jwtToken !== "string" || !jwtToken) {
|
|
2595
|
+
throw new errors.AuthenticationError(`Missing Google IAP header: ${IAP_JWT_HEADER}`);
|
|
2596
|
+
}
|
|
2597
|
+
let payload;
|
|
2598
|
+
try {
|
|
2599
|
+
payload = await tokenValidator(jwtToken);
|
|
2600
|
+
} catch (e) {
|
|
2601
|
+
throw new errors.AuthenticationError(`Google IAP token verification failed, ${e}`);
|
|
2602
|
+
}
|
|
2603
|
+
if (!payload.sub || !payload.email) {
|
|
2604
|
+
throw new errors.AuthenticationError("Google IAP token payload is missing sub and/or email claim");
|
|
2605
|
+
}
|
|
2606
|
+
return {
|
|
2607
|
+
iapToken: {
|
|
2608
|
+
...payload,
|
|
2609
|
+
sub: payload.sub,
|
|
2610
|
+
email: payload.email
|
|
2611
|
+
}
|
|
2612
|
+
};
|
|
2613
|
+
}
|
|
2614
|
+
const defaultAuthHandler = async ({
|
|
2615
|
+
iapToken
|
|
2616
|
+
}) => ({ profile: { email: iapToken.email } });
|
|
2617
|
+
|
|
2618
|
+
class GcpIapProvider {
|
|
2619
|
+
constructor(options) {
|
|
2620
|
+
this.authHandler = options.authHandler;
|
|
2621
|
+
this.signInResolver = options.signInResolver;
|
|
2622
|
+
this.tokenValidator = options.tokenValidator;
|
|
2623
|
+
this.tokenIssuer = options.tokenIssuer;
|
|
2624
|
+
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
2625
|
+
this.logger = options.logger;
|
|
2626
|
+
}
|
|
2627
|
+
async start() {
|
|
2628
|
+
}
|
|
2629
|
+
async frameHandler() {
|
|
2630
|
+
}
|
|
2631
|
+
async refresh(req, res) {
|
|
2632
|
+
const result = await parseRequestToken(req.header(IAP_JWT_HEADER), this.tokenValidator);
|
|
2633
|
+
const context = {
|
|
2634
|
+
logger: this.logger,
|
|
2635
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
2636
|
+
tokenIssuer: this.tokenIssuer
|
|
2637
|
+
};
|
|
2638
|
+
const { profile } = await this.authHandler(result, context);
|
|
2639
|
+
const backstageIdentity = await this.signInResolver({ profile, result }, context);
|
|
2640
|
+
const response = {
|
|
2641
|
+
providerInfo: { iapToken: result.iapToken },
|
|
2642
|
+
profile,
|
|
2643
|
+
backstageIdentity: prepareBackstageIdentityResponse(backstageIdentity)
|
|
2744
2644
|
};
|
|
2745
|
-
|
|
2746
|
-
response.backstageIdentity = await this.signInResolver({
|
|
2747
|
-
result,
|
|
2748
|
-
profile
|
|
2749
|
-
}, context);
|
|
2750
|
-
}
|
|
2751
|
-
return response;
|
|
2645
|
+
res.json(response);
|
|
2752
2646
|
}
|
|
2753
2647
|
}
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
};
|
|
2762
|
-
const createOneLoginProvider = (options) => {
|
|
2763
|
-
return ({
|
|
2764
|
-
providerId,
|
|
2765
|
-
globalConfig,
|
|
2766
|
-
config,
|
|
2767
|
-
tokenIssuer,
|
|
2768
|
-
tokenManager,
|
|
2769
|
-
catalogApi,
|
|
2770
|
-
logger
|
|
2771
|
-
}) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
|
|
2772
|
-
var _a, _b;
|
|
2773
|
-
const clientId = envConfig.getString("clientId");
|
|
2774
|
-
const clientSecret = envConfig.getString("clientSecret");
|
|
2775
|
-
const issuer = envConfig.getString("issuer");
|
|
2776
|
-
const customCallbackUrl = envConfig.getOptionalString("callbackUrl");
|
|
2777
|
-
const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
2648
|
+
function createGcpIapProvider(options) {
|
|
2649
|
+
return ({ config, tokenIssuer, catalogApi, logger, tokenManager }) => {
|
|
2650
|
+
var _a;
|
|
2651
|
+
const audience = config.getString("audience");
|
|
2652
|
+
const authHandler = (_a = options.authHandler) != null ? _a : defaultAuthHandler;
|
|
2653
|
+
const signInResolver = options.signIn.resolver;
|
|
2654
|
+
const tokenValidator = createTokenValidator(audience);
|
|
2778
2655
|
const catalogIdentityClient = new CatalogIdentityClient({
|
|
2779
2656
|
catalogApi,
|
|
2780
2657
|
tokenManager
|
|
2781
2658
|
});
|
|
2782
|
-
|
|
2783
|
-
profile: makeProfileInfo(fullProfile, params.id_token)
|
|
2784
|
-
});
|
|
2785
|
-
const signInResolver = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : defaultSignInResolver;
|
|
2786
|
-
const provider = new OneLoginProvider({
|
|
2787
|
-
clientId,
|
|
2788
|
-
clientSecret,
|
|
2789
|
-
callbackUrl,
|
|
2790
|
-
issuer,
|
|
2659
|
+
return new GcpIapProvider({
|
|
2791
2660
|
authHandler,
|
|
2792
2661
|
signInResolver,
|
|
2662
|
+
tokenValidator,
|
|
2793
2663
|
tokenIssuer,
|
|
2794
2664
|
catalogIdentityClient,
|
|
2795
2665
|
logger
|
|
2796
2666
|
});
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
const factories = {
|
|
2671
|
+
google: createGoogleProvider(),
|
|
2672
|
+
github: createGithubProvider(),
|
|
2673
|
+
gitlab: createGitlabProvider(),
|
|
2674
|
+
saml: createSamlProvider(),
|
|
2675
|
+
okta: createOktaProvider(),
|
|
2676
|
+
auth0: createAuth0Provider(),
|
|
2677
|
+
microsoft: createMicrosoftProvider(),
|
|
2678
|
+
oauth2: createOAuth2Provider(),
|
|
2679
|
+
oidc: createOidcProvider(),
|
|
2680
|
+
onelogin: createOneLoginProvider(),
|
|
2681
|
+
awsalb: createAwsAlbProvider(),
|
|
2682
|
+
bitbucket: createBitbucketProvider(),
|
|
2683
|
+
atlassian: createAtlassianProvider()
|
|
2804
2684
|
};
|
|
2805
2685
|
|
|
2806
|
-
|
|
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 {
|
|
2807
2720
|
constructor(options) {
|
|
2808
|
-
this.
|
|
2809
|
-
this.signInResolver = options.signInResolver;
|
|
2810
|
-
this.authHandler = options.authHandler;
|
|
2811
|
-
this.tokenIssuer = options.tokenIssuer;
|
|
2812
|
-
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
2721
|
+
this.issuer = options.issuer;
|
|
2813
2722
|
this.logger = options.logger;
|
|
2814
|
-
this.
|
|
2815
|
-
|
|
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
|
|
2816
2738
|
});
|
|
2817
2739
|
}
|
|
2818
|
-
async
|
|
2819
|
-
const {
|
|
2820
|
-
|
|
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) };
|
|
2821
2762
|
}
|
|
2822
|
-
async
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
catalogIdentityClient: this.catalogIdentityClient,
|
|
2827
|
-
tokenIssuer: this.tokenIssuer
|
|
2828
|
-
};
|
|
2829
|
-
const { result } = await executeFrameHandlerStrategy(req, this.strategy);
|
|
2830
|
-
const { profile } = await this.authHandler(result, context);
|
|
2831
|
-
const response = {
|
|
2832
|
-
profile,
|
|
2833
|
-
providerInfo: {}
|
|
2834
|
-
};
|
|
2835
|
-
if (this.signInResolver) {
|
|
2836
|
-
const signInResponse = await this.signInResolver({
|
|
2837
|
-
result,
|
|
2838
|
-
profile
|
|
2839
|
-
}, context);
|
|
2840
|
-
response.backstageIdentity = prepareBackstageIdentityResponse(signInResponse);
|
|
2763
|
+
async getKey() {
|
|
2764
|
+
if (this.privateKeyPromise) {
|
|
2765
|
+
if (this.keyExpiry && luxon.DateTime.fromJSDate(this.keyExpiry) > luxon.DateTime.local()) {
|
|
2766
|
+
return this.privateKeyPromise;
|
|
2841
2767
|
}
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
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"
|
|
2845
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;
|
|
2846
2787
|
} catch (error) {
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
error: { name, message }
|
|
2851
|
-
});
|
|
2788
|
+
this.logger.error(`Failed to generate new signing key, ${error}`);
|
|
2789
|
+
delete this.keyExpiry;
|
|
2790
|
+
delete this.privateKeyPromise;
|
|
2852
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
|
+
};
|
|
2853
2830
|
}
|
|
2854
|
-
async
|
|
2855
|
-
|
|
2831
|
+
async removeKeys(kids) {
|
|
2832
|
+
await this.database(TABLE).delete().whereIn("kid", kids);
|
|
2856
2833
|
}
|
|
2857
2834
|
}
|
|
2858
|
-
const samlDefaultSignInResolver = async (info, ctx) => {
|
|
2859
|
-
const id = info.result.fullProfile.nameID;
|
|
2860
|
-
const token = await ctx.tokenIssuer.issueToken({
|
|
2861
|
-
claims: { sub: id }
|
|
2862
|
-
});
|
|
2863
|
-
return { id, token };
|
|
2864
|
-
};
|
|
2865
|
-
const createSamlProvider = (options) => {
|
|
2866
|
-
return ({
|
|
2867
|
-
providerId,
|
|
2868
|
-
globalConfig,
|
|
2869
|
-
config,
|
|
2870
|
-
tokenIssuer,
|
|
2871
|
-
tokenManager,
|
|
2872
|
-
catalogApi,
|
|
2873
|
-
logger
|
|
2874
|
-
}) => {
|
|
2875
|
-
var _a, _b;
|
|
2876
|
-
const catalogIdentityClient = new CatalogIdentityClient({
|
|
2877
|
-
catalogApi,
|
|
2878
|
-
tokenManager
|
|
2879
|
-
});
|
|
2880
|
-
const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ fullProfile }) => ({
|
|
2881
|
-
profile: {
|
|
2882
|
-
email: fullProfile.email,
|
|
2883
|
-
displayName: fullProfile.displayName
|
|
2884
|
-
}
|
|
2885
|
-
});
|
|
2886
|
-
const signInResolverFn = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : samlDefaultSignInResolver;
|
|
2887
|
-
const signInResolver = (info) => signInResolverFn(info, {
|
|
2888
|
-
catalogIdentityClient,
|
|
2889
|
-
tokenIssuer,
|
|
2890
|
-
logger
|
|
2891
|
-
});
|
|
2892
|
-
return new SamlAuthProvider({
|
|
2893
|
-
callbackUrl: `${globalConfig.baseUrl}/${providerId}/handler/frame`,
|
|
2894
|
-
entryPoint: config.getString("entryPoint"),
|
|
2895
|
-
logoutUrl: config.getOptionalString("logoutUrl"),
|
|
2896
|
-
audience: config.getOptionalString("audience"),
|
|
2897
|
-
issuer: config.getString("issuer"),
|
|
2898
|
-
cert: config.getString("cert"),
|
|
2899
|
-
privateKey: config.getOptionalString("privateKey"),
|
|
2900
|
-
authnContext: config.getOptionalStringArray("authnContext"),
|
|
2901
|
-
identifierFormat: config.getOptionalString("identifierFormat"),
|
|
2902
|
-
decryptionPvk: config.getOptionalString("decryptionPvk"),
|
|
2903
|
-
signatureAlgorithm: config.getOptionalString("signatureAlgorithm"),
|
|
2904
|
-
digestAlgorithm: config.getOptionalString("digestAlgorithm"),
|
|
2905
|
-
acceptedClockSkewMs: config.getOptionalNumber("acceptedClockSkewMs"),
|
|
2906
|
-
tokenIssuer,
|
|
2907
|
-
appUrl: globalConfig.appUrl,
|
|
2908
|
-
authHandler,
|
|
2909
|
-
signInResolver,
|
|
2910
|
-
logger,
|
|
2911
|
-
catalogIdentityClient
|
|
2912
|
-
});
|
|
2913
|
-
};
|
|
2914
|
-
};
|
|
2915
|
-
|
|
2916
|
-
const IAP_JWT_HEADER = "x-goog-iap-jwt-assertion";
|
|
2917
2835
|
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
const response = await client.getIapPublicKeys();
|
|
2922
|
-
const ticket = await client.verifySignedJwtWithCertsAsync(token, response.pubkeys, audience, ["https://cloud.google.com/iap"]);
|
|
2923
|
-
const payload = ticket.getPayload();
|
|
2924
|
-
if (!payload) {
|
|
2925
|
-
throw new TypeError("Token had no payload");
|
|
2926
|
-
}
|
|
2927
|
-
return payload;
|
|
2928
|
-
};
|
|
2929
|
-
}
|
|
2930
|
-
async function parseRequestToken(jwtToken, tokenValidator) {
|
|
2931
|
-
if (typeof jwtToken !== "string" || !jwtToken) {
|
|
2932
|
-
throw new errors.AuthenticationError(`Missing Google IAP header: ${IAP_JWT_HEADER}`);
|
|
2933
|
-
}
|
|
2934
|
-
let payload;
|
|
2935
|
-
try {
|
|
2936
|
-
payload = await tokenValidator(jwtToken);
|
|
2937
|
-
} catch (e) {
|
|
2938
|
-
throw new errors.AuthenticationError(`Google IAP token verification failed, ${e}`);
|
|
2836
|
+
class MemoryKeyStore {
|
|
2837
|
+
constructor() {
|
|
2838
|
+
this.keys = /* @__PURE__ */ new Map();
|
|
2939
2839
|
}
|
|
2940
|
-
|
|
2941
|
-
|
|
2840
|
+
async addKey(key) {
|
|
2841
|
+
this.keys.set(key.kid, {
|
|
2842
|
+
createdAt: luxon.DateTime.utc().toJSDate(),
|
|
2843
|
+
key: JSON.stringify(key)
|
|
2844
|
+
});
|
|
2942
2845
|
}
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
sub: payload.sub,
|
|
2947
|
-
email: payload.email
|
|
2846
|
+
async removeKeys(kids) {
|
|
2847
|
+
for (const kid of kids) {
|
|
2848
|
+
this.keys.delete(kid);
|
|
2948
2849
|
}
|
|
2949
|
-
}
|
|
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
|
+
}
|
|
2950
2859
|
}
|
|
2951
|
-
const defaultAuthHandler = async ({
|
|
2952
|
-
iapToken
|
|
2953
|
-
}) => ({ profile: { email: iapToken.email } });
|
|
2954
2860
|
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
this.
|
|
2960
|
-
this.
|
|
2961
|
-
this.
|
|
2962
|
-
this.logger = options.logger;
|
|
2861
|
+
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
2862
|
+
const DEFAULT_DOCUMENT_PATH = "sessions";
|
|
2863
|
+
class FirestoreKeyStore {
|
|
2864
|
+
constructor(database, path, timeout) {
|
|
2865
|
+
this.database = database;
|
|
2866
|
+
this.path = path;
|
|
2867
|
+
this.timeout = timeout;
|
|
2963
2868
|
}
|
|
2964
|
-
async
|
|
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);
|
|
2965
2873
|
}
|
|
2966
|
-
async
|
|
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
|
+
}
|
|
2967
2883
|
}
|
|
2968
|
-
async
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
const
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
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
|
+
}))
|
|
2981
2897
|
};
|
|
2982
|
-
|
|
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());
|
|
2983
2912
|
}
|
|
2984
2913
|
}
|
|
2985
|
-
|
|
2986
|
-
|
|
2914
|
+
|
|
2915
|
+
class KeyStores {
|
|
2916
|
+
static async fromConfig(config, options) {
|
|
2987
2917
|
var _a;
|
|
2988
|
-
const
|
|
2989
|
-
const
|
|
2990
|
-
const
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
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
|
+
}
|
|
3005
2949
|
}
|
|
3006
2950
|
|
|
3007
|
-
const factories = {
|
|
3008
|
-
google: createGoogleProvider(),
|
|
3009
|
-
github: createGithubProvider(),
|
|
3010
|
-
gitlab: createGitlabProvider(),
|
|
3011
|
-
saml: createSamlProvider(),
|
|
3012
|
-
okta: createOktaProvider(),
|
|
3013
|
-
auth0: createAuth0Provider(),
|
|
3014
|
-
microsoft: createMicrosoftProvider(),
|
|
3015
|
-
oauth2: createOAuth2Provider(),
|
|
3016
|
-
oidc: createOidcProvider(),
|
|
3017
|
-
onelogin: createOneLoginProvider(),
|
|
3018
|
-
awsalb: createAwsAlbProvider(),
|
|
3019
|
-
bitbucket: createBitbucketProvider(),
|
|
3020
|
-
atlassian: createAtlassianProvider()
|
|
3021
|
-
};
|
|
3022
|
-
|
|
3023
2951
|
async function createRouter(options) {
|
|
3024
2952
|
const {
|
|
3025
2953
|
logger,
|
|
@@ -3071,7 +2999,11 @@ async function createRouter(options) {
|
|
|
3071
2999
|
try {
|
|
3072
3000
|
const provider = providerFactory({
|
|
3073
3001
|
providerId,
|
|
3074
|
-
globalConfig: {
|
|
3002
|
+
globalConfig: {
|
|
3003
|
+
baseUrl: authUrl,
|
|
3004
|
+
appUrl,
|
|
3005
|
+
isOriginAllowed
|
|
3006
|
+
},
|
|
3075
3007
|
config: providersConfig.getConfig(providerId),
|
|
3076
3008
|
logger,
|
|
3077
3009
|
tokenManager,
|
|
@@ -3131,7 +3063,6 @@ function createOriginFilter(config) {
|
|
|
3131
3063
|
}
|
|
3132
3064
|
|
|
3133
3065
|
exports.CatalogIdentityClient = CatalogIdentityClient;
|
|
3134
|
-
exports.IdentityClient = IdentityClient;
|
|
3135
3066
|
exports.OAuthAdapter = OAuthAdapter;
|
|
3136
3067
|
exports.OAuthEnvironmentHandler = OAuthEnvironmentHandler;
|
|
3137
3068
|
exports.bitbucketUserIdSignInResolver = bitbucketUserIdSignInResolver;
|