@backstage/plugin-auth-backend 0.23.0-next.1 → 0.23.0-next.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/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 Auth0InternalStrategy = require('passport-auth0');
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 passportOauth2 = require('passport-oauth2');
32
- var fetch = require('node-fetch');
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
- function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
50
-
51
- var express__default = /*#__PURE__*/_interopDefaultCompat(express);
52
- var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
53
- var cookieParser__default = /*#__PURE__*/_interopDefaultCompat(cookieParser);
54
- var Auth0InternalStrategy__default = /*#__PURE__*/_interopDefaultCompat(Auth0InternalStrategy);
55
- var crypto__default = /*#__PURE__*/_interopDefaultCompat(crypto);
56
- var fetch__default = /*#__PURE__*/_interopDefaultCompat(fetch);
57
- var session__default = /*#__PURE__*/_interopDefaultCompat(session);
58
- var connectSessionKnex__default = /*#__PURE__*/_interopDefaultCompat(connectSessionKnex);
59
- var passport__default = /*#__PURE__*/_interopDefaultCompat(passport);
60
-
61
- function adaptLegacyOAuthHandler(authHandler) {
62
- return authHandler && (async (result, ctx) => authHandler(
63
- {
64
- fullProfile: result.fullProfile,
65
- accessToken: result.session.accessToken,
66
- params: {
67
- scope: result.session.scope,
68
- id_token: result.session.idToken,
69
- token_type: result.session.tokenType,
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
- const executeFetchUserProfileStrategy = async (providerStrategy, accessToken) => {
536
- return new Promise((resolve, reject) => {
537
- const anyStrategy = providerStrategy;
538
- anyStrategy.userProfile(
539
- accessToken,
540
- (error, rawProfile) => {
541
- if (error) {
542
- reject(error);
543
- } else {
544
- resolve(rawProfile);
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
- verify(_req, _state, cb) {
572
- cb(null, true);
573
- }
574
- };
575
- constructor(options) {
576
- this.signInResolver = options.signInResolver;
577
- this.authHandler = options.authHandler;
578
- this.resolverContext = options.resolverContext;
579
- this.audience = options.audience;
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
- clientID: options.clientId,
585
- clientSecret: options.clientSecret,
586
- callbackURL: options.callbackUrl,
587
- domain: options.domain,
588
- // We need passReqToCallback set to false to get params, but there's
589
- // no matching type signature for that, so instead behold this beauty
590
- passReqToCallback: false,
591
- store: this.store
592
- },
593
- (accessToken, refreshToken, params, fullProfile, done) => {
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
- async start(req) {
610
- return await executeRedirectStrategy(req, this._strategy, {
611
- accessType: "offline",
612
- prompt: "consent",
613
- scope: req.scope,
614
- state: encodeState(req.state),
615
- ...this.audience ? { audience: this.audience } : {},
616
- ...this.connection ? { connection: this.connection } : {},
617
- ...this.connectionScope ? { connection_scope: this.connectionScope } : {}
618
- });
619
- }
620
- async handler(req) {
621
- const { result, privateInfo } = await executeFrameHandlerStrategy(req, this._strategy, {
622
- ...this.audience ? { audience: this.audience } : {},
623
- ...this.connection ? { connection: this.connection } : {},
624
- ...this.connectionScope ? { connection_scope: this.connectionScope } : {}
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 ({ providerId, globalConfig, config, resolverContext }) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
676
- const clientId = envConfig.getString("clientId");
677
- const clientSecret = envConfig.getString("clientSecret");
678
- const domain = envConfig.getString("domain");
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
- cert: config.getString("cert"),
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 ({ providerId, globalConfig, config, resolverContext }) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
1210
- const clientId = envConfig.getString("clientId");
1211
- const clientSecret = envConfig.getString("clientSecret");
1212
- const host = envConfig.getString("host");
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: () => commonByEmailResolver
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;