@backstage/plugin-auth-backend 0.0.0-nightly-20220208022044 → 0.0.0-nightly-20220212022325
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 +86 -2
- package/dist/index.cjs.js +369 -445
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +6 -120
- package/migrations/20210326100300_timestamptz.js +2 -2
- package/package.json +11 -10
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');
|
|
@@ -735,7 +736,6 @@ const createAtlassianProvider = (options) => {
|
|
|
735
736
|
tokenIssuer
|
|
736
737
|
});
|
|
737
738
|
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
738
|
-
disableRefresh: true,
|
|
739
739
|
providerId,
|
|
740
740
|
tokenIssuer,
|
|
741
741
|
callbackUrl
|
|
@@ -1935,461 +1935,121 @@ const createOAuth2Provider = (options) => {
|
|
|
1935
1935
|
});
|
|
1936
1936
|
};
|
|
1937
1937
|
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
const router = Router__default["default"]();
|
|
1941
|
-
const config = {
|
|
1942
|
-
issuer: baseUrl,
|
|
1943
|
-
token_endpoint: `${baseUrl}/v1/token`,
|
|
1944
|
-
userinfo_endpoint: `${baseUrl}/v1/userinfo`,
|
|
1945
|
-
jwks_uri: `${baseUrl}/.well-known/jwks.json`,
|
|
1946
|
-
response_types_supported: ["id_token"],
|
|
1947
|
-
subject_types_supported: ["public"],
|
|
1948
|
-
id_token_signing_alg_values_supported: ["RS256"],
|
|
1949
|
-
scopes_supported: ["openid"],
|
|
1950
|
-
token_endpoint_auth_methods_supported: [],
|
|
1951
|
-
claims_supported: ["sub"],
|
|
1952
|
-
grant_types_supported: []
|
|
1953
|
-
};
|
|
1954
|
-
router.get("/.well-known/openid-configuration", (_req, res) => {
|
|
1955
|
-
res.json(config);
|
|
1956
|
-
});
|
|
1957
|
-
router.get("/.well-known/jwks.json", async (_req, res) => {
|
|
1958
|
-
const { keys } = await tokenIssuer.listPublicKeys();
|
|
1959
|
-
res.json({ keys });
|
|
1960
|
-
});
|
|
1961
|
-
router.get("/v1/token", (_req, res) => {
|
|
1962
|
-
res.status(501).send("Not Implemented");
|
|
1963
|
-
});
|
|
1964
|
-
router.get("/v1/userinfo", (_req, res) => {
|
|
1965
|
-
res.status(501).send("Not Implemented");
|
|
1966
|
-
});
|
|
1967
|
-
return router;
|
|
1968
|
-
}
|
|
1969
|
-
|
|
1970
|
-
const CLOCK_MARGIN_S = 10;
|
|
1971
|
-
class IdentityClient {
|
|
1938
|
+
const OAUTH2_PROXY_JWT_HEADER = "X-OAUTH2-PROXY-ID-TOKEN";
|
|
1939
|
+
class Oauth2ProxyAuthProvider {
|
|
1972
1940
|
constructor(options) {
|
|
1973
|
-
this.
|
|
1974
|
-
this.
|
|
1975
|
-
this.
|
|
1976
|
-
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;
|
|
1977
1946
|
}
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
if (!token) {
|
|
1981
|
-
throw new errors.AuthenticationError("No token specified");
|
|
1982
|
-
}
|
|
1983
|
-
const key = await this.getKey(token);
|
|
1984
|
-
if (!key) {
|
|
1985
|
-
throw new errors.AuthenticationError("No signing key matching token found");
|
|
1986
|
-
}
|
|
1987
|
-
const decoded = jose.JWT.IdToken.verify(token, key, {
|
|
1988
|
-
algorithms: ["ES256"],
|
|
1989
|
-
audience: "backstage",
|
|
1990
|
-
issuer: this.issuer
|
|
1991
|
-
});
|
|
1992
|
-
if (!decoded.sub) {
|
|
1993
|
-
throw new errors.AuthenticationError("No user sub found in token");
|
|
1994
|
-
}
|
|
1995
|
-
const user = {
|
|
1996
|
-
id: decoded.sub,
|
|
1997
|
-
token,
|
|
1998
|
-
identity: {
|
|
1999
|
-
type: "user",
|
|
2000
|
-
userEntityRef: decoded.sub,
|
|
2001
|
-
ownershipEntityRefs: (_a = decoded.ent) != null ? _a : []
|
|
2002
|
-
}
|
|
2003
|
-
};
|
|
2004
|
-
return user;
|
|
1947
|
+
frameHandler() {
|
|
1948
|
+
return Promise.resolve(void 0);
|
|
2005
1949
|
}
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
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();
|
|
2009
1959
|
}
|
|
2010
|
-
const matches = authorizationHeader.match(/Bearer\s+(\S+)/i);
|
|
2011
|
-
return matches == null ? void 0 : matches[1];
|
|
2012
1960
|
}
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
complete: true
|
|
2016
|
-
});
|
|
2017
|
-
const keyStoreHasKey = !!this.keyStore.get({ kid: header.kid });
|
|
2018
|
-
const issuedAfterLastRefresh = (payload == null ? void 0 : payload.iat) && payload.iat > this.keyStoreUpdated - CLOCK_MARGIN_S;
|
|
2019
|
-
if (!keyStoreHasKey && issuedAfterLastRefresh) {
|
|
2020
|
-
await this.refreshKeyStore();
|
|
2021
|
-
}
|
|
2022
|
-
return this.keyStore.get({ kid: header.kid });
|
|
1961
|
+
start() {
|
|
1962
|
+
return Promise.resolve(void 0);
|
|
2023
1963
|
}
|
|
2024
|
-
async
|
|
2025
|
-
const
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
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
|
+
};
|
|
2034
1982
|
}
|
|
2035
|
-
|
|
2036
|
-
const
|
|
2037
|
-
const
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
}
|
|
2041
|
-
|
|
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
|
+
};
|
|
2042
1994
|
}
|
|
2043
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
|
+
};
|
|
2044
2011
|
|
|
2045
|
-
|
|
2046
|
-
class TokenFactory {
|
|
2012
|
+
class OidcAuthProvider {
|
|
2047
2013
|
constructor(options) {
|
|
2048
|
-
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;
|
|
2049
2021
|
this.logger = options.logger;
|
|
2050
|
-
this.keyStore = options.keyStore;
|
|
2051
|
-
this.keyDurationSeconds = options.keyDurationSeconds;
|
|
2052
|
-
}
|
|
2053
|
-
async issueToken(params) {
|
|
2054
|
-
const key = await this.getKey();
|
|
2055
|
-
const iss = this.issuer;
|
|
2056
|
-
const sub = params.claims.sub;
|
|
2057
|
-
const ent = params.claims.ent;
|
|
2058
|
-
const aud = "backstage";
|
|
2059
|
-
const iat = Math.floor(Date.now() / MS_IN_S);
|
|
2060
|
-
const exp = iat + this.keyDurationSeconds;
|
|
2061
|
-
this.logger.info(`Issuing token for ${sub}, with entities ${ent != null ? ent : []}`);
|
|
2062
|
-
return jose.JWS.sign({ iss, sub, aud, iat, exp, ent }, key, {
|
|
2063
|
-
alg: key.alg,
|
|
2064
|
-
kid: key.kid
|
|
2065
|
-
});
|
|
2066
|
-
}
|
|
2067
|
-
async listPublicKeys() {
|
|
2068
|
-
const { items: keys } = await this.keyStore.listKeys();
|
|
2069
|
-
const validKeys = [];
|
|
2070
|
-
const expiredKeys = [];
|
|
2071
|
-
for (const key of keys) {
|
|
2072
|
-
const expireAt = luxon.DateTime.fromJSDate(key.createdAt).plus({
|
|
2073
|
-
seconds: 3 * this.keyDurationSeconds
|
|
2074
|
-
});
|
|
2075
|
-
if (expireAt < luxon.DateTime.local()) {
|
|
2076
|
-
expiredKeys.push(key);
|
|
2077
|
-
} else {
|
|
2078
|
-
validKeys.push(key);
|
|
2079
|
-
}
|
|
2080
|
-
}
|
|
2081
|
-
if (expiredKeys.length > 0) {
|
|
2082
|
-
const kids = expiredKeys.map(({ key }) => key.kid);
|
|
2083
|
-
this.logger.info(`Removing expired signing keys, '${kids.join("', '")}'`);
|
|
2084
|
-
this.keyStore.removeKeys(kids).catch((error) => {
|
|
2085
|
-
this.logger.error(`Failed to remove expired keys, ${error}`);
|
|
2086
|
-
});
|
|
2087
|
-
}
|
|
2088
|
-
return { keys: validKeys.map(({ key }) => key) };
|
|
2089
2022
|
}
|
|
2090
|
-
async
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
seconds: this.keyDurationSeconds
|
|
2100
|
-
}).toJSDate();
|
|
2101
|
-
const promise = (async () => {
|
|
2102
|
-
const key = await jose.JWK.generate("EC", "P-256", {
|
|
2103
|
-
use: "sig",
|
|
2104
|
-
kid: uuid.v4(),
|
|
2105
|
-
alg: "ES256"
|
|
2106
|
-
});
|
|
2107
|
-
this.logger.info(`Created new signing key ${key.kid}`);
|
|
2108
|
-
await this.keyStore.addKey(key.toJWK(false));
|
|
2109
|
-
return key;
|
|
2110
|
-
})();
|
|
2111
|
-
this.privateKeyPromise = promise;
|
|
2112
|
-
try {
|
|
2113
|
-
await promise;
|
|
2114
|
-
} catch (error) {
|
|
2115
|
-
this.logger.error(`Failed to generate new signing key, ${error}`);
|
|
2116
|
-
delete this.keyExpiry;
|
|
2117
|
-
delete this.privateKeyPromise;
|
|
2023
|
+
async start(req) {
|
|
2024
|
+
const { strategy } = await this.implementation;
|
|
2025
|
+
const options = {
|
|
2026
|
+
scope: req.scope || this.scope || "openid profile email",
|
|
2027
|
+
state: encodeState(req.state)
|
|
2028
|
+
};
|
|
2029
|
+
const prompt = this.prompt || "none";
|
|
2030
|
+
if (prompt !== "auto") {
|
|
2031
|
+
options.prompt = prompt;
|
|
2118
2032
|
}
|
|
2119
|
-
return
|
|
2120
|
-
}
|
|
2121
|
-
}
|
|
2122
|
-
|
|
2123
|
-
const migrationsDir = backendCommon.resolvePackagePath("@backstage/plugin-auth-backend", "migrations");
|
|
2124
|
-
const TABLE = "signing_keys";
|
|
2125
|
-
const parseDate = (date) => {
|
|
2126
|
-
const parsedDate = typeof date === "string" ? luxon.DateTime.fromSQL(date, { zone: "UTC" }) : luxon.DateTime.fromJSDate(date);
|
|
2127
|
-
if (!parsedDate.isValid) {
|
|
2128
|
-
throw new Error(`Failed to parse date, reason: ${parsedDate.invalidReason}, explanation: ${parsedDate.invalidExplanation}`);
|
|
2129
|
-
}
|
|
2130
|
-
return parsedDate.toJSDate();
|
|
2131
|
-
};
|
|
2132
|
-
class DatabaseKeyStore {
|
|
2133
|
-
static async create(options) {
|
|
2134
|
-
const { database } = options;
|
|
2135
|
-
await database.migrate.latest({
|
|
2136
|
-
directory: migrationsDir
|
|
2137
|
-
});
|
|
2138
|
-
return new DatabaseKeyStore(options);
|
|
2139
|
-
}
|
|
2140
|
-
constructor(options) {
|
|
2141
|
-
this.database = options.database;
|
|
2033
|
+
return await executeRedirectStrategy(req, strategy, options);
|
|
2142
2034
|
}
|
|
2143
|
-
async
|
|
2144
|
-
await this.
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
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
|
+
};
|
|
2148
2042
|
}
|
|
2149
|
-
async
|
|
2150
|
-
const
|
|
2043
|
+
async refresh(req) {
|
|
2044
|
+
const { client } = await this.implementation;
|
|
2045
|
+
const tokenset = await client.refresh(req.refreshToken);
|
|
2046
|
+
if (!tokenset.access_token) {
|
|
2047
|
+
throw new Error("Refresh failed");
|
|
2048
|
+
}
|
|
2049
|
+
const userinfo = await client.userinfo(tokenset.access_token);
|
|
2151
2050
|
return {
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
createdAt: parseDate(row.created_at)
|
|
2155
|
-
}))
|
|
2156
|
-
};
|
|
2157
|
-
}
|
|
2158
|
-
async removeKeys(kids) {
|
|
2159
|
-
await this.database(TABLE).delete().whereIn("kid", kids);
|
|
2160
|
-
}
|
|
2161
|
-
}
|
|
2162
|
-
|
|
2163
|
-
class MemoryKeyStore {
|
|
2164
|
-
constructor() {
|
|
2165
|
-
this.keys = /* @__PURE__ */ new Map();
|
|
2166
|
-
}
|
|
2167
|
-
async addKey(key) {
|
|
2168
|
-
this.keys.set(key.kid, {
|
|
2169
|
-
createdAt: luxon.DateTime.utc().toJSDate(),
|
|
2170
|
-
key: JSON.stringify(key)
|
|
2171
|
-
});
|
|
2172
|
-
}
|
|
2173
|
-
async removeKeys(kids) {
|
|
2174
|
-
for (const kid of kids) {
|
|
2175
|
-
this.keys.delete(kid);
|
|
2176
|
-
}
|
|
2177
|
-
}
|
|
2178
|
-
async listKeys() {
|
|
2179
|
-
return {
|
|
2180
|
-
items: Array.from(this.keys).map(([, { createdAt, key: keyStr }]) => ({
|
|
2181
|
-
createdAt,
|
|
2182
|
-
key: JSON.parse(keyStr)
|
|
2183
|
-
}))
|
|
2184
|
-
};
|
|
2185
|
-
}
|
|
2186
|
-
}
|
|
2187
|
-
|
|
2188
|
-
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
2189
|
-
const DEFAULT_DOCUMENT_PATH = "sessions";
|
|
2190
|
-
class FirestoreKeyStore {
|
|
2191
|
-
constructor(database, path, timeout) {
|
|
2192
|
-
this.database = database;
|
|
2193
|
-
this.path = path;
|
|
2194
|
-
this.timeout = timeout;
|
|
2195
|
-
}
|
|
2196
|
-
static async create(settings) {
|
|
2197
|
-
const { path, timeout, ...firestoreSettings } = settings != null ? settings : {};
|
|
2198
|
-
const database = new firestore.Firestore(firestoreSettings);
|
|
2199
|
-
return new FirestoreKeyStore(database, path != null ? path : DEFAULT_DOCUMENT_PATH, timeout != null ? timeout : DEFAULT_TIMEOUT_MS);
|
|
2200
|
-
}
|
|
2201
|
-
static async verifyConnection(keyStore, logger) {
|
|
2202
|
-
try {
|
|
2203
|
-
await keyStore.verify();
|
|
2204
|
-
} catch (error) {
|
|
2205
|
-
if (process.env.NODE_ENV !== "development") {
|
|
2206
|
-
throw new Error(`Failed to connect to database: ${error.message}`);
|
|
2207
|
-
}
|
|
2208
|
-
logger == null ? void 0 : logger.warn(`Failed to connect to database: ${error.message}`);
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2211
|
-
async addKey(key) {
|
|
2212
|
-
await this.withTimeout(this.database.collection(this.path).doc(key.kid).set({
|
|
2213
|
-
kid: key.kid,
|
|
2214
|
-
key: JSON.stringify(key)
|
|
2215
|
-
}));
|
|
2216
|
-
}
|
|
2217
|
-
async listKeys() {
|
|
2218
|
-
const keys = await this.withTimeout(this.database.collection(this.path).get());
|
|
2219
|
-
return {
|
|
2220
|
-
items: keys.docs.map((key) => ({
|
|
2221
|
-
key: key.data(),
|
|
2222
|
-
createdAt: key.createTime.toDate()
|
|
2223
|
-
}))
|
|
2224
|
-
};
|
|
2225
|
-
}
|
|
2226
|
-
async removeKeys(kids) {
|
|
2227
|
-
for (const kid of kids) {
|
|
2228
|
-
await this.withTimeout(this.database.collection(this.path).doc(kid).delete());
|
|
2229
|
-
}
|
|
2230
|
-
}
|
|
2231
|
-
async withTimeout(operation) {
|
|
2232
|
-
const timer = new Promise((_, reject) => setTimeout(() => {
|
|
2233
|
-
reject(new Error(`Operation timed out after ${this.timeout}ms`));
|
|
2234
|
-
}, this.timeout));
|
|
2235
|
-
return Promise.race([operation, timer]);
|
|
2236
|
-
}
|
|
2237
|
-
async verify() {
|
|
2238
|
-
await this.withTimeout(this.database.collection(this.path).limit(1).get());
|
|
2239
|
-
}
|
|
2240
|
-
}
|
|
2241
|
-
|
|
2242
|
-
class KeyStores {
|
|
2243
|
-
static async fromConfig(config, options) {
|
|
2244
|
-
var _a;
|
|
2245
|
-
const { logger, database } = options != null ? options : {};
|
|
2246
|
-
const ks = config.getOptionalConfig("auth.keyStore");
|
|
2247
|
-
const provider = (_a = ks == null ? void 0 : ks.getOptionalString("provider")) != null ? _a : "database";
|
|
2248
|
-
logger == null ? void 0 : logger.info(`Configuring "${provider}" as KeyStore provider`);
|
|
2249
|
-
if (provider === "database") {
|
|
2250
|
-
if (!database) {
|
|
2251
|
-
throw new Error("This KeyStore provider requires a database");
|
|
2252
|
-
}
|
|
2253
|
-
return await DatabaseKeyStore.create({
|
|
2254
|
-
database: await database.getClient()
|
|
2255
|
-
});
|
|
2256
|
-
}
|
|
2257
|
-
if (provider === "memory") {
|
|
2258
|
-
return new MemoryKeyStore();
|
|
2259
|
-
}
|
|
2260
|
-
if (provider === "firestore") {
|
|
2261
|
-
const settings = ks == null ? void 0 : ks.getConfig(provider);
|
|
2262
|
-
const keyStore = await FirestoreKeyStore.create(lodash.pickBy({
|
|
2263
|
-
projectId: settings == null ? void 0 : settings.getOptionalString("projectId"),
|
|
2264
|
-
keyFilename: settings == null ? void 0 : settings.getOptionalString("keyFilename"),
|
|
2265
|
-
host: settings == null ? void 0 : settings.getOptionalString("host"),
|
|
2266
|
-
port: settings == null ? void 0 : settings.getOptionalNumber("port"),
|
|
2267
|
-
ssl: settings == null ? void 0 : settings.getOptionalBoolean("ssl"),
|
|
2268
|
-
path: settings == null ? void 0 : settings.getOptionalString("path"),
|
|
2269
|
-
timeout: settings == null ? void 0 : settings.getOptionalNumber("timeout")
|
|
2270
|
-
}, (value) => value !== void 0));
|
|
2271
|
-
await FirestoreKeyStore.verifyConnection(keyStore, logger);
|
|
2272
|
-
return keyStore;
|
|
2273
|
-
}
|
|
2274
|
-
throw new Error(`Unknown KeyStore provider: ${provider}`);
|
|
2275
|
-
}
|
|
2276
|
-
}
|
|
2277
|
-
|
|
2278
|
-
const OAUTH2_PROXY_JWT_HEADER = "X-OAUTH2-PROXY-ID-TOKEN";
|
|
2279
|
-
class Oauth2ProxyAuthProvider {
|
|
2280
|
-
constructor(options) {
|
|
2281
|
-
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
2282
|
-
this.logger = options.logger;
|
|
2283
|
-
this.tokenIssuer = options.tokenIssuer;
|
|
2284
|
-
this.signInResolver = options.signInResolver;
|
|
2285
|
-
this.authHandler = options.authHandler;
|
|
2286
|
-
}
|
|
2287
|
-
frameHandler() {
|
|
2288
|
-
return Promise.resolve(void 0);
|
|
2289
|
-
}
|
|
2290
|
-
async refresh(req, res) {
|
|
2291
|
-
try {
|
|
2292
|
-
const result = this.getResult(req);
|
|
2293
|
-
const response = await this.handleResult(result);
|
|
2294
|
-
res.json(response);
|
|
2295
|
-
} catch (e) {
|
|
2296
|
-
this.logger.error(`Exception occurred during ${OAUTH2_PROXY_JWT_HEADER} refresh`, e);
|
|
2297
|
-
res.status(401);
|
|
2298
|
-
res.end();
|
|
2299
|
-
}
|
|
2300
|
-
}
|
|
2301
|
-
start() {
|
|
2302
|
-
return Promise.resolve(void 0);
|
|
2303
|
-
}
|
|
2304
|
-
async handleResult(result) {
|
|
2305
|
-
const ctx = {
|
|
2306
|
-
logger: this.logger,
|
|
2307
|
-
tokenIssuer: this.tokenIssuer,
|
|
2308
|
-
catalogIdentityClient: this.catalogIdentityClient
|
|
2309
|
-
};
|
|
2310
|
-
const { profile } = await this.authHandler(result, ctx);
|
|
2311
|
-
const backstageSignInResult = await this.signInResolver({
|
|
2312
|
-
result,
|
|
2313
|
-
profile
|
|
2314
|
-
}, ctx);
|
|
2315
|
-
return {
|
|
2316
|
-
providerInfo: {
|
|
2317
|
-
accessToken: result.accessToken
|
|
2318
|
-
},
|
|
2319
|
-
backstageIdentity: prepareBackstageIdentityResponse(backstageSignInResult),
|
|
2320
|
-
profile
|
|
2321
|
-
};
|
|
2322
|
-
}
|
|
2323
|
-
getResult(req) {
|
|
2324
|
-
const authHeader = req.header(OAUTH2_PROXY_JWT_HEADER);
|
|
2325
|
-
const jwt = IdentityClient.getBearerToken(authHeader);
|
|
2326
|
-
if (!jwt) {
|
|
2327
|
-
throw new errors.AuthenticationError(`Missing or in incorrect format - Oauth2Proxy OIDC header: ${OAUTH2_PROXY_JWT_HEADER}`);
|
|
2328
|
-
}
|
|
2329
|
-
const decodedJWT = jose.JWT.decode(jwt);
|
|
2330
|
-
return {
|
|
2331
|
-
fullProfile: decodedJWT,
|
|
2332
|
-
accessToken: jwt
|
|
2333
|
-
};
|
|
2334
|
-
}
|
|
2335
|
-
}
|
|
2336
|
-
const createOauth2ProxyProvider = (options) => ({ catalogApi, logger, tokenIssuer, tokenManager }) => {
|
|
2337
|
-
const signInResolver = options.signIn.resolver;
|
|
2338
|
-
const authHandler = options.authHandler;
|
|
2339
|
-
const catalogIdentityClient = new CatalogIdentityClient({
|
|
2340
|
-
catalogApi,
|
|
2341
|
-
tokenManager
|
|
2342
|
-
});
|
|
2343
|
-
return new Oauth2ProxyAuthProvider({
|
|
2344
|
-
logger,
|
|
2345
|
-
signInResolver,
|
|
2346
|
-
authHandler,
|
|
2347
|
-
tokenIssuer,
|
|
2348
|
-
catalogIdentityClient
|
|
2349
|
-
});
|
|
2350
|
-
};
|
|
2351
|
-
|
|
2352
|
-
class OidcAuthProvider {
|
|
2353
|
-
constructor(options) {
|
|
2354
|
-
this.implementation = this.setupStrategy(options);
|
|
2355
|
-
this.scope = options.scope;
|
|
2356
|
-
this.prompt = options.prompt;
|
|
2357
|
-
this.signInResolver = options.signInResolver;
|
|
2358
|
-
this.authHandler = options.authHandler;
|
|
2359
|
-
this.tokenIssuer = options.tokenIssuer;
|
|
2360
|
-
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
2361
|
-
this.logger = options.logger;
|
|
2362
|
-
}
|
|
2363
|
-
async start(req) {
|
|
2364
|
-
const { strategy } = await this.implementation;
|
|
2365
|
-
const options = {
|
|
2366
|
-
scope: req.scope || this.scope || "openid profile email",
|
|
2367
|
-
state: encodeState(req.state)
|
|
2368
|
-
};
|
|
2369
|
-
const prompt = this.prompt || "none";
|
|
2370
|
-
if (prompt !== "auto") {
|
|
2371
|
-
options.prompt = prompt;
|
|
2372
|
-
}
|
|
2373
|
-
return await executeRedirectStrategy(req, strategy, options);
|
|
2374
|
-
}
|
|
2375
|
-
async handler(req) {
|
|
2376
|
-
const { strategy } = await this.implementation;
|
|
2377
|
-
const { result, privateInfo } = await executeFrameHandlerStrategy(req, strategy);
|
|
2378
|
-
return {
|
|
2379
|
-
response: await this.handleResult(result),
|
|
2380
|
-
refreshToken: privateInfo.refreshToken
|
|
2381
|
-
};
|
|
2382
|
-
}
|
|
2383
|
-
async refresh(req) {
|
|
2384
|
-
const { client } = await this.implementation;
|
|
2385
|
-
const tokenset = await client.refresh(req.refreshToken);
|
|
2386
|
-
if (!tokenset.access_token) {
|
|
2387
|
-
throw new Error("Refresh failed");
|
|
2388
|
-
}
|
|
2389
|
-
const userinfo = await client.userinfo(tokenset.access_token);
|
|
2390
|
-
return {
|
|
2391
|
-
response: await this.handleResult({ tokenset, userinfo }),
|
|
2392
|
-
refreshToken: tokenset.refresh_token
|
|
2051
|
+
response: await this.handleResult({ tokenset, userinfo }),
|
|
2052
|
+
refreshToken: tokenset.refresh_token
|
|
2393
2053
|
};
|
|
2394
2054
|
}
|
|
2395
2055
|
async setupStrategy(options) {
|
|
@@ -3023,6 +2683,271 @@ const factories = {
|
|
|
3023
2683
|
atlassian: createAtlassianProvider()
|
|
3024
2684
|
};
|
|
3025
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
|
+
|
|
3026
2951
|
async function createRouter(options) {
|
|
3027
2952
|
const {
|
|
3028
2953
|
logger,
|
|
@@ -3138,7 +3063,6 @@ function createOriginFilter(config) {
|
|
|
3138
3063
|
}
|
|
3139
3064
|
|
|
3140
3065
|
exports.CatalogIdentityClient = CatalogIdentityClient;
|
|
3141
|
-
exports.IdentityClient = IdentityClient;
|
|
3142
3066
|
exports.OAuthAdapter = OAuthAdapter;
|
|
3143
3067
|
exports.OAuthEnvironmentHandler = OAuthEnvironmentHandler;
|
|
3144
3068
|
exports.bitbucketUserIdSignInResolver = bitbucketUserIdSignInResolver;
|