@backstage/plugin-auth-backend 0.23.0-next.1 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/dist/index.cjs.js +430 -816
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/package.json +29 -27
package/dist/index.cjs.js
CHANGED
|
@@ -9,11 +9,7 @@ var express = require('express');
|
|
|
9
9
|
var Router = require('express-promise-router');
|
|
10
10
|
var cookieParser = require('cookie-parser');
|
|
11
11
|
var pluginAuthBackendModuleAtlassianProvider = require('@backstage/plugin-auth-backend-module-atlassian-provider');
|
|
12
|
-
var
|
|
13
|
-
var crypto = require('crypto');
|
|
14
|
-
var url = require('url');
|
|
15
|
-
var errors = require('@backstage/errors');
|
|
16
|
-
var jose = require('jose');
|
|
12
|
+
var pluginAuthBackendModuleAuth0Provider = require('@backstage/plugin-auth-backend-module-auth0-provider');
|
|
17
13
|
var pluginAuthBackendModuleAwsAlbProvider = require('@backstage/plugin-auth-backend-module-aws-alb-provider');
|
|
18
14
|
var pluginAuthBackendModuleBitbucketProvider = require('@backstage/plugin-auth-backend-module-bitbucket-provider');
|
|
19
15
|
var pluginAuthBackendModuleCloudflareAccessProvider = require('@backstage/plugin-auth-backend-module-cloudflare-access-provider');
|
|
@@ -28,8 +24,10 @@ var pluginAuthBackendModuleOidcProvider = require('@backstage/plugin-auth-backen
|
|
|
28
24
|
var pluginAuthBackendModuleOktaProvider = require('@backstage/plugin-auth-backend-module-okta-provider');
|
|
29
25
|
var pluginAuthBackendModuleOneloginProvider = require('@backstage/plugin-auth-backend-module-onelogin-provider');
|
|
30
26
|
var passportSaml = require('@node-saml/passport-saml');
|
|
31
|
-
var
|
|
32
|
-
var
|
|
27
|
+
var jose = require('jose');
|
|
28
|
+
var crypto = require('crypto');
|
|
29
|
+
var errors = require('@backstage/errors');
|
|
30
|
+
var pluginAuthBackendModuleBitbucketServerProvider = require('@backstage/plugin-auth-backend-module-bitbucket-server-provider');
|
|
33
31
|
var pluginAuthBackendModuleAzureEasyauthProvider = require('@backstage/plugin-auth-backend-module-azure-easyauth-provider');
|
|
34
32
|
var catalogClient = require('@backstage/catalog-client');
|
|
35
33
|
var minimatch = require('minimatch');
|
|
@@ -42,665 +40,105 @@ var firestore = require('@google-cloud/firestore');
|
|
|
42
40
|
var fs = require('fs');
|
|
43
41
|
var session = require('express-session');
|
|
44
42
|
var connectSessionKnex = require('connect-session-knex');
|
|
45
|
-
var passport = require('passport');
|
|
46
|
-
var config = require('@backstage/config');
|
|
47
|
-
var types = require('@backstage/types');
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
var
|
|
53
|
-
var
|
|
54
|
-
var
|
|
55
|
-
var crypto__default = /*#__PURE__*/_interopDefaultCompat(crypto);
|
|
56
|
-
var
|
|
57
|
-
var
|
|
58
|
-
var
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
expires_in: result.session.expiresInSeconds
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
ctx
|
|
74
|
-
));
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function adaptLegacyOAuthSignInResolver(signInResolver) {
|
|
78
|
-
return signInResolver && (async (input, ctx) => signInResolver(
|
|
79
|
-
{
|
|
80
|
-
profile: input.profile,
|
|
81
|
-
result: {
|
|
82
|
-
fullProfile: input.result.fullProfile,
|
|
83
|
-
accessToken: input.result.session.accessToken,
|
|
84
|
-
refreshToken: input.result.session.refreshToken,
|
|
85
|
-
params: {
|
|
86
|
-
scope: input.result.session.scope,
|
|
87
|
-
id_token: input.result.session.idToken,
|
|
88
|
-
token_type: input.result.session.tokenType,
|
|
89
|
-
expires_in: input.result.session.expiresInSeconds
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
ctx
|
|
94
|
-
));
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function adaptOAuthSignInResolverToLegacy(resolvers) {
|
|
98
|
-
const legacyResolvers = {};
|
|
99
|
-
for (const name of Object.keys(resolvers)) {
|
|
100
|
-
const resolver = resolvers[name];
|
|
101
|
-
legacyResolvers[name] = () => async (input, ctx) => resolver(
|
|
102
|
-
{
|
|
103
|
-
profile: input.profile,
|
|
104
|
-
result: {
|
|
105
|
-
fullProfile: input.result.fullProfile,
|
|
106
|
-
session: {
|
|
107
|
-
accessToken: input.result.accessToken,
|
|
108
|
-
expiresInSeconds: input.result.params.expires_in,
|
|
109
|
-
scope: input.result.params.scope,
|
|
110
|
-
idToken: input.result.params.id_token,
|
|
111
|
-
tokenType: input.result.params.token_type ?? "bearer",
|
|
112
|
-
refreshToken: input.result.refreshToken
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
},
|
|
116
|
-
ctx
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
return legacyResolvers;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function createAuthProviderIntegration(config) {
|
|
123
|
-
return Object.freeze({
|
|
124
|
-
...config,
|
|
125
|
-
resolvers: Object.freeze(config.resolvers ?? {})
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const atlassian = createAuthProviderIntegration({
|
|
130
|
-
create(options) {
|
|
131
|
-
return pluginAuthNode.createOAuthProviderFactory({
|
|
132
|
-
authenticator: pluginAuthBackendModuleAtlassianProvider.atlassianAuthenticator,
|
|
133
|
-
profileTransform: adaptLegacyOAuthHandler(options?.authHandler),
|
|
134
|
-
signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver)
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
class Auth0Strategy extends Auth0InternalStrategy__default.default {
|
|
140
|
-
constructor(options, verify) {
|
|
141
|
-
const optionsWithURLs = {
|
|
142
|
-
...options,
|
|
143
|
-
authorizationURL: `https://${options.domain}/authorize`,
|
|
144
|
-
tokenURL: `https://${options.domain}/oauth/token`,
|
|
145
|
-
userInfoURL: `https://${options.domain}/userinfo`,
|
|
146
|
-
apiUrl: `https://${options.domain}/api`
|
|
147
|
-
};
|
|
148
|
-
super(optionsWithURLs, verify);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const OAuthEnvironmentHandler = pluginAuthNode.OAuthEnvironmentHandler;
|
|
153
|
-
|
|
154
|
-
const readState = pluginAuthNode.decodeOAuthState;
|
|
155
|
-
const encodeState = pluginAuthNode.encodeOAuthState;
|
|
156
|
-
const verifyNonce = (req, providerId) => {
|
|
157
|
-
const cookieNonce = req.cookies[`${providerId}-nonce`];
|
|
158
|
-
const state = readState(req.query.state?.toString() ?? "");
|
|
159
|
-
const stateNonce = state.nonce;
|
|
160
|
-
if (!cookieNonce) {
|
|
161
|
-
throw new Error("Auth response is missing cookie nonce");
|
|
162
|
-
}
|
|
163
|
-
if (stateNonce.length === 0) {
|
|
164
|
-
throw new Error("Auth response is missing state nonce");
|
|
165
|
-
}
|
|
166
|
-
if (cookieNonce !== stateNonce) {
|
|
167
|
-
throw new Error("Invalid nonce");
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
const defaultCookieConfigurer = ({
|
|
171
|
-
callbackUrl,
|
|
172
|
-
providerId,
|
|
173
|
-
appOrigin
|
|
174
|
-
}) => {
|
|
175
|
-
const { hostname: domain, pathname, protocol } = new URL(callbackUrl);
|
|
176
|
-
const secure = protocol === "https:";
|
|
177
|
-
let sameSite = "lax";
|
|
178
|
-
if (new URL(appOrigin).hostname !== domain && secure) {
|
|
179
|
-
sameSite = "none";
|
|
180
|
-
}
|
|
181
|
-
const path = pathname.endsWith(`${providerId}/handler/frame`) ? pathname.slice(0, -"/handler/frame".length) : `${pathname}/${providerId}`;
|
|
182
|
-
return { domain, path, secure, sameSite };
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
const safelyEncodeURIComponent = (value) => {
|
|
186
|
-
return encodeURIComponent(value).replace(/'/g, "%27");
|
|
187
|
-
};
|
|
188
|
-
const postMessageResponse = (res, appOrigin, response) => {
|
|
189
|
-
const jsonData = JSON.stringify(response);
|
|
190
|
-
const base64Data = safelyEncodeURIComponent(jsonData);
|
|
191
|
-
const base64Origin = safelyEncodeURIComponent(appOrigin);
|
|
192
|
-
const script = `
|
|
193
|
-
var authResponse = decodeURIComponent('${base64Data}');
|
|
194
|
-
var origin = decodeURIComponent('${base64Origin}');
|
|
195
|
-
var originInfo = {'type': 'config_info', 'targetOrigin': origin};
|
|
196
|
-
(window.opener || window.parent).postMessage(originInfo, '*');
|
|
197
|
-
(window.opener || window.parent).postMessage(JSON.parse(authResponse), origin);
|
|
198
|
-
setTimeout(() => {
|
|
199
|
-
window.close();
|
|
200
|
-
}, 100); // same as the interval of the core-app-api lib/loginPopup.ts (to address race conditions)
|
|
201
|
-
`;
|
|
202
|
-
const hash = crypto__default.default.createHash("sha256").update(script).digest("base64");
|
|
203
|
-
res.setHeader("Content-Type", "text/html");
|
|
204
|
-
res.setHeader("X-Frame-Options", "sameorigin");
|
|
205
|
-
res.setHeader("Content-Security-Policy", `script-src 'sha256-${hash}'`);
|
|
206
|
-
res.end(`<html><body><script>${script}<\/script></body></html>`);
|
|
207
|
-
};
|
|
208
|
-
const ensuresXRequestedWith = (req) => {
|
|
209
|
-
const requiredHeader = req.header("X-Requested-With");
|
|
210
|
-
if (!requiredHeader || requiredHeader !== "XMLHttpRequest") {
|
|
211
|
-
return false;
|
|
212
|
-
}
|
|
213
|
-
return true;
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
const prepareBackstageIdentityResponse = pluginAuthNode.prepareBackstageIdentityResponse;
|
|
217
|
-
|
|
218
|
-
const THOUSAND_DAYS_MS = 1e3 * 24 * 60 * 60 * 1e3;
|
|
219
|
-
const TEN_MINUTES_MS = 600 * 1e3;
|
|
220
|
-
class OAuthAdapter {
|
|
221
|
-
constructor(handlers, options) {
|
|
222
|
-
this.handlers = handlers;
|
|
223
|
-
this.options = options;
|
|
224
|
-
this.baseCookieOptions = {
|
|
225
|
-
httpOnly: true,
|
|
226
|
-
sameSite: "lax"
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
static fromConfig(config, handlers, options) {
|
|
230
|
-
const { appUrl, baseUrl, isOriginAllowed } = config;
|
|
231
|
-
const { origin: appOrigin } = new url.URL(appUrl);
|
|
232
|
-
const cookieConfigurer = config.cookieConfigurer ?? defaultCookieConfigurer;
|
|
233
|
-
return new OAuthAdapter(handlers, {
|
|
234
|
-
...options,
|
|
235
|
-
appOrigin,
|
|
236
|
-
baseUrl,
|
|
237
|
-
cookieConfigurer,
|
|
238
|
-
isOriginAllowed
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
baseCookieOptions;
|
|
242
|
-
async start(req, res) {
|
|
243
|
-
const scope = req.query.scope?.toString() ?? "";
|
|
244
|
-
const env = req.query.env?.toString();
|
|
245
|
-
const origin = req.query.origin?.toString();
|
|
246
|
-
const redirectUrl = req.query.redirectUrl?.toString();
|
|
247
|
-
const flow = req.query.flow?.toString();
|
|
248
|
-
if (!env) {
|
|
249
|
-
throw new errors.InputError("No env provided in request query parameters");
|
|
250
|
-
}
|
|
251
|
-
const cookieConfig = this.getCookieConfig(origin);
|
|
252
|
-
const nonce = crypto__default.default.randomBytes(16).toString("base64");
|
|
253
|
-
this.setNonceCookie(res, nonce, cookieConfig);
|
|
254
|
-
const state = { nonce, env, origin, redirectUrl, flow };
|
|
255
|
-
if (this.options.persistScopes) {
|
|
256
|
-
state.scope = scope;
|
|
257
|
-
}
|
|
258
|
-
const forwardReq = Object.assign(req, { scope, state });
|
|
259
|
-
const { url, status } = await this.handlers.start(
|
|
260
|
-
forwardReq
|
|
261
|
-
);
|
|
262
|
-
res.statusCode = status || 302;
|
|
263
|
-
res.setHeader("Location", url);
|
|
264
|
-
res.setHeader("Content-Length", "0");
|
|
265
|
-
res.end();
|
|
266
|
-
}
|
|
267
|
-
async frameHandler(req, res) {
|
|
268
|
-
let appOrigin = this.options.appOrigin;
|
|
269
|
-
try {
|
|
270
|
-
const state = readState(req.query.state?.toString() ?? "");
|
|
271
|
-
if (state.origin) {
|
|
272
|
-
try {
|
|
273
|
-
appOrigin = new url.URL(state.origin).origin;
|
|
274
|
-
} catch {
|
|
275
|
-
throw new errors.NotAllowedError("App origin is invalid, failed to parse");
|
|
276
|
-
}
|
|
277
|
-
if (!this.options.isOriginAllowed(appOrigin)) {
|
|
278
|
-
throw new errors.NotAllowedError(`Origin '${appOrigin}' is not allowed`);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
verifyNonce(req, this.options.providerId);
|
|
282
|
-
const { response, refreshToken } = await this.handlers.handler(req);
|
|
283
|
-
const cookieConfig = this.getCookieConfig(appOrigin);
|
|
284
|
-
if (this.options.persistScopes && state.scope) {
|
|
285
|
-
this.setGrantedScopeCookie(res, state.scope, cookieConfig);
|
|
286
|
-
response.providerInfo.scope = state.scope;
|
|
287
|
-
}
|
|
288
|
-
if (refreshToken) {
|
|
289
|
-
this.setRefreshTokenCookie(res, refreshToken, cookieConfig);
|
|
290
|
-
}
|
|
291
|
-
const identity = await this.populateIdentity(response.backstageIdentity);
|
|
292
|
-
const responseObj = {
|
|
293
|
-
type: "authorization_response",
|
|
294
|
-
response: { ...response, backstageIdentity: identity }
|
|
295
|
-
};
|
|
296
|
-
if (state.flow === "redirect") {
|
|
297
|
-
if (!state.redirectUrl) {
|
|
298
|
-
throw new errors.InputError(
|
|
299
|
-
"No redirectUrl provided in request query parameters"
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
res.redirect(state.redirectUrl);
|
|
303
|
-
return void 0;
|
|
304
|
-
}
|
|
305
|
-
return postMessageResponse(res, appOrigin, responseObj);
|
|
306
|
-
} catch (error) {
|
|
307
|
-
const { name, message } = errors.isError(error) ? error : new Error("Encountered invalid error");
|
|
308
|
-
return postMessageResponse(res, appOrigin, {
|
|
309
|
-
type: "authorization_response",
|
|
310
|
-
error: { name, message }
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
async logout(req, res) {
|
|
315
|
-
if (!ensuresXRequestedWith(req)) {
|
|
316
|
-
throw new errors.AuthenticationError("Invalid X-Requested-With header");
|
|
317
|
-
}
|
|
318
|
-
if (this.handlers.logout) {
|
|
319
|
-
const refreshToken = this.getRefreshTokenFromCookie(req);
|
|
320
|
-
const revokeRequest = Object.assign(req, {
|
|
321
|
-
refreshToken
|
|
322
|
-
});
|
|
323
|
-
await this.handlers.logout(revokeRequest);
|
|
324
|
-
}
|
|
325
|
-
const origin = req.get("origin");
|
|
326
|
-
const cookieConfig = this.getCookieConfig(origin);
|
|
327
|
-
this.removeRefreshTokenCookie(res, cookieConfig);
|
|
328
|
-
res.status(200).end();
|
|
329
|
-
}
|
|
330
|
-
async refresh(req, res) {
|
|
331
|
-
if (!ensuresXRequestedWith(req)) {
|
|
332
|
-
throw new errors.AuthenticationError("Invalid X-Requested-With header");
|
|
333
|
-
}
|
|
334
|
-
if (!this.handlers.refresh) {
|
|
335
|
-
throw new errors.InputError(
|
|
336
|
-
`Refresh token is not supported for provider ${this.options.providerId}`
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
try {
|
|
340
|
-
const refreshToken = this.getRefreshTokenFromCookie(req);
|
|
341
|
-
if (!refreshToken) {
|
|
342
|
-
throw new errors.InputError("Missing session cookie");
|
|
343
|
-
}
|
|
344
|
-
let scope = req.query.scope?.toString() ?? "";
|
|
345
|
-
if (this.options.persistScopes) {
|
|
346
|
-
scope = this.getGrantedScopeFromCookie(req);
|
|
347
|
-
}
|
|
348
|
-
const forwardReq = Object.assign(req, { scope, refreshToken });
|
|
349
|
-
const { response, refreshToken: newRefreshToken } = await this.handlers.refresh(forwardReq);
|
|
350
|
-
const backstageIdentity = await this.populateIdentity(
|
|
351
|
-
response.backstageIdentity
|
|
352
|
-
);
|
|
353
|
-
if (newRefreshToken && newRefreshToken !== refreshToken) {
|
|
354
|
-
const origin = req.get("origin");
|
|
355
|
-
const cookieConfig = this.getCookieConfig(origin);
|
|
356
|
-
this.setRefreshTokenCookie(res, newRefreshToken, cookieConfig);
|
|
357
|
-
}
|
|
358
|
-
res.status(200).json({ ...response, backstageIdentity });
|
|
359
|
-
} catch (error) {
|
|
360
|
-
throw new errors.AuthenticationError("Refresh failed", error);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
/**
|
|
364
|
-
* If the response from the OAuth provider includes a Backstage identity, we
|
|
365
|
-
* make sure it's populated with all the information we can derive from the user ID.
|
|
366
|
-
*/
|
|
367
|
-
async populateIdentity(identity) {
|
|
368
|
-
if (!identity) {
|
|
369
|
-
return void 0;
|
|
370
|
-
}
|
|
371
|
-
if (!identity.token) {
|
|
372
|
-
throw new errors.InputError(`Identity response must return a token`);
|
|
373
|
-
}
|
|
374
|
-
return prepareBackstageIdentityResponse(identity);
|
|
375
|
-
}
|
|
376
|
-
setNonceCookie = (res, nonce, cookieConfig) => {
|
|
377
|
-
res.cookie(`${this.options.providerId}-nonce`, nonce, {
|
|
378
|
-
maxAge: TEN_MINUTES_MS,
|
|
379
|
-
...this.baseCookieOptions,
|
|
380
|
-
...cookieConfig,
|
|
381
|
-
path: `${cookieConfig.path}/handler`
|
|
382
|
-
});
|
|
383
|
-
};
|
|
384
|
-
setGrantedScopeCookie = (res, scope, cookieConfig) => {
|
|
385
|
-
res.cookie(`${this.options.providerId}-granted-scope`, scope, {
|
|
386
|
-
maxAge: THOUSAND_DAYS_MS,
|
|
387
|
-
...this.baseCookieOptions,
|
|
388
|
-
...cookieConfig
|
|
389
|
-
});
|
|
390
|
-
};
|
|
391
|
-
getRefreshTokenFromCookie = (req) => {
|
|
392
|
-
return req.cookies[`${this.options.providerId}-refresh-token`];
|
|
393
|
-
};
|
|
394
|
-
getGrantedScopeFromCookie = (req) => {
|
|
395
|
-
return req.cookies[`${this.options.providerId}-granted-scope`];
|
|
396
|
-
};
|
|
397
|
-
setRefreshTokenCookie = (res, refreshToken, cookieConfig) => {
|
|
398
|
-
res.cookie(`${this.options.providerId}-refresh-token`, refreshToken, {
|
|
399
|
-
maxAge: THOUSAND_DAYS_MS,
|
|
400
|
-
...this.baseCookieOptions,
|
|
401
|
-
...cookieConfig
|
|
402
|
-
});
|
|
403
|
-
};
|
|
404
|
-
removeRefreshTokenCookie = (res, cookieConfig) => {
|
|
405
|
-
res.cookie(`${this.options.providerId}-refresh-token`, "", {
|
|
406
|
-
maxAge: 0,
|
|
407
|
-
...this.baseCookieOptions,
|
|
408
|
-
...cookieConfig
|
|
409
|
-
});
|
|
410
|
-
};
|
|
411
|
-
getCookieConfig = (origin) => {
|
|
412
|
-
return this.options.cookieConfigurer({
|
|
413
|
-
providerId: this.options.providerId,
|
|
414
|
-
baseUrl: this.options.baseUrl,
|
|
415
|
-
callbackUrl: this.options.callbackUrl,
|
|
416
|
-
appOrigin: origin ?? this.options.appOrigin
|
|
417
|
-
});
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
const makeProfileInfo = (profile, idToken) => {
|
|
422
|
-
let email = void 0;
|
|
423
|
-
if (profile.emails && profile.emails.length > 0) {
|
|
424
|
-
const [firstEmail] = profile.emails;
|
|
425
|
-
email = firstEmail.value;
|
|
426
|
-
}
|
|
427
|
-
let picture = void 0;
|
|
428
|
-
if (profile.avatarUrl) {
|
|
429
|
-
picture = profile.avatarUrl;
|
|
430
|
-
} else if (profile.photos && profile.photos.length > 0) {
|
|
431
|
-
const [firstPhoto] = profile.photos;
|
|
432
|
-
picture = firstPhoto.value;
|
|
433
|
-
}
|
|
434
|
-
let displayName = profile.displayName ?? profile.username ?? profile.id;
|
|
435
|
-
if ((!email || !picture || !displayName) && idToken) {
|
|
436
|
-
try {
|
|
437
|
-
const decoded = jose.decodeJwt(idToken);
|
|
438
|
-
if (!email && decoded.email) {
|
|
439
|
-
email = decoded.email;
|
|
440
|
-
}
|
|
441
|
-
if (!picture && decoded.picture) {
|
|
442
|
-
picture = decoded.picture;
|
|
443
|
-
}
|
|
444
|
-
if (!displayName && decoded.name) {
|
|
445
|
-
displayName = decoded.name;
|
|
446
|
-
}
|
|
447
|
-
} catch (e) {
|
|
448
|
-
throw new Error(`Failed to parse id token and get profile info, ${e}`);
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
return {
|
|
452
|
-
email,
|
|
453
|
-
picture,
|
|
454
|
-
displayName
|
|
455
|
-
};
|
|
456
|
-
};
|
|
457
|
-
const executeRedirectStrategy = async (req, providerStrategy, options) => {
|
|
458
|
-
return new Promise((resolve) => {
|
|
459
|
-
const strategy = Object.create(providerStrategy);
|
|
460
|
-
strategy.redirect = (url, status) => {
|
|
461
|
-
resolve({ url, status: status ?? void 0 });
|
|
462
|
-
};
|
|
463
|
-
strategy.authenticate(req, { ...options });
|
|
464
|
-
});
|
|
465
|
-
};
|
|
466
|
-
const executeFrameHandlerStrategy = async (req, providerStrategy, options) => {
|
|
467
|
-
return new Promise(
|
|
468
|
-
(resolve, reject) => {
|
|
469
|
-
const strategy = Object.create(providerStrategy);
|
|
470
|
-
strategy.success = (result, privateInfo) => {
|
|
471
|
-
resolve({ result, privateInfo });
|
|
472
|
-
};
|
|
473
|
-
strategy.fail = (info) => {
|
|
474
|
-
reject(new Error(`Authentication rejected, ${info.message ?? ""}`));
|
|
475
|
-
};
|
|
476
|
-
strategy.error = (error) => {
|
|
477
|
-
let message = `Authentication failed, ${error.message}`;
|
|
478
|
-
if (error.oauthError?.data) {
|
|
479
|
-
try {
|
|
480
|
-
const errorData = JSON.parse(error.oauthError.data);
|
|
481
|
-
if (errorData.message) {
|
|
482
|
-
message += ` - ${errorData.message}`;
|
|
483
|
-
}
|
|
484
|
-
} catch (parseError) {
|
|
485
|
-
message += ` - ${error.oauthError}`;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
reject(new Error(message));
|
|
489
|
-
};
|
|
490
|
-
strategy.redirect = () => {
|
|
491
|
-
reject(new Error("Unexpected redirect"));
|
|
492
|
-
};
|
|
493
|
-
strategy.authenticate(req, { ...options ?? {} });
|
|
494
|
-
}
|
|
495
|
-
);
|
|
496
|
-
};
|
|
497
|
-
const executeRefreshTokenStrategy = async (providerStrategy, refreshToken, scope) => {
|
|
498
|
-
return new Promise((resolve, reject) => {
|
|
499
|
-
const anyStrategy = providerStrategy;
|
|
500
|
-
const OAuth2 = anyStrategy._oauth2.constructor;
|
|
501
|
-
const oauth2 = new OAuth2(
|
|
502
|
-
anyStrategy._oauth2._clientId,
|
|
503
|
-
anyStrategy._oauth2._clientSecret,
|
|
504
|
-
anyStrategy._oauth2._baseSite,
|
|
505
|
-
anyStrategy._oauth2._authorizeUrl,
|
|
506
|
-
anyStrategy._refreshURL || anyStrategy._oauth2._accessTokenUrl,
|
|
507
|
-
anyStrategy._oauth2._customHeaders
|
|
508
|
-
);
|
|
509
|
-
oauth2.getOAuthAccessToken(
|
|
510
|
-
refreshToken,
|
|
511
|
-
{
|
|
512
|
-
scope,
|
|
513
|
-
grant_type: "refresh_token"
|
|
514
|
-
},
|
|
515
|
-
(err, accessToken, newRefreshToken, params) => {
|
|
516
|
-
if (err) {
|
|
517
|
-
reject(new Error(`Failed to refresh access token ${err.toString()}`));
|
|
518
|
-
}
|
|
519
|
-
if (!accessToken) {
|
|
520
|
-
reject(
|
|
521
|
-
new Error(
|
|
522
|
-
`Failed to refresh access token, no access token received`
|
|
523
|
-
)
|
|
524
|
-
);
|
|
525
|
-
}
|
|
526
|
-
resolve({
|
|
527
|
-
accessToken,
|
|
528
|
-
refreshToken: newRefreshToken,
|
|
529
|
-
params
|
|
530
|
-
});
|
|
43
|
+
var passport = require('passport');
|
|
44
|
+
var config = require('@backstage/config');
|
|
45
|
+
var types = require('@backstage/types');
|
|
46
|
+
var url = require('url');
|
|
47
|
+
|
|
48
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
49
|
+
|
|
50
|
+
var express__default = /*#__PURE__*/_interopDefaultCompat(express);
|
|
51
|
+
var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
|
|
52
|
+
var cookieParser__default = /*#__PURE__*/_interopDefaultCompat(cookieParser);
|
|
53
|
+
var crypto__default = /*#__PURE__*/_interopDefaultCompat(crypto);
|
|
54
|
+
var session__default = /*#__PURE__*/_interopDefaultCompat(session);
|
|
55
|
+
var connectSessionKnex__default = /*#__PURE__*/_interopDefaultCompat(connectSessionKnex);
|
|
56
|
+
var passport__default = /*#__PURE__*/_interopDefaultCompat(passport);
|
|
57
|
+
|
|
58
|
+
function adaptLegacyOAuthHandler(authHandler) {
|
|
59
|
+
return authHandler && (async (result, ctx) => authHandler(
|
|
60
|
+
{
|
|
61
|
+
fullProfile: result.fullProfile,
|
|
62
|
+
accessToken: result.session.accessToken,
|
|
63
|
+
params: {
|
|
64
|
+
scope: result.session.scope,
|
|
65
|
+
id_token: result.session.idToken,
|
|
66
|
+
token_type: result.session.tokenType,
|
|
67
|
+
expires_in: result.session.expiresInSeconds
|
|
531
68
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
69
|
+
},
|
|
70
|
+
ctx
|
|
71
|
+
));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function adaptLegacyOAuthSignInResolver(signInResolver) {
|
|
75
|
+
return signInResolver && (async (input, ctx) => signInResolver(
|
|
76
|
+
{
|
|
77
|
+
profile: input.profile,
|
|
78
|
+
result: {
|
|
79
|
+
fullProfile: input.result.fullProfile,
|
|
80
|
+
accessToken: input.result.session.accessToken,
|
|
81
|
+
refreshToken: input.result.session.refreshToken,
|
|
82
|
+
params: {
|
|
83
|
+
scope: input.result.session.scope,
|
|
84
|
+
id_token: input.result.session.idToken,
|
|
85
|
+
token_type: input.result.session.tokenType,
|
|
86
|
+
expires_in: input.result.session.expiresInSeconds
|
|
545
87
|
}
|
|
546
88
|
}
|
|
547
|
-
);
|
|
548
|
-
});
|
|
549
|
-
};
|
|
550
|
-
|
|
551
|
-
class Auth0AuthProvider {
|
|
552
|
-
_strategy;
|
|
553
|
-
signInResolver;
|
|
554
|
-
authHandler;
|
|
555
|
-
resolverContext;
|
|
556
|
-
audience;
|
|
557
|
-
connection;
|
|
558
|
-
connectionScope;
|
|
559
|
-
/**
|
|
560
|
-
* Due to passport-auth0 forcing options.state = true,
|
|
561
|
-
* passport-oauth2 requires express-session to be installed
|
|
562
|
-
* so that the 'state' parameter of the oauth2 flow can be stored.
|
|
563
|
-
* This implementation of StateStore matches the NullStore found within
|
|
564
|
-
* passport-oauth2, which is the StateStore implementation used when options.state = false,
|
|
565
|
-
* allowing us to avoid using express-session in order to integrate with auth0.
|
|
566
|
-
*/
|
|
567
|
-
store = {
|
|
568
|
-
store(_req, cb) {
|
|
569
|
-
cb(null, null);
|
|
570
89
|
},
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
this.connection = options.connection;
|
|
581
|
-
this.connectionScope = options.connectionScope;
|
|
582
|
-
this._strategy = new Auth0Strategy(
|
|
90
|
+
ctx
|
|
91
|
+
));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function adaptOAuthSignInResolverToLegacy(resolvers) {
|
|
95
|
+
const legacyResolvers = {};
|
|
96
|
+
for (const name of Object.keys(resolvers)) {
|
|
97
|
+
const resolver = resolvers[name];
|
|
98
|
+
legacyResolvers[name] = () => async (input, ctx) => resolver(
|
|
583
99
|
{
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
done(
|
|
595
|
-
void 0,
|
|
596
|
-
{
|
|
597
|
-
fullProfile,
|
|
598
|
-
accessToken,
|
|
599
|
-
refreshToken,
|
|
600
|
-
params
|
|
601
|
-
},
|
|
602
|
-
{
|
|
603
|
-
refreshToken
|
|
100
|
+
profile: input.profile,
|
|
101
|
+
result: {
|
|
102
|
+
fullProfile: input.result.fullProfile,
|
|
103
|
+
session: {
|
|
104
|
+
accessToken: input.result.accessToken,
|
|
105
|
+
expiresInSeconds: input.result.params.expires_in,
|
|
106
|
+
scope: input.result.params.scope,
|
|
107
|
+
idToken: input.result.params.id_token,
|
|
108
|
+
tokenType: input.result.params.token_type ?? "bearer",
|
|
109
|
+
refreshToken: input.result.refreshToken
|
|
604
110
|
}
|
|
605
|
-
|
|
606
|
-
}
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
ctx
|
|
607
114
|
);
|
|
608
115
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
116
|
+
return legacyResolvers;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function createAuthProviderIntegration(config) {
|
|
120
|
+
return Object.freeze({
|
|
121
|
+
...config,
|
|
122
|
+
resolvers: Object.freeze(config.resolvers ?? {})
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const atlassian = createAuthProviderIntegration({
|
|
127
|
+
create(options) {
|
|
128
|
+
return pluginAuthNode.createOAuthProviderFactory({
|
|
129
|
+
authenticator: pluginAuthBackendModuleAtlassianProvider.atlassianAuthenticator,
|
|
130
|
+
profileTransform: adaptLegacyOAuthHandler(options?.authHandler),
|
|
131
|
+
signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver)
|
|
625
132
|
});
|
|
626
|
-
return {
|
|
627
|
-
response: await this.handleResult(result),
|
|
628
|
-
refreshToken: privateInfo.refreshToken
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
async refresh(req) {
|
|
632
|
-
const { accessToken, refreshToken, params } = await executeRefreshTokenStrategy(
|
|
633
|
-
this._strategy,
|
|
634
|
-
req.refreshToken,
|
|
635
|
-
req.scope
|
|
636
|
-
);
|
|
637
|
-
const fullProfile = await executeFetchUserProfileStrategy(
|
|
638
|
-
this._strategy,
|
|
639
|
-
accessToken
|
|
640
|
-
);
|
|
641
|
-
return {
|
|
642
|
-
response: await this.handleResult({
|
|
643
|
-
fullProfile,
|
|
644
|
-
params,
|
|
645
|
-
accessToken
|
|
646
|
-
}),
|
|
647
|
-
refreshToken
|
|
648
|
-
};
|
|
649
|
-
}
|
|
650
|
-
async handleResult(result) {
|
|
651
|
-
const { profile } = await this.authHandler(result, this.resolverContext);
|
|
652
|
-
const response = {
|
|
653
|
-
providerInfo: {
|
|
654
|
-
idToken: result.params.id_token,
|
|
655
|
-
accessToken: result.accessToken,
|
|
656
|
-
scope: result.params.scope,
|
|
657
|
-
expiresInSeconds: result.params.expires_in
|
|
658
|
-
},
|
|
659
|
-
profile
|
|
660
|
-
};
|
|
661
|
-
if (this.signInResolver) {
|
|
662
|
-
response.backstageIdentity = await this.signInResolver(
|
|
663
|
-
{
|
|
664
|
-
result,
|
|
665
|
-
profile
|
|
666
|
-
},
|
|
667
|
-
this.resolverContext
|
|
668
|
-
);
|
|
669
|
-
}
|
|
670
|
-
return response;
|
|
671
133
|
}
|
|
672
|
-
}
|
|
134
|
+
});
|
|
135
|
+
|
|
673
136
|
const auth0 = createAuthProviderIntegration({
|
|
674
137
|
create(options) {
|
|
675
|
-
return
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
const customCallbackUrl = envConfig.getOptionalString("callbackUrl");
|
|
680
|
-
const audience = envConfig.getOptionalString("audience");
|
|
681
|
-
const connection = envConfig.getOptionalString("connection");
|
|
682
|
-
const connectionScope = envConfig.getOptionalString("connectionScope");
|
|
683
|
-
const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
684
|
-
const authHandler = options?.authHandler ? options.authHandler : async ({ fullProfile, params }) => ({
|
|
685
|
-
profile: makeProfileInfo(fullProfile, params.id_token)
|
|
686
|
-
});
|
|
687
|
-
const signInResolver = options?.signIn?.resolver;
|
|
688
|
-
const provider = new Auth0AuthProvider({
|
|
689
|
-
clientId,
|
|
690
|
-
clientSecret,
|
|
691
|
-
callbackUrl,
|
|
692
|
-
domain,
|
|
693
|
-
authHandler,
|
|
694
|
-
signInResolver,
|
|
695
|
-
resolverContext,
|
|
696
|
-
audience,
|
|
697
|
-
connection,
|
|
698
|
-
connectionScope
|
|
699
|
-
});
|
|
700
|
-
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
701
|
-
providerId,
|
|
702
|
-
callbackUrl
|
|
703
|
-
});
|
|
138
|
+
return pluginAuthNode.createOAuthProviderFactory({
|
|
139
|
+
authenticator: pluginAuthBackendModuleAuth0Provider.auth0Authenticator,
|
|
140
|
+
profileTransform: adaptLegacyOAuthHandler(options?.authHandler),
|
|
141
|
+
signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver)
|
|
704
142
|
});
|
|
705
143
|
}
|
|
706
144
|
});
|
|
@@ -963,6 +401,80 @@ const onelogin = createAuthProviderIntegration({
|
|
|
963
401
|
}
|
|
964
402
|
});
|
|
965
403
|
|
|
404
|
+
const executeRedirectStrategy = async (req, providerStrategy, options) => {
|
|
405
|
+
return new Promise((resolve) => {
|
|
406
|
+
const strategy = Object.create(providerStrategy);
|
|
407
|
+
strategy.redirect = (url, status) => {
|
|
408
|
+
resolve({ url, status: status ?? void 0 });
|
|
409
|
+
};
|
|
410
|
+
strategy.authenticate(req, { ...options });
|
|
411
|
+
});
|
|
412
|
+
};
|
|
413
|
+
const executeFrameHandlerStrategy = async (req, providerStrategy, options) => {
|
|
414
|
+
return new Promise(
|
|
415
|
+
(resolve, reject) => {
|
|
416
|
+
const strategy = Object.create(providerStrategy);
|
|
417
|
+
strategy.success = (result, privateInfo) => {
|
|
418
|
+
resolve({ result, privateInfo });
|
|
419
|
+
};
|
|
420
|
+
strategy.fail = (info) => {
|
|
421
|
+
reject(new Error(`Authentication rejected, ${info.message ?? ""}`));
|
|
422
|
+
};
|
|
423
|
+
strategy.error = (error) => {
|
|
424
|
+
let message = `Authentication failed, ${error.message}`;
|
|
425
|
+
if (error.oauthError?.data) {
|
|
426
|
+
try {
|
|
427
|
+
const errorData = JSON.parse(error.oauthError.data);
|
|
428
|
+
if (errorData.message) {
|
|
429
|
+
message += ` - ${errorData.message}`;
|
|
430
|
+
}
|
|
431
|
+
} catch (parseError) {
|
|
432
|
+
message += ` - ${error.oauthError}`;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
reject(new Error(message));
|
|
436
|
+
};
|
|
437
|
+
strategy.redirect = () => {
|
|
438
|
+
reject(new Error("Unexpected redirect"));
|
|
439
|
+
};
|
|
440
|
+
strategy.authenticate(req, { ...{} });
|
|
441
|
+
}
|
|
442
|
+
);
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const safelyEncodeURIComponent = (value) => {
|
|
446
|
+
return encodeURIComponent(value).replace(/'/g, "%27");
|
|
447
|
+
};
|
|
448
|
+
const postMessageResponse = (res, appOrigin, response) => {
|
|
449
|
+
const jsonData = JSON.stringify(response);
|
|
450
|
+
const base64Data = safelyEncodeURIComponent(jsonData);
|
|
451
|
+
const base64Origin = safelyEncodeURIComponent(appOrigin);
|
|
452
|
+
const script = `
|
|
453
|
+
var authResponse = decodeURIComponent('${base64Data}');
|
|
454
|
+
var origin = decodeURIComponent('${base64Origin}');
|
|
455
|
+
var originInfo = {'type': 'config_info', 'targetOrigin': origin};
|
|
456
|
+
(window.opener || window.parent).postMessage(originInfo, '*');
|
|
457
|
+
(window.opener || window.parent).postMessage(JSON.parse(authResponse), origin);
|
|
458
|
+
setTimeout(() => {
|
|
459
|
+
window.close();
|
|
460
|
+
}, 100); // same as the interval of the core-app-api lib/loginPopup.ts (to address race conditions)
|
|
461
|
+
`;
|
|
462
|
+
const hash = crypto__default.default.createHash("sha256").update(script).digest("base64");
|
|
463
|
+
res.setHeader("Content-Type", "text/html");
|
|
464
|
+
res.setHeader("X-Frame-Options", "sameorigin");
|
|
465
|
+
res.setHeader("Content-Security-Policy", `script-src 'sha256-${hash}'`);
|
|
466
|
+
res.end(`<html><body><script>${script}<\/script></body></html>`);
|
|
467
|
+
};
|
|
468
|
+
const ensuresXRequestedWith = (req) => {
|
|
469
|
+
const requiredHeader = req.header("X-Requested-With");
|
|
470
|
+
if (!requiredHeader || requiredHeader !== "XMLHttpRequest") {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
return true;
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const prepareBackstageIdentityResponse = pluginAuthNode.prepareBackstageIdentityResponse;
|
|
477
|
+
|
|
966
478
|
class SamlAuthProvider {
|
|
967
479
|
strategy;
|
|
968
480
|
signInResolver;
|
|
@@ -1035,7 +547,7 @@ const saml = createAuthProviderIntegration({
|
|
|
1035
547
|
logoutUrl: config.getOptionalString("logoutUrl"),
|
|
1036
548
|
audience: config.getString("audience"),
|
|
1037
549
|
issuer: config.getString("issuer"),
|
|
1038
|
-
|
|
550
|
+
idpCert: config.getString("cert"),
|
|
1039
551
|
privateKey: config.getOptionalString("privateKey"),
|
|
1040
552
|
authnContext: config.getOptionalStringArray("authnContext"),
|
|
1041
553
|
identifierFormat: config.getOptionalString("identifierFormat"),
|
|
@@ -1070,175 +582,41 @@ const saml = createAuthProviderIntegration({
|
|
|
1070
582
|
};
|
|
1071
583
|
}
|
|
1072
584
|
}
|
|
1073
|
-
});
|
|
1074
|
-
|
|
1075
|
-
class BitbucketServerAuthProvider {
|
|
1076
|
-
signInResolver;
|
|
1077
|
-
authHandler;
|
|
1078
|
-
resolverContext;
|
|
1079
|
-
strategy;
|
|
1080
|
-
host;
|
|
1081
|
-
constructor(options) {
|
|
1082
|
-
this.signInResolver = options.signInResolver;
|
|
1083
|
-
this.authHandler = options.authHandler;
|
|
1084
|
-
this.resolverContext = options.resolverContext;
|
|
1085
|
-
this.strategy = new passportOauth2.Strategy(
|
|
1086
|
-
{
|
|
1087
|
-
authorizationURL: options.authorizationUrl,
|
|
1088
|
-
tokenURL: options.tokenUrl,
|
|
1089
|
-
clientID: options.clientId,
|
|
1090
|
-
clientSecret: options.clientSecret,
|
|
1091
|
-
callbackURL: options.callbackUrl
|
|
1092
|
-
},
|
|
1093
|
-
(accessToken, refreshToken, params, fullProfile, done) => {
|
|
1094
|
-
done(void 0, { fullProfile, params, accessToken }, { refreshToken });
|
|
1095
|
-
}
|
|
1096
|
-
);
|
|
1097
|
-
this.host = options.host;
|
|
1098
|
-
}
|
|
1099
|
-
async start(req) {
|
|
1100
|
-
return await executeRedirectStrategy(req, this.strategy, {
|
|
1101
|
-
accessType: "offline",
|
|
1102
|
-
prompt: "consent",
|
|
1103
|
-
scope: req.scope,
|
|
1104
|
-
state: encodeState(req.state)
|
|
1105
|
-
});
|
|
1106
|
-
}
|
|
1107
|
-
async handler(req) {
|
|
1108
|
-
const { result, privateInfo } = await executeFrameHandlerStrategy(req, this.strategy);
|
|
1109
|
-
return {
|
|
1110
|
-
response: await this.handleResult(result),
|
|
1111
|
-
refreshToken: privateInfo.refreshToken
|
|
1112
|
-
};
|
|
1113
|
-
}
|
|
1114
|
-
async refresh(req) {
|
|
1115
|
-
const { accessToken, refreshToken, params } = await executeRefreshTokenStrategy(
|
|
1116
|
-
this.strategy,
|
|
1117
|
-
req.refreshToken,
|
|
1118
|
-
req.scope
|
|
1119
|
-
);
|
|
1120
|
-
const fullProfile = await executeFetchUserProfileStrategy(
|
|
1121
|
-
this.strategy,
|
|
1122
|
-
accessToken
|
|
1123
|
-
);
|
|
1124
|
-
return {
|
|
1125
|
-
response: await this.handleResult({
|
|
1126
|
-
fullProfile,
|
|
1127
|
-
params,
|
|
1128
|
-
accessToken
|
|
1129
|
-
}),
|
|
1130
|
-
refreshToken
|
|
1131
|
-
};
|
|
1132
|
-
}
|
|
1133
|
-
async handleResult(result) {
|
|
1134
|
-
result.fullProfile = await this.fetchProfile(result);
|
|
1135
|
-
const { profile } = await this.authHandler(result, this.resolverContext);
|
|
1136
|
-
let backstageIdentity = void 0;
|
|
1137
|
-
if (this.signInResolver) {
|
|
1138
|
-
backstageIdentity = await this.signInResolver(
|
|
1139
|
-
{ result, profile },
|
|
1140
|
-
this.resolverContext
|
|
1141
|
-
);
|
|
1142
|
-
}
|
|
1143
|
-
return {
|
|
1144
|
-
providerInfo: {
|
|
1145
|
-
accessToken: result.accessToken,
|
|
1146
|
-
scope: result.params.scope,
|
|
1147
|
-
expiresInSeconds: result.params.expires_in
|
|
1148
|
-
},
|
|
1149
|
-
profile,
|
|
1150
|
-
backstageIdentity
|
|
1151
|
-
};
|
|
1152
|
-
}
|
|
1153
|
-
async fetchProfile(result) {
|
|
1154
|
-
let whoAmIResponse;
|
|
1155
|
-
try {
|
|
1156
|
-
whoAmIResponse = await fetch__default.default(
|
|
1157
|
-
`https://${this.host}/plugins/servlet/applinks/whoami`,
|
|
1158
|
-
{
|
|
1159
|
-
headers: {
|
|
1160
|
-
Authorization: `Bearer ${result.accessToken}`
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
);
|
|
1164
|
-
} catch (e) {
|
|
1165
|
-
throw new Error(`Failed to retrieve the username of the logged in user`);
|
|
1166
|
-
}
|
|
1167
|
-
const username = whoAmIResponse.headers.get("X-Ausername");
|
|
1168
|
-
if (!username) {
|
|
1169
|
-
throw new Error(`Failed to retrieve the username of the logged in user`);
|
|
1170
|
-
}
|
|
1171
|
-
let userResponse;
|
|
1172
|
-
try {
|
|
1173
|
-
userResponse = await fetch__default.default(
|
|
1174
|
-
`https://${this.host}/rest/api/latest/users/${username}?avatarSize=256`,
|
|
1175
|
-
{
|
|
1176
|
-
headers: {
|
|
1177
|
-
Authorization: `Bearer ${result.accessToken}`
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
);
|
|
1181
|
-
} catch (e) {
|
|
1182
|
-
throw new Error(`Failed to retrieve the user '${username}'`);
|
|
1183
|
-
}
|
|
1184
|
-
if (!userResponse.ok) {
|
|
1185
|
-
throw new Error(`Failed to retrieve the user '${username}'`);
|
|
1186
|
-
}
|
|
1187
|
-
const user = await userResponse.json();
|
|
1188
|
-
const passportProfile = {
|
|
1189
|
-
provider: "bitbucketServer",
|
|
1190
|
-
id: user.id.toString(),
|
|
1191
|
-
displayName: user.displayName,
|
|
1192
|
-
username: user.name,
|
|
1193
|
-
emails: [
|
|
1194
|
-
{
|
|
1195
|
-
value: user.emailAddress
|
|
1196
|
-
}
|
|
1197
|
-
]
|
|
1198
|
-
};
|
|
1199
|
-
if (user.avatarUrl) {
|
|
1200
|
-
passportProfile.photos = [
|
|
1201
|
-
{ value: `https://${this.host}${user.avatarUrl}` }
|
|
1202
|
-
];
|
|
1203
|
-
}
|
|
1204
|
-
return passportProfile;
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
585
|
+
});
|
|
586
|
+
|
|
1207
587
|
const bitbucketServer = createAuthProviderIntegration({
|
|
1208
588
|
create(options) {
|
|
1209
|
-
return
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
const customCallbackUrl = envConfig.getOptionalString("callbackUrl");
|
|
1214
|
-
const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
1215
|
-
const authorizationUrl = `https://${host}/rest/oauth2/latest/authorize`;
|
|
1216
|
-
const tokenUrl = `https://${host}/rest/oauth2/latest/token`;
|
|
1217
|
-
const authHandler = options?.authHandler ? options.authHandler : async ({ fullProfile }) => ({
|
|
1218
|
-
profile: makeProfileInfo(fullProfile)
|
|
1219
|
-
});
|
|
1220
|
-
const provider = new BitbucketServerAuthProvider({
|
|
1221
|
-
callbackUrl,
|
|
1222
|
-
clientId,
|
|
1223
|
-
clientSecret,
|
|
1224
|
-
host,
|
|
1225
|
-
authorizationUrl,
|
|
1226
|
-
tokenUrl,
|
|
1227
|
-
authHandler,
|
|
1228
|
-
signInResolver: options?.signIn?.resolver,
|
|
1229
|
-
resolverContext
|
|
1230
|
-
});
|
|
1231
|
-
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
1232
|
-
providerId,
|
|
1233
|
-
callbackUrl
|
|
1234
|
-
});
|
|
589
|
+
return pluginAuthNode.createOAuthProviderFactory({
|
|
590
|
+
authenticator: pluginAuthBackendModuleBitbucketServerProvider.bitbucketServerAuthenticator,
|
|
591
|
+
profileTransform: adaptLegacyOAuthHandler(options?.authHandler),
|
|
592
|
+
signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver)
|
|
1235
593
|
});
|
|
1236
594
|
},
|
|
1237
595
|
resolvers: {
|
|
1238
596
|
/**
|
|
1239
597
|
* Looks up the user by matching their email to the entity email.
|
|
1240
598
|
*/
|
|
1241
|
-
emailMatchingUserEntityProfileEmail: () =>
|
|
599
|
+
emailMatchingUserEntityProfileEmail: () => {
|
|
600
|
+
const resolver = pluginAuthBackendModuleBitbucketServerProvider.bitbucketServerSignInResolvers.emailMatchingUserEntityProfileEmail();
|
|
601
|
+
return async (info, ctx) => {
|
|
602
|
+
return resolver(
|
|
603
|
+
{
|
|
604
|
+
profile: info.profile,
|
|
605
|
+
result: {
|
|
606
|
+
fullProfile: info.result.fullProfile,
|
|
607
|
+
session: {
|
|
608
|
+
accessToken: info.result.accessToken,
|
|
609
|
+
tokenType: info.result.params.token_type ?? "bearer",
|
|
610
|
+
scope: info.result.params.scope,
|
|
611
|
+
expiresInSeconds: info.result.params.expires_in,
|
|
612
|
+
refreshToken: info.result.refreshToken
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
},
|
|
616
|
+
ctx
|
|
617
|
+
);
|
|
618
|
+
};
|
|
619
|
+
}
|
|
1242
620
|
}
|
|
1243
621
|
});
|
|
1244
622
|
|
|
@@ -2353,6 +1731,242 @@ const authPlugin = backendPluginApi.createBackendPlugin({
|
|
|
2353
1731
|
}
|
|
2354
1732
|
});
|
|
2355
1733
|
|
|
1734
|
+
const OAuthEnvironmentHandler = pluginAuthNode.OAuthEnvironmentHandler;
|
|
1735
|
+
|
|
1736
|
+
const readState = pluginAuthNode.decodeOAuthState;
|
|
1737
|
+
const encodeState = pluginAuthNode.encodeOAuthState;
|
|
1738
|
+
const verifyNonce = (req, providerId) => {
|
|
1739
|
+
const cookieNonce = req.cookies[`${providerId}-nonce`];
|
|
1740
|
+
const state = readState(req.query.state?.toString() ?? "");
|
|
1741
|
+
const stateNonce = state.nonce;
|
|
1742
|
+
if (!cookieNonce) {
|
|
1743
|
+
throw new Error("Auth response is missing cookie nonce");
|
|
1744
|
+
}
|
|
1745
|
+
if (stateNonce.length === 0) {
|
|
1746
|
+
throw new Error("Auth response is missing state nonce");
|
|
1747
|
+
}
|
|
1748
|
+
if (cookieNonce !== stateNonce) {
|
|
1749
|
+
throw new Error("Invalid nonce");
|
|
1750
|
+
}
|
|
1751
|
+
};
|
|
1752
|
+
const defaultCookieConfigurer = ({
|
|
1753
|
+
callbackUrl,
|
|
1754
|
+
providerId,
|
|
1755
|
+
appOrigin
|
|
1756
|
+
}) => {
|
|
1757
|
+
const { hostname: domain, pathname, protocol } = new URL(callbackUrl);
|
|
1758
|
+
const secure = protocol === "https:";
|
|
1759
|
+
let sameSite = "lax";
|
|
1760
|
+
if (new URL(appOrigin).hostname !== domain && secure) {
|
|
1761
|
+
sameSite = "none";
|
|
1762
|
+
}
|
|
1763
|
+
const path = pathname.endsWith(`${providerId}/handler/frame`) ? pathname.slice(0, -"/handler/frame".length) : `${pathname}/${providerId}`;
|
|
1764
|
+
return { domain, path, secure, sameSite };
|
|
1765
|
+
};
|
|
1766
|
+
|
|
1767
|
+
const THOUSAND_DAYS_MS = 1e3 * 24 * 60 * 60 * 1e3;
|
|
1768
|
+
const TEN_MINUTES_MS = 600 * 1e3;
|
|
1769
|
+
class OAuthAdapter {
|
|
1770
|
+
constructor(handlers, options) {
|
|
1771
|
+
this.handlers = handlers;
|
|
1772
|
+
this.options = options;
|
|
1773
|
+
this.baseCookieOptions = {
|
|
1774
|
+
httpOnly: true,
|
|
1775
|
+
sameSite: "lax"
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
static fromConfig(config, handlers, options) {
|
|
1779
|
+
const { appUrl, baseUrl, isOriginAllowed } = config;
|
|
1780
|
+
const { origin: appOrigin } = new url.URL(appUrl);
|
|
1781
|
+
const cookieConfigurer = config.cookieConfigurer ?? defaultCookieConfigurer;
|
|
1782
|
+
return new OAuthAdapter(handlers, {
|
|
1783
|
+
...options,
|
|
1784
|
+
appOrigin,
|
|
1785
|
+
baseUrl,
|
|
1786
|
+
cookieConfigurer,
|
|
1787
|
+
isOriginAllowed
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
baseCookieOptions;
|
|
1791
|
+
async start(req, res) {
|
|
1792
|
+
const scope = req.query.scope?.toString() ?? "";
|
|
1793
|
+
const env = req.query.env?.toString();
|
|
1794
|
+
const origin = req.query.origin?.toString();
|
|
1795
|
+
const redirectUrl = req.query.redirectUrl?.toString();
|
|
1796
|
+
const flow = req.query.flow?.toString();
|
|
1797
|
+
if (!env) {
|
|
1798
|
+
throw new errors.InputError("No env provided in request query parameters");
|
|
1799
|
+
}
|
|
1800
|
+
const cookieConfig = this.getCookieConfig(origin);
|
|
1801
|
+
const nonce = crypto__default.default.randomBytes(16).toString("base64");
|
|
1802
|
+
this.setNonceCookie(res, nonce, cookieConfig);
|
|
1803
|
+
const state = { nonce, env, origin, redirectUrl, flow };
|
|
1804
|
+
if (this.options.persistScopes) {
|
|
1805
|
+
state.scope = scope;
|
|
1806
|
+
}
|
|
1807
|
+
const forwardReq = Object.assign(req, { scope, state });
|
|
1808
|
+
const { url, status } = await this.handlers.start(
|
|
1809
|
+
forwardReq
|
|
1810
|
+
);
|
|
1811
|
+
res.statusCode = status || 302;
|
|
1812
|
+
res.setHeader("Location", url);
|
|
1813
|
+
res.setHeader("Content-Length", "0");
|
|
1814
|
+
res.end();
|
|
1815
|
+
}
|
|
1816
|
+
async frameHandler(req, res) {
|
|
1817
|
+
let appOrigin = this.options.appOrigin;
|
|
1818
|
+
try {
|
|
1819
|
+
const state = readState(req.query.state?.toString() ?? "");
|
|
1820
|
+
if (state.origin) {
|
|
1821
|
+
try {
|
|
1822
|
+
appOrigin = new url.URL(state.origin).origin;
|
|
1823
|
+
} catch {
|
|
1824
|
+
throw new errors.NotAllowedError("App origin is invalid, failed to parse");
|
|
1825
|
+
}
|
|
1826
|
+
if (!this.options.isOriginAllowed(appOrigin)) {
|
|
1827
|
+
throw new errors.NotAllowedError(`Origin '${appOrigin}' is not allowed`);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
verifyNonce(req, this.options.providerId);
|
|
1831
|
+
const { response, refreshToken } = await this.handlers.handler(req);
|
|
1832
|
+
const cookieConfig = this.getCookieConfig(appOrigin);
|
|
1833
|
+
if (this.options.persistScopes && state.scope) {
|
|
1834
|
+
this.setGrantedScopeCookie(res, state.scope, cookieConfig);
|
|
1835
|
+
response.providerInfo.scope = state.scope;
|
|
1836
|
+
}
|
|
1837
|
+
if (refreshToken) {
|
|
1838
|
+
this.setRefreshTokenCookie(res, refreshToken, cookieConfig);
|
|
1839
|
+
}
|
|
1840
|
+
const identity = await this.populateIdentity(response.backstageIdentity);
|
|
1841
|
+
const responseObj = {
|
|
1842
|
+
type: "authorization_response",
|
|
1843
|
+
response: { ...response, backstageIdentity: identity }
|
|
1844
|
+
};
|
|
1845
|
+
if (state.flow === "redirect") {
|
|
1846
|
+
if (!state.redirectUrl) {
|
|
1847
|
+
throw new errors.InputError(
|
|
1848
|
+
"No redirectUrl provided in request query parameters"
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
res.redirect(state.redirectUrl);
|
|
1852
|
+
return void 0;
|
|
1853
|
+
}
|
|
1854
|
+
return postMessageResponse(res, appOrigin, responseObj);
|
|
1855
|
+
} catch (error) {
|
|
1856
|
+
const { name, message } = errors.isError(error) ? error : new Error("Encountered invalid error");
|
|
1857
|
+
return postMessageResponse(res, appOrigin, {
|
|
1858
|
+
type: "authorization_response",
|
|
1859
|
+
error: { name, message }
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
async logout(req, res) {
|
|
1864
|
+
if (!ensuresXRequestedWith(req)) {
|
|
1865
|
+
throw new errors.AuthenticationError("Invalid X-Requested-With header");
|
|
1866
|
+
}
|
|
1867
|
+
if (this.handlers.logout) {
|
|
1868
|
+
const refreshToken = this.getRefreshTokenFromCookie(req);
|
|
1869
|
+
const revokeRequest = Object.assign(req, {
|
|
1870
|
+
refreshToken
|
|
1871
|
+
});
|
|
1872
|
+
await this.handlers.logout(revokeRequest);
|
|
1873
|
+
}
|
|
1874
|
+
const origin = req.get("origin");
|
|
1875
|
+
const cookieConfig = this.getCookieConfig(origin);
|
|
1876
|
+
this.removeRefreshTokenCookie(res, cookieConfig);
|
|
1877
|
+
res.status(200).end();
|
|
1878
|
+
}
|
|
1879
|
+
async refresh(req, res) {
|
|
1880
|
+
if (!ensuresXRequestedWith(req)) {
|
|
1881
|
+
throw new errors.AuthenticationError("Invalid X-Requested-With header");
|
|
1882
|
+
}
|
|
1883
|
+
if (!this.handlers.refresh) {
|
|
1884
|
+
throw new errors.InputError(
|
|
1885
|
+
`Refresh token is not supported for provider ${this.options.providerId}`
|
|
1886
|
+
);
|
|
1887
|
+
}
|
|
1888
|
+
try {
|
|
1889
|
+
const refreshToken = this.getRefreshTokenFromCookie(req);
|
|
1890
|
+
if (!refreshToken) {
|
|
1891
|
+
throw new errors.InputError("Missing session cookie");
|
|
1892
|
+
}
|
|
1893
|
+
let scope = req.query.scope?.toString() ?? "";
|
|
1894
|
+
if (this.options.persistScopes) {
|
|
1895
|
+
scope = this.getGrantedScopeFromCookie(req);
|
|
1896
|
+
}
|
|
1897
|
+
const forwardReq = Object.assign(req, { scope, refreshToken });
|
|
1898
|
+
const { response, refreshToken: newRefreshToken } = await this.handlers.refresh(forwardReq);
|
|
1899
|
+
const backstageIdentity = await this.populateIdentity(
|
|
1900
|
+
response.backstageIdentity
|
|
1901
|
+
);
|
|
1902
|
+
if (newRefreshToken && newRefreshToken !== refreshToken) {
|
|
1903
|
+
const origin = req.get("origin");
|
|
1904
|
+
const cookieConfig = this.getCookieConfig(origin);
|
|
1905
|
+
this.setRefreshTokenCookie(res, newRefreshToken, cookieConfig);
|
|
1906
|
+
}
|
|
1907
|
+
res.status(200).json({ ...response, backstageIdentity });
|
|
1908
|
+
} catch (error) {
|
|
1909
|
+
throw new errors.AuthenticationError("Refresh failed", error);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
/**
|
|
1913
|
+
* If the response from the OAuth provider includes a Backstage identity, we
|
|
1914
|
+
* make sure it's populated with all the information we can derive from the user ID.
|
|
1915
|
+
*/
|
|
1916
|
+
async populateIdentity(identity) {
|
|
1917
|
+
if (!identity) {
|
|
1918
|
+
return void 0;
|
|
1919
|
+
}
|
|
1920
|
+
if (!identity.token) {
|
|
1921
|
+
throw new errors.InputError(`Identity response must return a token`);
|
|
1922
|
+
}
|
|
1923
|
+
return prepareBackstageIdentityResponse(identity);
|
|
1924
|
+
}
|
|
1925
|
+
setNonceCookie = (res, nonce, cookieConfig) => {
|
|
1926
|
+
res.cookie(`${this.options.providerId}-nonce`, nonce, {
|
|
1927
|
+
maxAge: TEN_MINUTES_MS,
|
|
1928
|
+
...this.baseCookieOptions,
|
|
1929
|
+
...cookieConfig,
|
|
1930
|
+
path: `${cookieConfig.path}/handler`
|
|
1931
|
+
});
|
|
1932
|
+
};
|
|
1933
|
+
setGrantedScopeCookie = (res, scope, cookieConfig) => {
|
|
1934
|
+
res.cookie(`${this.options.providerId}-granted-scope`, scope, {
|
|
1935
|
+
maxAge: THOUSAND_DAYS_MS,
|
|
1936
|
+
...this.baseCookieOptions,
|
|
1937
|
+
...cookieConfig
|
|
1938
|
+
});
|
|
1939
|
+
};
|
|
1940
|
+
getRefreshTokenFromCookie = (req) => {
|
|
1941
|
+
return req.cookies[`${this.options.providerId}-refresh-token`];
|
|
1942
|
+
};
|
|
1943
|
+
getGrantedScopeFromCookie = (req) => {
|
|
1944
|
+
return req.cookies[`${this.options.providerId}-granted-scope`];
|
|
1945
|
+
};
|
|
1946
|
+
setRefreshTokenCookie = (res, refreshToken, cookieConfig) => {
|
|
1947
|
+
res.cookie(`${this.options.providerId}-refresh-token`, refreshToken, {
|
|
1948
|
+
maxAge: THOUSAND_DAYS_MS,
|
|
1949
|
+
...this.baseCookieOptions,
|
|
1950
|
+
...cookieConfig
|
|
1951
|
+
});
|
|
1952
|
+
};
|
|
1953
|
+
removeRefreshTokenCookie = (res, cookieConfig) => {
|
|
1954
|
+
res.cookie(`${this.options.providerId}-refresh-token`, "", {
|
|
1955
|
+
maxAge: 0,
|
|
1956
|
+
...this.baseCookieOptions,
|
|
1957
|
+
...cookieConfig
|
|
1958
|
+
});
|
|
1959
|
+
};
|
|
1960
|
+
getCookieConfig = (origin) => {
|
|
1961
|
+
return this.options.cookieConfigurer({
|
|
1962
|
+
providerId: this.options.providerId,
|
|
1963
|
+
baseUrl: this.options.baseUrl,
|
|
1964
|
+
callbackUrl: this.options.callbackUrl,
|
|
1965
|
+
appOrigin: origin ?? this.options.appOrigin
|
|
1966
|
+
});
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1969
|
+
|
|
2356
1970
|
exports.CatalogIdentityClient = CatalogIdentityClient;
|
|
2357
1971
|
exports.OAuthAdapter = OAuthAdapter;
|
|
2358
1972
|
exports.OAuthEnvironmentHandler = OAuthEnvironmentHandler;
|