@backstage/plugin-auth-backend 0.5.2 → 0.7.0-next.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 +1671 -1418
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +291 -137
- package/package.json +13 -11
package/dist/index.cjs.js
CHANGED
|
@@ -5,32 +5,32 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
5
5
|
var express = require('express');
|
|
6
6
|
var Router = require('express-promise-router');
|
|
7
7
|
var cookieParser = require('cookie-parser');
|
|
8
|
-
var
|
|
9
|
-
var jwtDecoder = require('jwt-decode');
|
|
8
|
+
var OAuth2Strategy = require('passport-oauth2');
|
|
10
9
|
var errors = require('@backstage/errors');
|
|
11
10
|
var pickBy = require('lodash/pickBy');
|
|
12
11
|
var crypto = require('crypto');
|
|
13
12
|
var url = require('url');
|
|
14
13
|
var catalogModel = require('@backstage/catalog-model');
|
|
15
|
-
var
|
|
16
|
-
var passportGoogleOauth20 = require('passport-google-oauth20');
|
|
17
|
-
var passportMicrosoft = require('passport-microsoft');
|
|
18
|
-
var got = require('got');
|
|
19
|
-
var OAuth2Strategy = require('passport-oauth2');
|
|
20
|
-
var openidClient = require('openid-client');
|
|
21
|
-
var passportOktaOauth = require('passport-okta-oauth');
|
|
22
|
-
var passportBitbucketOauth2 = require('passport-bitbucket-oauth2');
|
|
14
|
+
var jwtDecoder = require('jwt-decode');
|
|
23
15
|
var fetch = require('node-fetch');
|
|
24
16
|
var NodeCache = require('node-cache');
|
|
25
17
|
var jose = require('jose');
|
|
26
|
-
var
|
|
27
|
-
var
|
|
28
|
-
var
|
|
18
|
+
var passportBitbucketOauth2 = require('passport-bitbucket-oauth2');
|
|
19
|
+
var passportGithub2 = require('passport-github2');
|
|
20
|
+
var passportGitlab2 = require('passport-gitlab2');
|
|
21
|
+
var passportGoogleOauth20 = require('passport-google-oauth20');
|
|
22
|
+
var passportMicrosoft = require('passport-microsoft');
|
|
29
23
|
var uuid = require('uuid');
|
|
30
24
|
var luxon = require('luxon');
|
|
31
25
|
var backendCommon = require('@backstage/backend-common');
|
|
32
26
|
var firestore = require('@google-cloud/firestore');
|
|
33
27
|
var lodash = require('lodash');
|
|
28
|
+
var openidClient = require('openid-client');
|
|
29
|
+
var passportOktaOauth = require('passport-okta-oauth');
|
|
30
|
+
var passportOneloginOauth = require('passport-onelogin-oauth');
|
|
31
|
+
var passportSaml = require('passport-saml');
|
|
32
|
+
var googleAuthLibrary = require('google-auth-library');
|
|
33
|
+
var catalogClient = require('@backstage/catalog-client');
|
|
34
34
|
var session = require('express-session');
|
|
35
35
|
var passport = require('passport');
|
|
36
36
|
var minimatch = require('minimatch');
|
|
@@ -58,129 +58,69 @@ function _interopNamespace(e) {
|
|
|
58
58
|
var express__default = /*#__PURE__*/_interopDefaultLegacy(express);
|
|
59
59
|
var Router__default = /*#__PURE__*/_interopDefaultLegacy(Router);
|
|
60
60
|
var cookieParser__default = /*#__PURE__*/_interopDefaultLegacy(cookieParser);
|
|
61
|
-
var
|
|
61
|
+
var OAuth2Strategy__default = /*#__PURE__*/_interopDefaultLegacy(OAuth2Strategy);
|
|
62
62
|
var pickBy__default = /*#__PURE__*/_interopDefaultLegacy(pickBy);
|
|
63
63
|
var crypto__default = /*#__PURE__*/_interopDefaultLegacy(crypto);
|
|
64
64
|
var crypto__namespace = /*#__PURE__*/_interopNamespace(crypto);
|
|
65
|
-
var
|
|
66
|
-
var OAuth2Strategy__default = /*#__PURE__*/_interopDefaultLegacy(OAuth2Strategy);
|
|
65
|
+
var jwtDecoder__default = /*#__PURE__*/_interopDefaultLegacy(jwtDecoder);
|
|
67
66
|
var fetch__default = /*#__PURE__*/_interopDefaultLegacy(fetch);
|
|
68
67
|
var NodeCache__default = /*#__PURE__*/_interopDefaultLegacy(NodeCache);
|
|
69
68
|
var session__default = /*#__PURE__*/_interopDefaultLegacy(session);
|
|
70
69
|
var passport__default = /*#__PURE__*/_interopDefaultLegacy(passport);
|
|
71
70
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
email = firstEmail.value;
|
|
78
|
-
}
|
|
79
|
-
let picture = void 0;
|
|
80
|
-
if (profile.avatarUrl) {
|
|
81
|
-
picture = profile.avatarUrl;
|
|
82
|
-
} else if (profile.photos && profile.photos.length > 0) {
|
|
83
|
-
const [firstPhoto] = profile.photos;
|
|
84
|
-
picture = firstPhoto.value;
|
|
85
|
-
}
|
|
86
|
-
let displayName = (_b = (_a = profile.displayName) != null ? _a : profile.username) != null ? _b : profile.id;
|
|
87
|
-
if ((!email || !picture || !displayName) && idToken) {
|
|
88
|
-
try {
|
|
89
|
-
const decoded = jwtDecoder__default["default"](idToken);
|
|
90
|
-
if (!email && decoded.email) {
|
|
91
|
-
email = decoded.email;
|
|
92
|
-
}
|
|
93
|
-
if (!picture && decoded.picture) {
|
|
94
|
-
picture = decoded.picture;
|
|
95
|
-
}
|
|
96
|
-
if (!displayName && decoded.name) {
|
|
97
|
-
displayName = decoded.name;
|
|
98
|
-
}
|
|
99
|
-
} catch (e) {
|
|
100
|
-
throw new Error(`Failed to parse id token and get profile info, ${e}`);
|
|
71
|
+
const defaultScopes = ["offline_access", "read:me"];
|
|
72
|
+
class AtlassianStrategy extends OAuth2Strategy__default["default"] {
|
|
73
|
+
constructor(options, verify) {
|
|
74
|
+
if (!options.scope) {
|
|
75
|
+
throw new TypeError("Atlassian requires a scope option");
|
|
101
76
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
};
|
|
109
|
-
const executeRedirectStrategy = async (req, providerStrategy, options) => {
|
|
110
|
-
return new Promise((resolve) => {
|
|
111
|
-
const strategy = Object.create(providerStrategy);
|
|
112
|
-
strategy.redirect = (url, status) => {
|
|
113
|
-
resolve({ url, status: status != null ? status : void 0 });
|
|
114
|
-
};
|
|
115
|
-
strategy.authenticate(req, { ...options });
|
|
116
|
-
});
|
|
117
|
-
};
|
|
118
|
-
const executeFrameHandlerStrategy = async (req, providerStrategy) => {
|
|
119
|
-
return new Promise((resolve, reject) => {
|
|
120
|
-
const strategy = Object.create(providerStrategy);
|
|
121
|
-
strategy.success = (result, privateInfo) => {
|
|
122
|
-
resolve({ result, privateInfo });
|
|
123
|
-
};
|
|
124
|
-
strategy.fail = (info) => {
|
|
125
|
-
var _a;
|
|
126
|
-
reject(new Error(`Authentication rejected, ${(_a = info.message) != null ? _a : ""}`));
|
|
127
|
-
};
|
|
128
|
-
strategy.error = (error) => {
|
|
129
|
-
var _a;
|
|
130
|
-
let message = `Authentication failed, ${error.message}`;
|
|
131
|
-
if ((_a = error.oauthError) == null ? void 0 : _a.data) {
|
|
132
|
-
try {
|
|
133
|
-
const errorData = JSON.parse(error.oauthError.data);
|
|
134
|
-
if (errorData.message) {
|
|
135
|
-
message += ` - ${errorData.message}`;
|
|
136
|
-
}
|
|
137
|
-
} catch (parseError) {
|
|
138
|
-
message += ` - ${error.oauthError}`;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
reject(new Error(message));
|
|
77
|
+
const scopes = options.scope.split(" ");
|
|
78
|
+
const optionsWithURLs = {
|
|
79
|
+
...options,
|
|
80
|
+
authorizationURL: `https://auth.atlassian.com/authorize`,
|
|
81
|
+
tokenURL: `https://auth.atlassian.com/oauth/token`,
|
|
82
|
+
scope: Array.from(/* @__PURE__ */ new Set([...defaultScopes, ...scopes]))
|
|
142
83
|
};
|
|
143
|
-
|
|
144
|
-
|
|
84
|
+
super(optionsWithURLs, verify);
|
|
85
|
+
this.profileURL = "https://api.atlassian.com/me";
|
|
86
|
+
this.name = "atlassian";
|
|
87
|
+
this._oauth2.useAuthorizationHeaderforGET(true);
|
|
88
|
+
}
|
|
89
|
+
authorizationParams() {
|
|
90
|
+
return {
|
|
91
|
+
audience: "api.atlassian.com",
|
|
92
|
+
prompt: "consent"
|
|
145
93
|
};
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const executeRefreshTokenStrategy = async (providerStrategy, refreshToken, scope) => {
|
|
150
|
-
return new Promise((resolve, reject) => {
|
|
151
|
-
const anyStrategy = providerStrategy;
|
|
152
|
-
const OAuth2 = anyStrategy._oauth2.constructor;
|
|
153
|
-
const oauth2 = new OAuth2(anyStrategy._oauth2._clientId, anyStrategy._oauth2._clientSecret, anyStrategy._oauth2._baseSite, anyStrategy._oauth2._authorizeUrl, anyStrategy._refreshURL || anyStrategy._oauth2._accessTokenUrl, anyStrategy._oauth2._customHeaders);
|
|
154
|
-
oauth2.getOAuthAccessToken(refreshToken, {
|
|
155
|
-
scope,
|
|
156
|
-
grant_type: "refresh_token"
|
|
157
|
-
}, (err, accessToken, newRefreshToken, params) => {
|
|
94
|
+
}
|
|
95
|
+
userProfile(accessToken, done) {
|
|
96
|
+
this._oauth2.get(this.profileURL, accessToken, (err, body) => {
|
|
158
97
|
if (err) {
|
|
159
|
-
|
|
98
|
+
return done(new OAuth2Strategy.InternalOAuthError("Failed to fetch user profile", err.statusCode));
|
|
160
99
|
}
|
|
161
|
-
if (!
|
|
162
|
-
|
|
100
|
+
if (!body) {
|
|
101
|
+
return done(new Error("Failed to fetch user profile, body cannot be empty"));
|
|
163
102
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
});
|
|
171
|
-
};
|
|
172
|
-
const executeFetchUserProfileStrategy = async (providerStrategy, accessToken) => {
|
|
173
|
-
return new Promise((resolve, reject) => {
|
|
174
|
-
const anyStrategy = providerStrategy;
|
|
175
|
-
anyStrategy.userProfile(accessToken, (error, rawProfile) => {
|
|
176
|
-
if (error) {
|
|
177
|
-
reject(error);
|
|
178
|
-
} else {
|
|
179
|
-
resolve(rawProfile);
|
|
103
|
+
try {
|
|
104
|
+
const json = typeof body !== "string" ? body.toString() : body;
|
|
105
|
+
const profile = AtlassianStrategy.parse(json);
|
|
106
|
+
return done(null, profile);
|
|
107
|
+
} catch (e) {
|
|
108
|
+
return done(new Error("Failed to parse user profile"));
|
|
180
109
|
}
|
|
181
110
|
});
|
|
182
|
-
}
|
|
183
|
-
|
|
111
|
+
}
|
|
112
|
+
static parse(json) {
|
|
113
|
+
const resp = JSON.parse(json);
|
|
114
|
+
return {
|
|
115
|
+
id: resp.account_id,
|
|
116
|
+
provider: "atlassian",
|
|
117
|
+
username: resp.nickname,
|
|
118
|
+
displayName: resp.name,
|
|
119
|
+
emails: [{ value: resp.email }],
|
|
120
|
+
photos: [{ value: resp.picture }]
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
184
124
|
|
|
185
125
|
const readState = (stateString) => {
|
|
186
126
|
var _a, _b;
|
|
@@ -462,10 +402,10 @@ class OAuthAdapter {
|
|
|
462
402
|
}
|
|
463
403
|
const scope = (_b = (_a = req.query.scope) == null ? void 0 : _a.toString()) != null ? _b : "";
|
|
464
404
|
const forwardReq = Object.assign(req, { scope, refreshToken });
|
|
465
|
-
const response = await this.handlers.refresh(forwardReq);
|
|
405
|
+
const { response, refreshToken: newRefreshToken } = await this.handlers.refresh(forwardReq);
|
|
466
406
|
const backstageIdentity = await this.populateIdentity(response.backstageIdentity);
|
|
467
|
-
if (
|
|
468
|
-
this.setRefreshTokenCookie(res,
|
|
407
|
+
if (newRefreshToken && newRefreshToken !== refreshToken) {
|
|
408
|
+
this.setRefreshTokenCookie(res, newRefreshToken);
|
|
469
409
|
}
|
|
470
410
|
res.status(200).json({ ...response, backstageIdentity });
|
|
471
411
|
} catch (error) {
|
|
@@ -490,63 +430,176 @@ class OAuthAdapter {
|
|
|
490
430
|
}
|
|
491
431
|
}
|
|
492
432
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
433
|
+
const makeProfileInfo = (profile, idToken) => {
|
|
434
|
+
var _a, _b;
|
|
435
|
+
let email = void 0;
|
|
436
|
+
if (profile.emails && profile.emails.length > 0) {
|
|
437
|
+
const [firstEmail] = profile.emails;
|
|
438
|
+
email = firstEmail.value;
|
|
497
439
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
505
|
-
const token = await this.tokenIssuer.issueToken({
|
|
506
|
-
claims: { sub: "backstage.io/auth-backend" }
|
|
507
|
-
});
|
|
508
|
-
const { items } = await this.catalogApi.getEntities({ filter }, { token });
|
|
509
|
-
if (items.length !== 1) {
|
|
510
|
-
if (items.length > 1) {
|
|
511
|
-
throw new errors.ConflictError("User lookup resulted in multiple matches");
|
|
512
|
-
} else {
|
|
513
|
-
throw new errors.NotFoundError("User not found");
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
return items[0];
|
|
440
|
+
let picture = void 0;
|
|
441
|
+
if (profile.avatarUrl) {
|
|
442
|
+
picture = profile.avatarUrl;
|
|
443
|
+
} else if (profile.photos && profile.photos.length > 0) {
|
|
444
|
+
const [firstPhoto] = profile.photos;
|
|
445
|
+
picture = firstPhoto.value;
|
|
517
446
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
defaultNamespace: "default"
|
|
525
|
-
});
|
|
526
|
-
return parsedRef;
|
|
527
|
-
} catch {
|
|
528
|
-
logger == null ? void 0 : logger.warn(`Failed to parse entityRef from ${ref}, ignoring`);
|
|
529
|
-
return null;
|
|
447
|
+
let displayName = (_b = (_a = profile.displayName) != null ? _a : profile.username) != null ? _b : profile.id;
|
|
448
|
+
if ((!email || !picture || !displayName) && idToken) {
|
|
449
|
+
try {
|
|
450
|
+
const decoded = jwtDecoder__default["default"](idToken);
|
|
451
|
+
if (!email && decoded.email) {
|
|
452
|
+
email = decoded.email;
|
|
530
453
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
const foundEntityNames = entities.map(catalogModel.stringifyEntityRef);
|
|
540
|
-
const missingEntityNames = resolvedEntityRefs.map(catalogModel.stringifyEntityRef).filter((s) => !foundEntityNames.includes(s));
|
|
541
|
-
logger == null ? void 0 : logger.debug(`Entities not found for refs ${missingEntityNames.join()}`);
|
|
454
|
+
if (!picture && decoded.picture) {
|
|
455
|
+
picture = decoded.picture;
|
|
456
|
+
}
|
|
457
|
+
if (!displayName && decoded.name) {
|
|
458
|
+
displayName = decoded.name;
|
|
459
|
+
}
|
|
460
|
+
} catch (e) {
|
|
461
|
+
throw new Error(`Failed to parse id token and get profile info, ${e}`);
|
|
542
462
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
email,
|
|
466
|
+
picture,
|
|
467
|
+
displayName
|
|
468
|
+
};
|
|
469
|
+
};
|
|
470
|
+
const executeRedirectStrategy = async (req, providerStrategy, options) => {
|
|
471
|
+
return new Promise((resolve) => {
|
|
472
|
+
const strategy = Object.create(providerStrategy);
|
|
473
|
+
strategy.redirect = (url, status) => {
|
|
474
|
+
resolve({ url, status: status != null ? status : void 0 });
|
|
475
|
+
};
|
|
476
|
+
strategy.authenticate(req, { ...options });
|
|
477
|
+
});
|
|
478
|
+
};
|
|
479
|
+
const executeFrameHandlerStrategy = async (req, providerStrategy) => {
|
|
480
|
+
return new Promise((resolve, reject) => {
|
|
481
|
+
const strategy = Object.create(providerStrategy);
|
|
482
|
+
strategy.success = (result, privateInfo) => {
|
|
483
|
+
resolve({ result, privateInfo });
|
|
484
|
+
};
|
|
485
|
+
strategy.fail = (info) => {
|
|
486
|
+
var _a;
|
|
487
|
+
reject(new Error(`Authentication rejected, ${(_a = info.message) != null ? _a : ""}`));
|
|
488
|
+
};
|
|
489
|
+
strategy.error = (error) => {
|
|
490
|
+
var _a;
|
|
491
|
+
let message = `Authentication failed, ${error.message}`;
|
|
492
|
+
if ((_a = error.oauthError) == null ? void 0 : _a.data) {
|
|
493
|
+
try {
|
|
494
|
+
const errorData = JSON.parse(error.oauthError.data);
|
|
495
|
+
if (errorData.message) {
|
|
496
|
+
message += ` - ${errorData.message}`;
|
|
497
|
+
}
|
|
498
|
+
} catch (parseError) {
|
|
499
|
+
message += ` - ${error.oauthError}`;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
reject(new Error(message));
|
|
503
|
+
};
|
|
504
|
+
strategy.redirect = () => {
|
|
505
|
+
reject(new Error("Unexpected redirect"));
|
|
506
|
+
};
|
|
507
|
+
strategy.authenticate(req, {});
|
|
508
|
+
});
|
|
509
|
+
};
|
|
510
|
+
const executeRefreshTokenStrategy = async (providerStrategy, refreshToken, scope) => {
|
|
511
|
+
return new Promise((resolve, reject) => {
|
|
512
|
+
const anyStrategy = providerStrategy;
|
|
513
|
+
const OAuth2 = anyStrategy._oauth2.constructor;
|
|
514
|
+
const oauth2 = new OAuth2(anyStrategy._oauth2._clientId, anyStrategy._oauth2._clientSecret, anyStrategy._oauth2._baseSite, anyStrategy._oauth2._authorizeUrl, anyStrategy._refreshURL || anyStrategy._oauth2._accessTokenUrl, anyStrategy._oauth2._customHeaders);
|
|
515
|
+
oauth2.getOAuthAccessToken(refreshToken, {
|
|
516
|
+
scope,
|
|
517
|
+
grant_type: "refresh_token"
|
|
518
|
+
}, (err, accessToken, newRefreshToken, params) => {
|
|
519
|
+
if (err) {
|
|
520
|
+
reject(new Error(`Failed to refresh access token ${err.toString()}`));
|
|
521
|
+
}
|
|
522
|
+
if (!accessToken) {
|
|
523
|
+
reject(new Error(`Failed to refresh access token, no access token received`));
|
|
524
|
+
}
|
|
525
|
+
resolve({
|
|
526
|
+
accessToken,
|
|
527
|
+
refreshToken: newRefreshToken,
|
|
528
|
+
params
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
};
|
|
533
|
+
const executeFetchUserProfileStrategy = async (providerStrategy, accessToken) => {
|
|
534
|
+
return new Promise((resolve, reject) => {
|
|
535
|
+
const anyStrategy = providerStrategy;
|
|
536
|
+
anyStrategy.userProfile(accessToken, (error, rawProfile) => {
|
|
537
|
+
if (error) {
|
|
538
|
+
reject(error);
|
|
539
|
+
} else {
|
|
540
|
+
resolve(rawProfile);
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
class CatalogIdentityClient {
|
|
547
|
+
constructor(options) {
|
|
548
|
+
this.catalogApi = options.catalogApi;
|
|
549
|
+
this.tokenIssuer = options.tokenIssuer;
|
|
550
|
+
}
|
|
551
|
+
async findUser(query) {
|
|
552
|
+
const filter = {
|
|
553
|
+
kind: "user"
|
|
554
|
+
};
|
|
555
|
+
for (const [key, value] of Object.entries(query.annotations)) {
|
|
556
|
+
filter[`metadata.annotations.${key}`] = value;
|
|
557
|
+
}
|
|
558
|
+
const token = await this.tokenIssuer.issueToken({
|
|
559
|
+
claims: { sub: "backstage.io/auth-backend" }
|
|
560
|
+
});
|
|
561
|
+
const { items } = await this.catalogApi.getEntities({ filter }, { token });
|
|
562
|
+
if (items.length !== 1) {
|
|
563
|
+
if (items.length > 1) {
|
|
564
|
+
throw new errors.ConflictError("User lookup resulted in multiple matches");
|
|
565
|
+
} else {
|
|
566
|
+
throw new errors.NotFoundError("User not found");
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return items[0];
|
|
570
|
+
}
|
|
571
|
+
async resolveCatalogMembership(query) {
|
|
572
|
+
const { entityRefs, logger } = query;
|
|
573
|
+
const resolvedEntityRefs = entityRefs.map((ref) => {
|
|
574
|
+
try {
|
|
575
|
+
const parsedRef = catalogModel.parseEntityRef(ref.toLocaleLowerCase("en-US"), {
|
|
576
|
+
defaultKind: "user",
|
|
577
|
+
defaultNamespace: "default"
|
|
578
|
+
});
|
|
579
|
+
return parsedRef;
|
|
580
|
+
} catch {
|
|
581
|
+
logger == null ? void 0 : logger.warn(`Failed to parse entityRef from ${ref}, ignoring`);
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
}).filter((ref) => ref !== null);
|
|
585
|
+
const filter = resolvedEntityRefs.map((ref) => ({
|
|
586
|
+
kind: ref.kind,
|
|
587
|
+
"metadata.namespace": ref.namespace,
|
|
588
|
+
"metadata.name": ref.name
|
|
589
|
+
}));
|
|
590
|
+
const entities = await this.catalogApi.getEntities({ filter }).then((r) => r.items);
|
|
591
|
+
if (entityRefs.length !== entities.length) {
|
|
592
|
+
const foundEntityNames = entities.map(catalogModel.stringifyEntityRef);
|
|
593
|
+
const missingEntityNames = resolvedEntityRefs.map(catalogModel.stringifyEntityRef).filter((s) => !foundEntityNames.includes(s));
|
|
594
|
+
logger == null ? void 0 : logger.debug(`Entities not found for refs ${missingEntityNames.join()}`);
|
|
595
|
+
}
|
|
596
|
+
const memberOf = entities.flatMap((e) => {
|
|
597
|
+
var _a, _b;
|
|
598
|
+
return (_b = (_a = e.relations) == null ? void 0 : _a.filter((r) => r.type === catalogModel.RELATION_MEMBER_OF).map((r) => r.target)) != null ? _b : [];
|
|
599
|
+
});
|
|
600
|
+
const newEntityRefs = [
|
|
601
|
+
...new Set(resolvedEntityRefs.concat(memberOf).map(catalogModel.stringifyEntityRef))
|
|
602
|
+
];
|
|
550
603
|
logger == null ? void 0 : logger.debug(`Found catalog membership: ${newEntityRefs.join()}`);
|
|
551
604
|
return newEntityRefs;
|
|
552
605
|
}
|
|
@@ -562,61 +615,58 @@ function getEntityClaims(entity) {
|
|
|
562
615
|
};
|
|
563
616
|
}
|
|
564
617
|
|
|
565
|
-
|
|
618
|
+
const atlassianDefaultAuthHandler = async ({
|
|
619
|
+
fullProfile,
|
|
620
|
+
params
|
|
621
|
+
}) => ({
|
|
622
|
+
profile: makeProfileInfo(fullProfile, params.id_token)
|
|
623
|
+
});
|
|
624
|
+
class AtlassianAuthProvider {
|
|
566
625
|
constructor(options) {
|
|
567
|
-
this.signInResolver = options.signInResolver;
|
|
568
|
-
this.authHandler = options.authHandler;
|
|
569
|
-
this.stateEncoder = options.stateEncoder;
|
|
570
|
-
this.tokenIssuer = options.tokenIssuer;
|
|
571
626
|
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
572
627
|
this.logger = options.logger;
|
|
573
|
-
this.
|
|
628
|
+
this.tokenIssuer = options.tokenIssuer;
|
|
629
|
+
this.authHandler = options.authHandler;
|
|
630
|
+
this.signInResolver = options.signInResolver;
|
|
631
|
+
this._strategy = new AtlassianStrategy({
|
|
574
632
|
clientID: options.clientId,
|
|
575
633
|
clientSecret: options.clientSecret,
|
|
576
634
|
callbackURL: options.callbackUrl,
|
|
577
|
-
|
|
578
|
-
userProfileURL: options.userProfileUrl,
|
|
579
|
-
authorizationURL: options.authorizationUrl
|
|
635
|
+
scope: options.scopes
|
|
580
636
|
}, (accessToken, refreshToken, params, fullProfile, done) => {
|
|
581
|
-
done(void 0, {
|
|
637
|
+
done(void 0, {
|
|
638
|
+
fullProfile,
|
|
639
|
+
accessToken,
|
|
640
|
+
refreshToken,
|
|
641
|
+
params
|
|
642
|
+
});
|
|
582
643
|
});
|
|
583
644
|
}
|
|
584
645
|
async start(req) {
|
|
585
646
|
return await executeRedirectStrategy(req, this._strategy, {
|
|
586
|
-
|
|
587
|
-
state: (await this.stateEncoder(req)).encodedState
|
|
647
|
+
state: encodeState(req.state)
|
|
588
648
|
});
|
|
589
649
|
}
|
|
590
650
|
async handler(req) {
|
|
591
|
-
const { result
|
|
651
|
+
const { result } = await executeFrameHandlerStrategy(req, this._strategy);
|
|
592
652
|
return {
|
|
593
653
|
response: await this.handleResult(result),
|
|
594
|
-
refreshToken:
|
|
654
|
+
refreshToken: result.refreshToken
|
|
595
655
|
};
|
|
596
656
|
}
|
|
597
|
-
async refresh(req) {
|
|
598
|
-
const {
|
|
599
|
-
accessToken,
|
|
600
|
-
refreshToken: newRefreshToken,
|
|
601
|
-
params
|
|
602
|
-
} = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
|
|
603
|
-
const fullProfile = await executeFetchUserProfileStrategy(this._strategy, accessToken);
|
|
604
|
-
return this.handleResult({
|
|
605
|
-
fullProfile,
|
|
606
|
-
params,
|
|
607
|
-
accessToken,
|
|
608
|
-
refreshToken: newRefreshToken
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
657
|
async handleResult(result) {
|
|
612
|
-
const
|
|
613
|
-
|
|
658
|
+
const context = {
|
|
659
|
+
logger: this.logger,
|
|
660
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
661
|
+
tokenIssuer: this.tokenIssuer
|
|
662
|
+
};
|
|
663
|
+
const { profile } = await this.authHandler(result, context);
|
|
614
664
|
const response = {
|
|
615
665
|
providerInfo: {
|
|
666
|
+
idToken: result.params.id_token,
|
|
616
667
|
accessToken: result.accessToken,
|
|
617
|
-
refreshToken: result.refreshToken,
|
|
618
668
|
scope: result.params.scope,
|
|
619
|
-
expiresInSeconds:
|
|
669
|
+
expiresInSeconds: result.params.expires_in
|
|
620
670
|
},
|
|
621
671
|
profile
|
|
622
672
|
};
|
|
@@ -624,24 +674,24 @@ class GithubAuthProvider {
|
|
|
624
674
|
response.backstageIdentity = await this.signInResolver({
|
|
625
675
|
result,
|
|
626
676
|
profile
|
|
627
|
-
},
|
|
628
|
-
tokenIssuer: this.tokenIssuer,
|
|
629
|
-
catalogIdentityClient: this.catalogIdentityClient,
|
|
630
|
-
logger: this.logger
|
|
631
|
-
});
|
|
677
|
+
}, context);
|
|
632
678
|
}
|
|
633
679
|
return response;
|
|
634
680
|
}
|
|
681
|
+
async refresh(req) {
|
|
682
|
+
const { accessToken, params, refreshToken } = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
|
|
683
|
+
const fullProfile = await executeFetchUserProfileStrategy(this._strategy, accessToken);
|
|
684
|
+
return {
|
|
685
|
+
response: await this.handleResult({
|
|
686
|
+
fullProfile,
|
|
687
|
+
params,
|
|
688
|
+
accessToken
|
|
689
|
+
}),
|
|
690
|
+
refreshToken
|
|
691
|
+
};
|
|
692
|
+
}
|
|
635
693
|
}
|
|
636
|
-
const
|
|
637
|
-
const { fullProfile } = info.result;
|
|
638
|
-
const userId = fullProfile.username || fullProfile.id;
|
|
639
|
-
const token = await ctx.tokenIssuer.issueToken({
|
|
640
|
-
claims: { sub: userId, ent: [`user:default/${userId}`] }
|
|
641
|
-
});
|
|
642
|
-
return { id: userId, token };
|
|
643
|
-
};
|
|
644
|
-
const createGithubProvider = (options) => {
|
|
694
|
+
const createAtlassianProvider = (options) => {
|
|
645
695
|
return ({
|
|
646
696
|
providerId,
|
|
647
697
|
globalConfig,
|
|
@@ -650,90 +700,76 @@ const createGithubProvider = (options) => {
|
|
|
650
700
|
catalogApi,
|
|
651
701
|
logger
|
|
652
702
|
}) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
|
|
653
|
-
var _a, _b
|
|
703
|
+
var _a, _b;
|
|
654
704
|
const clientId = envConfig.getString("clientId");
|
|
655
705
|
const clientSecret = envConfig.getString("clientSecret");
|
|
656
|
-
const
|
|
657
|
-
const
|
|
658
|
-
const authorizationUrl = enterpriseInstanceUrl ? `${enterpriseInstanceUrl}/login/oauth/authorize` : void 0;
|
|
659
|
-
const tokenUrl = enterpriseInstanceUrl ? `${enterpriseInstanceUrl}/login/oauth/access_token` : void 0;
|
|
660
|
-
const userProfileUrl = enterpriseInstanceUrl ? `${enterpriseInstanceUrl}/api/v3/user` : void 0;
|
|
661
|
-
const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
706
|
+
const scopes = envConfig.getString("scopes");
|
|
707
|
+
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
662
708
|
const catalogIdentityClient = new CatalogIdentityClient({
|
|
663
709
|
catalogApi,
|
|
664
710
|
tokenIssuer
|
|
665
711
|
});
|
|
666
|
-
const authHandler = (options == null ? void 0 : options.authHandler) ?
|
|
667
|
-
|
|
668
|
-
});
|
|
669
|
-
const signInResolverFn = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : githubDefaultSignInResolver;
|
|
670
|
-
const signInResolver = (info) => signInResolverFn(info, {
|
|
671
|
-
catalogIdentityClient,
|
|
672
|
-
tokenIssuer,
|
|
673
|
-
logger
|
|
674
|
-
});
|
|
675
|
-
const stateEncoder = (_c = options == null ? void 0 : options.stateEncoder) != null ? _c : async (req) => {
|
|
676
|
-
return { encodedState: encodeState(req.state) };
|
|
677
|
-
};
|
|
678
|
-
const provider = new GithubAuthProvider({
|
|
712
|
+
const authHandler = (_a = options == null ? void 0 : options.authHandler) != null ? _a : atlassianDefaultAuthHandler;
|
|
713
|
+
const provider = new AtlassianAuthProvider({
|
|
679
714
|
clientId,
|
|
680
715
|
clientSecret,
|
|
716
|
+
scopes,
|
|
681
717
|
callbackUrl,
|
|
682
|
-
tokenUrl,
|
|
683
|
-
userProfileUrl,
|
|
684
|
-
authorizationUrl,
|
|
685
|
-
signInResolver,
|
|
686
718
|
authHandler,
|
|
687
|
-
|
|
719
|
+
signInResolver: (_b = options == null ? void 0 : options.signIn) == null ? void 0 : _b.resolver,
|
|
688
720
|
catalogIdentityClient,
|
|
689
|
-
|
|
690
|
-
|
|
721
|
+
logger,
|
|
722
|
+
tokenIssuer
|
|
691
723
|
});
|
|
692
724
|
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
693
|
-
|
|
725
|
+
disableRefresh: true,
|
|
694
726
|
providerId,
|
|
695
727
|
tokenIssuer
|
|
696
728
|
});
|
|
697
729
|
});
|
|
698
730
|
};
|
|
699
731
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
}) => ({
|
|
715
|
-
profile: makeProfileInfo(fullProfile, params.id_token)
|
|
716
|
-
});
|
|
717
|
-
class GitlabAuthProvider {
|
|
732
|
+
class Auth0Strategy extends OAuth2Strategy__default["default"] {
|
|
733
|
+
constructor(options, verify) {
|
|
734
|
+
const optionsWithURLs = {
|
|
735
|
+
...options,
|
|
736
|
+
authorizationURL: `https://${options.domain}/authorize`,
|
|
737
|
+
tokenURL: `https://${options.domain}/oauth/token`,
|
|
738
|
+
userInfoURL: `https://${options.domain}/userinfo`,
|
|
739
|
+
apiUrl: `https://${options.domain}/api`
|
|
740
|
+
};
|
|
741
|
+
super(optionsWithURLs, verify);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
class Auth0AuthProvider {
|
|
718
746
|
constructor(options) {
|
|
747
|
+
this.signInResolver = options.signInResolver;
|
|
748
|
+
this.authHandler = options.authHandler;
|
|
749
|
+
this.tokenIssuer = options.tokenIssuer;
|
|
719
750
|
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
720
751
|
this.logger = options.logger;
|
|
721
|
-
this.
|
|
722
|
-
this.authHandler = options.authHandler;
|
|
723
|
-
this.signInResolver = options.signInResolver;
|
|
724
|
-
this._strategy = new passportGitlab2.Strategy({
|
|
752
|
+
this._strategy = new Auth0Strategy({
|
|
725
753
|
clientID: options.clientId,
|
|
726
754
|
clientSecret: options.clientSecret,
|
|
727
755
|
callbackURL: options.callbackUrl,
|
|
728
|
-
|
|
756
|
+
domain: options.domain,
|
|
757
|
+
passReqToCallback: false
|
|
729
758
|
}, (accessToken, refreshToken, params, fullProfile, done) => {
|
|
730
|
-
done(void 0, {
|
|
759
|
+
done(void 0, {
|
|
760
|
+
fullProfile,
|
|
761
|
+
accessToken,
|
|
762
|
+
refreshToken,
|
|
763
|
+
params
|
|
764
|
+
}, {
|
|
731
765
|
refreshToken
|
|
732
766
|
});
|
|
733
767
|
});
|
|
734
768
|
}
|
|
735
769
|
async start(req) {
|
|
736
770
|
return await executeRedirectStrategy(req, this._strategy, {
|
|
771
|
+
accessType: "offline",
|
|
772
|
+
prompt: "consent",
|
|
737
773
|
scope: req.scope,
|
|
738
774
|
state: encodeState(req.state)
|
|
739
775
|
});
|
|
@@ -746,26 +782,28 @@ class GitlabAuthProvider {
|
|
|
746
782
|
};
|
|
747
783
|
}
|
|
748
784
|
async refresh(req) {
|
|
749
|
-
const {
|
|
750
|
-
accessToken,
|
|
751
|
-
refreshToken: newRefreshToken,
|
|
752
|
-
params
|
|
753
|
-
} = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
|
|
785
|
+
const { accessToken, refreshToken, params } = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
|
|
754
786
|
const fullProfile = await executeFetchUserProfileStrategy(this._strategy, accessToken);
|
|
755
|
-
return
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
787
|
+
return {
|
|
788
|
+
response: await this.handleResult({
|
|
789
|
+
fullProfile,
|
|
790
|
+
params,
|
|
791
|
+
accessToken
|
|
792
|
+
}),
|
|
793
|
+
refreshToken
|
|
794
|
+
};
|
|
761
795
|
}
|
|
762
796
|
async handleResult(result) {
|
|
763
|
-
const
|
|
797
|
+
const context = {
|
|
798
|
+
logger: this.logger,
|
|
799
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
800
|
+
tokenIssuer: this.tokenIssuer
|
|
801
|
+
};
|
|
802
|
+
const { profile } = await this.authHandler(result, context);
|
|
764
803
|
const response = {
|
|
765
804
|
providerInfo: {
|
|
766
805
|
idToken: result.params.id_token,
|
|
767
806
|
accessToken: result.accessToken,
|
|
768
|
-
refreshToken: result.refreshToken,
|
|
769
807
|
scope: result.params.scope,
|
|
770
808
|
expiresInSeconds: result.params.expires_in
|
|
771
809
|
},
|
|
@@ -775,16 +813,20 @@ class GitlabAuthProvider {
|
|
|
775
813
|
response.backstageIdentity = await this.signInResolver({
|
|
776
814
|
result,
|
|
777
815
|
profile
|
|
778
|
-
},
|
|
779
|
-
tokenIssuer: this.tokenIssuer,
|
|
780
|
-
catalogIdentityClient: this.catalogIdentityClient,
|
|
781
|
-
logger: this.logger
|
|
782
|
-
});
|
|
816
|
+
}, context);
|
|
783
817
|
}
|
|
784
818
|
return response;
|
|
785
819
|
}
|
|
786
820
|
}
|
|
787
|
-
const
|
|
821
|
+
const defaultSignInResolver$1 = async (info) => {
|
|
822
|
+
const { profile } = info;
|
|
823
|
+
if (!profile.email) {
|
|
824
|
+
throw new Error("Profile does not contain an email");
|
|
825
|
+
}
|
|
826
|
+
const id = profile.email.split("@")[0];
|
|
827
|
+
return { id, token: "" };
|
|
828
|
+
};
|
|
829
|
+
const createAuth0Provider = (options) => {
|
|
788
830
|
return ({
|
|
789
831
|
providerId,
|
|
790
832
|
globalConfig,
|
|
@@ -793,50 +835,175 @@ const createGitlabProvider = (options) => {
|
|
|
793
835
|
catalogApi,
|
|
794
836
|
logger
|
|
795
837
|
}) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
|
|
796
|
-
var _a, _b
|
|
838
|
+
var _a, _b;
|
|
797
839
|
const clientId = envConfig.getString("clientId");
|
|
798
840
|
const clientSecret = envConfig.getString("clientSecret");
|
|
799
|
-
const
|
|
800
|
-
const baseUrl = audience || "https://gitlab.com";
|
|
841
|
+
const domain = envConfig.getString("domain");
|
|
801
842
|
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
802
843
|
const catalogIdentityClient = new CatalogIdentityClient({
|
|
803
844
|
catalogApi,
|
|
804
845
|
tokenIssuer
|
|
805
846
|
});
|
|
806
|
-
const authHandler = (
|
|
807
|
-
|
|
808
|
-
const signInResolver = (info) => signInResolverFn(info, {
|
|
809
|
-
catalogIdentityClient,
|
|
810
|
-
tokenIssuer,
|
|
811
|
-
logger
|
|
847
|
+
const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ fullProfile, params }) => ({
|
|
848
|
+
profile: makeProfileInfo(fullProfile, params.id_token)
|
|
812
849
|
});
|
|
813
|
-
const
|
|
850
|
+
const signInResolver = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : defaultSignInResolver$1;
|
|
851
|
+
const provider = new Auth0AuthProvider({
|
|
814
852
|
clientId,
|
|
815
853
|
clientSecret,
|
|
816
854
|
callbackUrl,
|
|
817
|
-
|
|
855
|
+
domain,
|
|
818
856
|
authHandler,
|
|
819
857
|
signInResolver,
|
|
858
|
+
tokenIssuer,
|
|
820
859
|
catalogIdentityClient,
|
|
821
|
-
logger
|
|
822
|
-
tokenIssuer
|
|
860
|
+
logger
|
|
823
861
|
});
|
|
824
862
|
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
825
|
-
disableRefresh:
|
|
863
|
+
disableRefresh: true,
|
|
826
864
|
providerId,
|
|
827
865
|
tokenIssuer
|
|
828
866
|
});
|
|
829
867
|
});
|
|
830
868
|
};
|
|
831
869
|
|
|
832
|
-
|
|
870
|
+
const ALB_JWT_HEADER = "x-amzn-oidc-data";
|
|
871
|
+
const ALB_ACCESS_TOKEN_HEADER = "x-amzn-oidc-accesstoken";
|
|
872
|
+
const getJWTHeaders = (input) => {
|
|
873
|
+
const encoded = input.split(".")[0];
|
|
874
|
+
return JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
|
|
875
|
+
};
|
|
876
|
+
class AwsAlbAuthProvider {
|
|
877
|
+
constructor(options) {
|
|
878
|
+
this.region = options.region;
|
|
879
|
+
this.issuer = options.issuer;
|
|
880
|
+
this.authHandler = options.authHandler;
|
|
881
|
+
this.signInResolver = options.signInResolver;
|
|
882
|
+
this.tokenIssuer = options.tokenIssuer;
|
|
883
|
+
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
884
|
+
this.logger = options.logger;
|
|
885
|
+
this.keyCache = new NodeCache__default["default"]({ stdTTL: 3600 });
|
|
886
|
+
}
|
|
887
|
+
frameHandler() {
|
|
888
|
+
return Promise.resolve(void 0);
|
|
889
|
+
}
|
|
890
|
+
async refresh(req, res) {
|
|
891
|
+
try {
|
|
892
|
+
const result = await this.getResult(req);
|
|
893
|
+
const response = await this.handleResult(result);
|
|
894
|
+
res.json(response);
|
|
895
|
+
} catch (e) {
|
|
896
|
+
this.logger.error("Exception occurred during AWS ALB token refresh", e);
|
|
897
|
+
res.status(401);
|
|
898
|
+
res.end();
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
start() {
|
|
902
|
+
return Promise.resolve(void 0);
|
|
903
|
+
}
|
|
904
|
+
async getResult(req) {
|
|
905
|
+
const jwt = req.header(ALB_JWT_HEADER);
|
|
906
|
+
const accessToken = req.header(ALB_ACCESS_TOKEN_HEADER);
|
|
907
|
+
if (jwt === void 0) {
|
|
908
|
+
throw new errors.AuthenticationError(`Missing ALB OIDC header: ${ALB_JWT_HEADER}`);
|
|
909
|
+
}
|
|
910
|
+
if (accessToken === void 0) {
|
|
911
|
+
throw new errors.AuthenticationError(`Missing ALB OIDC header: ${ALB_ACCESS_TOKEN_HEADER}`);
|
|
912
|
+
}
|
|
913
|
+
try {
|
|
914
|
+
const headers = getJWTHeaders(jwt);
|
|
915
|
+
const key = await this.getKey(headers.kid);
|
|
916
|
+
const claims = jose.JWT.verify(jwt, key);
|
|
917
|
+
if (this.issuer && claims.iss !== this.issuer) {
|
|
918
|
+
throw new errors.AuthenticationError("Issuer mismatch on JWT token");
|
|
919
|
+
}
|
|
920
|
+
const fullProfile = {
|
|
921
|
+
provider: "unknown",
|
|
922
|
+
id: claims.sub,
|
|
923
|
+
displayName: claims.name,
|
|
924
|
+
username: claims.email.split("@")[0].toLowerCase(),
|
|
925
|
+
name: {
|
|
926
|
+
familyName: claims.family_name,
|
|
927
|
+
givenName: claims.given_name
|
|
928
|
+
},
|
|
929
|
+
emails: [{ value: claims.email.toLowerCase() }],
|
|
930
|
+
photos: [{ value: claims.picture }]
|
|
931
|
+
};
|
|
932
|
+
return {
|
|
933
|
+
fullProfile,
|
|
934
|
+
expiresInSeconds: claims.exp,
|
|
935
|
+
accessToken
|
|
936
|
+
};
|
|
937
|
+
} catch (e) {
|
|
938
|
+
throw new Error(`Exception occurred during JWT processing: ${e}`);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
async handleResult(result) {
|
|
942
|
+
const context = {
|
|
943
|
+
tokenIssuer: this.tokenIssuer,
|
|
944
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
945
|
+
logger: this.logger
|
|
946
|
+
};
|
|
947
|
+
const { profile } = await this.authHandler(result, context);
|
|
948
|
+
const backstageIdentity = await this.signInResolver({
|
|
949
|
+
result,
|
|
950
|
+
profile
|
|
951
|
+
}, context);
|
|
952
|
+
return {
|
|
953
|
+
providerInfo: {
|
|
954
|
+
accessToken: result.accessToken,
|
|
955
|
+
expiresInSeconds: result.expiresInSeconds
|
|
956
|
+
},
|
|
957
|
+
backstageIdentity: prepareBackstageIdentityResponse(backstageIdentity),
|
|
958
|
+
profile
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
async getKey(keyId) {
|
|
962
|
+
const optionalCacheKey = this.keyCache.get(keyId);
|
|
963
|
+
if (optionalCacheKey) {
|
|
964
|
+
return crypto__namespace.createPublicKey(optionalCacheKey);
|
|
965
|
+
}
|
|
966
|
+
const keyText = await fetch__default["default"](`https://public-keys.auth.elb.${this.region}.amazonaws.com/${keyId}`).then((response) => response.text());
|
|
967
|
+
const keyValue = crypto__namespace.createPublicKey(keyText);
|
|
968
|
+
this.keyCache.set(keyId, keyValue.export({ format: "pem", type: "spki" }));
|
|
969
|
+
return keyValue;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
const createAwsAlbProvider = (options) => {
|
|
973
|
+
return ({ config, tokenIssuer, catalogApi, logger }) => {
|
|
974
|
+
const region = config.getString("region");
|
|
975
|
+
const issuer = config.getOptionalString("iss");
|
|
976
|
+
if ((options == null ? void 0 : options.signIn.resolver) === void 0) {
|
|
977
|
+
throw new Error("SignInResolver is required to use this authentication provider");
|
|
978
|
+
}
|
|
979
|
+
const catalogIdentityClient = new CatalogIdentityClient({
|
|
980
|
+
catalogApi,
|
|
981
|
+
tokenIssuer
|
|
982
|
+
});
|
|
983
|
+
const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ fullProfile }) => ({
|
|
984
|
+
profile: makeProfileInfo(fullProfile)
|
|
985
|
+
});
|
|
986
|
+
const signInResolver = options == null ? void 0 : options.signIn.resolver;
|
|
987
|
+
return new AwsAlbAuthProvider({
|
|
988
|
+
region,
|
|
989
|
+
issuer,
|
|
990
|
+
signInResolver,
|
|
991
|
+
authHandler,
|
|
992
|
+
tokenIssuer,
|
|
993
|
+
catalogIdentityClient,
|
|
994
|
+
logger
|
|
995
|
+
});
|
|
996
|
+
};
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
class BitbucketAuthProvider {
|
|
833
1000
|
constructor(options) {
|
|
834
1001
|
this.signInResolver = options.signInResolver;
|
|
835
1002
|
this.authHandler = options.authHandler;
|
|
836
1003
|
this.tokenIssuer = options.tokenIssuer;
|
|
837
1004
|
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
838
1005
|
this.logger = options.logger;
|
|
839
|
-
this._strategy = new
|
|
1006
|
+
this._strategy = new passportBitbucketOauth2.Strategy({
|
|
840
1007
|
clientID: options.clientId,
|
|
841
1008
|
clientSecret: options.clientSecret,
|
|
842
1009
|
callbackURL: options.callbackUrl,
|
|
@@ -868,17 +1035,25 @@ class GoogleAuthProvider {
|
|
|
868
1035
|
};
|
|
869
1036
|
}
|
|
870
1037
|
async refresh(req) {
|
|
871
|
-
const { accessToken, params } = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
|
|
1038
|
+
const { accessToken, refreshToken, params } = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
|
|
872
1039
|
const fullProfile = await executeFetchUserProfileStrategy(this._strategy, accessToken);
|
|
873
|
-
return
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1040
|
+
return {
|
|
1041
|
+
response: await this.handleResult({
|
|
1042
|
+
fullProfile,
|
|
1043
|
+
params,
|
|
1044
|
+
accessToken
|
|
1045
|
+
}),
|
|
1046
|
+
refreshToken
|
|
1047
|
+
};
|
|
879
1048
|
}
|
|
880
1049
|
async handleResult(result) {
|
|
881
|
-
|
|
1050
|
+
result.fullProfile.avatarUrl = result.fullProfile._json.links.avatar.href;
|
|
1051
|
+
const context = {
|
|
1052
|
+
logger: this.logger,
|
|
1053
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
1054
|
+
tokenIssuer: this.tokenIssuer
|
|
1055
|
+
};
|
|
1056
|
+
const { profile } = await this.authHandler(result, context);
|
|
882
1057
|
const response = {
|
|
883
1058
|
providerInfo: {
|
|
884
1059
|
idToken: result.params.id_token,
|
|
@@ -892,52 +1067,40 @@ class GoogleAuthProvider {
|
|
|
892
1067
|
response.backstageIdentity = await this.signInResolver({
|
|
893
1068
|
result,
|
|
894
1069
|
profile
|
|
895
|
-
},
|
|
896
|
-
tokenIssuer: this.tokenIssuer,
|
|
897
|
-
catalogIdentityClient: this.catalogIdentityClient,
|
|
898
|
-
logger: this.logger
|
|
899
|
-
});
|
|
1070
|
+
}, context);
|
|
900
1071
|
}
|
|
901
1072
|
return response;
|
|
902
1073
|
}
|
|
903
1074
|
}
|
|
904
|
-
const
|
|
905
|
-
const {
|
|
906
|
-
if (!
|
|
907
|
-
throw new Error("
|
|
1075
|
+
const bitbucketUsernameSignInResolver = async (info, ctx) => {
|
|
1076
|
+
const { result } = info;
|
|
1077
|
+
if (!result.fullProfile.username) {
|
|
1078
|
+
throw new Error("Bitbucket profile contained no Username");
|
|
908
1079
|
}
|
|
909
1080
|
const entity = await ctx.catalogIdentityClient.findUser({
|
|
910
1081
|
annotations: {
|
|
911
|
-
"
|
|
1082
|
+
"bitbucket.org/username": result.fullProfile.username
|
|
912
1083
|
}
|
|
913
1084
|
});
|
|
914
1085
|
const claims = getEntityClaims(entity);
|
|
915
1086
|
const token = await ctx.tokenIssuer.issueToken({ claims });
|
|
916
1087
|
return { id: entity.metadata.name, entity, token };
|
|
917
1088
|
};
|
|
918
|
-
const
|
|
919
|
-
const {
|
|
920
|
-
if (!
|
|
921
|
-
throw new Error("
|
|
922
|
-
}
|
|
923
|
-
let userId;
|
|
924
|
-
try {
|
|
925
|
-
const entity = await ctx.catalogIdentityClient.findUser({
|
|
926
|
-
annotations: {
|
|
927
|
-
"google.com/email": profile.email
|
|
928
|
-
}
|
|
929
|
-
});
|
|
930
|
-
userId = entity.metadata.name;
|
|
931
|
-
} catch (error) {
|
|
932
|
-
ctx.logger.warn(`Failed to look up user, ${error}, falling back to allowing login based on email pattern, this will probably break in the future`);
|
|
933
|
-
userId = profile.email.split("@")[0];
|
|
1089
|
+
const bitbucketUserIdSignInResolver = async (info, ctx) => {
|
|
1090
|
+
const { result } = info;
|
|
1091
|
+
if (!result.fullProfile.id) {
|
|
1092
|
+
throw new Error("Bitbucket profile contained no User ID");
|
|
934
1093
|
}
|
|
935
|
-
const
|
|
936
|
-
|
|
1094
|
+
const entity = await ctx.catalogIdentityClient.findUser({
|
|
1095
|
+
annotations: {
|
|
1096
|
+
"bitbucket.org/user-id": result.fullProfile.id
|
|
1097
|
+
}
|
|
937
1098
|
});
|
|
938
|
-
|
|
1099
|
+
const claims = getEntityClaims(entity);
|
|
1100
|
+
const token = await ctx.tokenIssuer.issueToken({ claims });
|
|
1101
|
+
return { id: entity.metadata.name, entity, token };
|
|
939
1102
|
};
|
|
940
|
-
const
|
|
1103
|
+
const createBitbucketProvider = (options) => {
|
|
941
1104
|
return ({
|
|
942
1105
|
providerId,
|
|
943
1106
|
globalConfig,
|
|
@@ -946,7 +1109,7 @@ const createGoogleProvider = (options) => {
|
|
|
946
1109
|
catalogApi,
|
|
947
1110
|
logger
|
|
948
1111
|
}) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
|
|
949
|
-
var _a
|
|
1112
|
+
var _a;
|
|
950
1113
|
const clientId = envConfig.getString("clientId");
|
|
951
1114
|
const clientSecret = envConfig.getString("clientSecret");
|
|
952
1115
|
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
@@ -957,17 +1120,11 @@ const createGoogleProvider = (options) => {
|
|
|
957
1120
|
const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ fullProfile, params }) => ({
|
|
958
1121
|
profile: makeProfileInfo(fullProfile, params.id_token)
|
|
959
1122
|
});
|
|
960
|
-
const
|
|
961
|
-
const signInResolver = (info) => signInResolverFn(info, {
|
|
962
|
-
catalogIdentityClient,
|
|
963
|
-
tokenIssuer,
|
|
964
|
-
logger
|
|
965
|
-
});
|
|
966
|
-
const provider = new GoogleAuthProvider({
|
|
1123
|
+
const provider = new BitbucketAuthProvider({
|
|
967
1124
|
clientId,
|
|
968
1125
|
clientSecret,
|
|
969
1126
|
callbackUrl,
|
|
970
|
-
signInResolver,
|
|
1127
|
+
signInResolver: (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver,
|
|
971
1128
|
authHandler,
|
|
972
1129
|
tokenIssuer,
|
|
973
1130
|
catalogIdentityClient,
|
|
@@ -981,28 +1138,29 @@ const createGoogleProvider = (options) => {
|
|
|
981
1138
|
});
|
|
982
1139
|
};
|
|
983
1140
|
|
|
984
|
-
class
|
|
1141
|
+
class GithubAuthProvider {
|
|
985
1142
|
constructor(options) {
|
|
986
1143
|
this.signInResolver = options.signInResolver;
|
|
987
1144
|
this.authHandler = options.authHandler;
|
|
1145
|
+
this.stateEncoder = options.stateEncoder;
|
|
988
1146
|
this.tokenIssuer = options.tokenIssuer;
|
|
989
|
-
this.logger = options.logger;
|
|
990
1147
|
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
991
|
-
this.
|
|
1148
|
+
this.logger = options.logger;
|
|
1149
|
+
this._strategy = new passportGithub2.Strategy({
|
|
992
1150
|
clientID: options.clientId,
|
|
993
1151
|
clientSecret: options.clientSecret,
|
|
994
1152
|
callbackURL: options.callbackUrl,
|
|
995
|
-
authorizationURL: options.authorizationUrl,
|
|
996
1153
|
tokenURL: options.tokenUrl,
|
|
997
|
-
|
|
1154
|
+
userProfileURL: options.userProfileUrl,
|
|
1155
|
+
authorizationURL: options.authorizationUrl
|
|
998
1156
|
}, (accessToken, refreshToken, params, fullProfile, done) => {
|
|
999
|
-
done(void 0, { fullProfile,
|
|
1157
|
+
done(void 0, { fullProfile, params, accessToken }, { refreshToken });
|
|
1000
1158
|
});
|
|
1001
1159
|
}
|
|
1002
1160
|
async start(req) {
|
|
1003
1161
|
return await executeRedirectStrategy(req, this._strategy, {
|
|
1004
1162
|
scope: req.scope,
|
|
1005
|
-
state:
|
|
1163
|
+
state: (await this.stateEncoder(req)).encodedState
|
|
1006
1164
|
});
|
|
1007
1165
|
}
|
|
1008
1166
|
async handler(req) {
|
|
@@ -1013,25 +1171,30 @@ class MicrosoftAuthProvider {
|
|
|
1013
1171
|
};
|
|
1014
1172
|
}
|
|
1015
1173
|
async refresh(req) {
|
|
1016
|
-
const { accessToken, params } = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
|
|
1174
|
+
const { accessToken, refreshToken, params } = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
|
|
1017
1175
|
const fullProfile = await executeFetchUserProfileStrategy(this._strategy, accessToken);
|
|
1018
|
-
return
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1176
|
+
return {
|
|
1177
|
+
response: await this.handleResult({
|
|
1178
|
+
fullProfile,
|
|
1179
|
+
params,
|
|
1180
|
+
accessToken
|
|
1181
|
+
}),
|
|
1182
|
+
refreshToken
|
|
1183
|
+
};
|
|
1024
1184
|
}
|
|
1025
1185
|
async handleResult(result) {
|
|
1026
|
-
const
|
|
1027
|
-
|
|
1028
|
-
|
|
1186
|
+
const context = {
|
|
1187
|
+
logger: this.logger,
|
|
1188
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
1189
|
+
tokenIssuer: this.tokenIssuer
|
|
1190
|
+
};
|
|
1191
|
+
const { profile } = await this.authHandler(result, context);
|
|
1192
|
+
const expiresInStr = result.params.expires_in;
|
|
1029
1193
|
const response = {
|
|
1030
1194
|
providerInfo: {
|
|
1031
|
-
idToken: result.params.id_token,
|
|
1032
1195
|
accessToken: result.accessToken,
|
|
1033
1196
|
scope: result.params.scope,
|
|
1034
|
-
expiresInSeconds:
|
|
1197
|
+
expiresInSeconds: expiresInStr === void 0 ? void 0 : Number(expiresInStr)
|
|
1035
1198
|
},
|
|
1036
1199
|
profile
|
|
1037
1200
|
};
|
|
@@ -1039,58 +1202,20 @@ class MicrosoftAuthProvider {
|
|
|
1039
1202
|
response.backstageIdentity = await this.signInResolver({
|
|
1040
1203
|
result,
|
|
1041
1204
|
profile
|
|
1042
|
-
},
|
|
1043
|
-
tokenIssuer: this.tokenIssuer,
|
|
1044
|
-
catalogIdentityClient: this.catalogIdentityClient,
|
|
1045
|
-
logger: this.logger
|
|
1046
|
-
});
|
|
1205
|
+
}, context);
|
|
1047
1206
|
}
|
|
1048
1207
|
return response;
|
|
1049
1208
|
}
|
|
1050
|
-
getUserPhoto(accessToken) {
|
|
1051
|
-
return new Promise((resolve) => {
|
|
1052
|
-
got__default["default"].get("https://graph.microsoft.com/v1.0/me/photos/48x48/$value", {
|
|
1053
|
-
encoding: "binary",
|
|
1054
|
-
responseType: "buffer",
|
|
1055
|
-
headers: {
|
|
1056
|
-
Authorization: `Bearer ${accessToken}`
|
|
1057
|
-
}
|
|
1058
|
-
}).then((photoData) => {
|
|
1059
|
-
const photoURL = `data:image/jpeg;base64,${Buffer.from(photoData.body).toString("base64")}`;
|
|
1060
|
-
resolve(photoURL);
|
|
1061
|
-
}).catch((error) => {
|
|
1062
|
-
this.logger.warn(`Could not retrieve user profile photo from Microsoft Graph API: ${error}`);
|
|
1063
|
-
resolve(void 0);
|
|
1064
|
-
});
|
|
1065
|
-
});
|
|
1066
|
-
}
|
|
1067
1209
|
}
|
|
1068
|
-
const
|
|
1069
|
-
const {
|
|
1070
|
-
|
|
1071
|
-
throw new Error("Microsoft profile contained no email");
|
|
1072
|
-
}
|
|
1073
|
-
const entity = await ctx.catalogIdentityClient.findUser({
|
|
1074
|
-
annotations: {
|
|
1075
|
-
"microsoft.com/email": profile.email
|
|
1076
|
-
}
|
|
1077
|
-
});
|
|
1078
|
-
const claims = getEntityClaims(entity);
|
|
1079
|
-
const token = await ctx.tokenIssuer.issueToken({ claims });
|
|
1080
|
-
return { id: entity.metadata.name, entity, token };
|
|
1081
|
-
};
|
|
1082
|
-
const microsoftDefaultSignInResolver = async (info, ctx) => {
|
|
1083
|
-
const { profile } = info;
|
|
1084
|
-
if (!profile.email) {
|
|
1085
|
-
throw new Error("Profile contained no email");
|
|
1086
|
-
}
|
|
1087
|
-
const userId = profile.email.split("@")[0];
|
|
1210
|
+
const githubDefaultSignInResolver = async (info, ctx) => {
|
|
1211
|
+
const { fullProfile } = info.result;
|
|
1212
|
+
const userId = fullProfile.username || fullProfile.id;
|
|
1088
1213
|
const token = await ctx.tokenIssuer.issueToken({
|
|
1089
1214
|
claims: { sub: userId, ent: [`user:default/${userId}`] }
|
|
1090
1215
|
});
|
|
1091
1216
|
return { id: userId, token };
|
|
1092
1217
|
};
|
|
1093
|
-
const
|
|
1218
|
+
const createGithubProvider = (options) => {
|
|
1094
1219
|
return ({
|
|
1095
1220
|
providerId,
|
|
1096
1221
|
globalConfig,
|
|
@@ -1099,79 +1224,90 @@ const createMicrosoftProvider = (options) => {
|
|
|
1099
1224
|
catalogApi,
|
|
1100
1225
|
logger
|
|
1101
1226
|
}) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
|
|
1102
|
-
var _a, _b;
|
|
1227
|
+
var _a, _b, _c;
|
|
1103
1228
|
const clientId = envConfig.getString("clientId");
|
|
1104
1229
|
const clientSecret = envConfig.getString("clientSecret");
|
|
1105
|
-
const
|
|
1106
|
-
const
|
|
1107
|
-
const authorizationUrl =
|
|
1108
|
-
const tokenUrl =
|
|
1230
|
+
const enterpriseInstanceUrl = envConfig.getOptionalString("enterpriseInstanceUrl");
|
|
1231
|
+
const customCallbackUrl = envConfig.getOptionalString("callbackUrl");
|
|
1232
|
+
const authorizationUrl = enterpriseInstanceUrl ? `${enterpriseInstanceUrl}/login/oauth/authorize` : void 0;
|
|
1233
|
+
const tokenUrl = enterpriseInstanceUrl ? `${enterpriseInstanceUrl}/login/oauth/access_token` : void 0;
|
|
1234
|
+
const userProfileUrl = enterpriseInstanceUrl ? `${enterpriseInstanceUrl}/api/v3/user` : void 0;
|
|
1235
|
+
const callbackUrl = customCallbackUrl || `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
1109
1236
|
const catalogIdentityClient = new CatalogIdentityClient({
|
|
1110
1237
|
catalogApi,
|
|
1111
1238
|
tokenIssuer
|
|
1112
1239
|
});
|
|
1113
|
-
const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ fullProfile
|
|
1114
|
-
profile: makeProfileInfo(fullProfile
|
|
1240
|
+
const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ fullProfile }) => ({
|
|
1241
|
+
profile: makeProfileInfo(fullProfile)
|
|
1115
1242
|
});
|
|
1116
|
-
const signInResolverFn = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b :
|
|
1243
|
+
const signInResolverFn = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : githubDefaultSignInResolver;
|
|
1117
1244
|
const signInResolver = (info) => signInResolverFn(info, {
|
|
1118
1245
|
catalogIdentityClient,
|
|
1119
1246
|
tokenIssuer,
|
|
1120
1247
|
logger
|
|
1121
1248
|
});
|
|
1122
|
-
const
|
|
1249
|
+
const stateEncoder = (_c = options == null ? void 0 : options.stateEncoder) != null ? _c : async (req) => {
|
|
1250
|
+
return { encodedState: encodeState(req.state) };
|
|
1251
|
+
};
|
|
1252
|
+
const provider = new GithubAuthProvider({
|
|
1123
1253
|
clientId,
|
|
1124
1254
|
clientSecret,
|
|
1125
1255
|
callbackUrl,
|
|
1126
|
-
authorizationUrl,
|
|
1127
1256
|
tokenUrl,
|
|
1128
|
-
|
|
1257
|
+
userProfileUrl,
|
|
1258
|
+
authorizationUrl,
|
|
1129
1259
|
signInResolver,
|
|
1260
|
+
authHandler,
|
|
1261
|
+
tokenIssuer,
|
|
1130
1262
|
catalogIdentityClient,
|
|
1131
|
-
|
|
1132
|
-
|
|
1263
|
+
stateEncoder,
|
|
1264
|
+
logger
|
|
1133
1265
|
});
|
|
1134
1266
|
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
1135
|
-
|
|
1267
|
+
persistScopes: true,
|
|
1136
1268
|
providerId,
|
|
1137
1269
|
tokenIssuer
|
|
1138
1270
|
});
|
|
1139
1271
|
});
|
|
1140
1272
|
};
|
|
1141
1273
|
|
|
1142
|
-
|
|
1274
|
+
const gitlabDefaultSignInResolver = async (info, ctx) => {
|
|
1275
|
+
const { profile, result } = info;
|
|
1276
|
+
let id = result.fullProfile.id;
|
|
1277
|
+
if (profile.email) {
|
|
1278
|
+
id = profile.email.split("@")[0];
|
|
1279
|
+
}
|
|
1280
|
+
const token = await ctx.tokenIssuer.issueToken({
|
|
1281
|
+
claims: { sub: id, ent: [`user:default/${id}`] }
|
|
1282
|
+
});
|
|
1283
|
+
return { id, token };
|
|
1284
|
+
};
|
|
1285
|
+
const gitlabDefaultAuthHandler = async ({
|
|
1286
|
+
fullProfile,
|
|
1287
|
+
params
|
|
1288
|
+
}) => ({
|
|
1289
|
+
profile: makeProfileInfo(fullProfile, params.id_token)
|
|
1290
|
+
});
|
|
1291
|
+
class GitlabAuthProvider {
|
|
1143
1292
|
constructor(options) {
|
|
1144
|
-
this.signInResolver = options.signInResolver;
|
|
1145
|
-
this.authHandler = options.authHandler;
|
|
1146
|
-
this.tokenIssuer = options.tokenIssuer;
|
|
1147
1293
|
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
1148
1294
|
this.logger = options.logger;
|
|
1149
|
-
this.
|
|
1295
|
+
this.tokenIssuer = options.tokenIssuer;
|
|
1296
|
+
this.authHandler = options.authHandler;
|
|
1297
|
+
this.signInResolver = options.signInResolver;
|
|
1298
|
+
this._strategy = new passportGitlab2.Strategy({
|
|
1150
1299
|
clientID: options.clientId,
|
|
1151
1300
|
clientSecret: options.clientSecret,
|
|
1152
1301
|
callbackURL: options.callbackUrl,
|
|
1153
|
-
|
|
1154
|
-
tokenURL: options.tokenUrl,
|
|
1155
|
-
passReqToCallback: false,
|
|
1156
|
-
scope: options.scope,
|
|
1157
|
-
customHeaders: options.includeBasicAuth ? {
|
|
1158
|
-
Authorization: `Basic ${this.encodeClientCredentials(options.clientId, options.clientSecret)}`
|
|
1159
|
-
} : void 0
|
|
1302
|
+
baseURL: options.baseUrl
|
|
1160
1303
|
}, (accessToken, refreshToken, params, fullProfile, done) => {
|
|
1161
|
-
done(void 0, {
|
|
1162
|
-
fullProfile,
|
|
1163
|
-
accessToken,
|
|
1164
|
-
refreshToken,
|
|
1165
|
-
params
|
|
1166
|
-
}, {
|
|
1304
|
+
done(void 0, { fullProfile, params, accessToken }, {
|
|
1167
1305
|
refreshToken
|
|
1168
1306
|
});
|
|
1169
1307
|
});
|
|
1170
1308
|
}
|
|
1171
1309
|
async start(req) {
|
|
1172
1310
|
return await executeRedirectStrategy(req, this._strategy, {
|
|
1173
|
-
accessType: "offline",
|
|
1174
|
-
prompt: "consent",
|
|
1175
1311
|
scope: req.scope,
|
|
1176
1312
|
state: encodeState(req.state)
|
|
1177
1313
|
});
|
|
@@ -1184,29 +1320,30 @@ class OAuth2AuthProvider {
|
|
|
1184
1320
|
};
|
|
1185
1321
|
}
|
|
1186
1322
|
async refresh(req) {
|
|
1187
|
-
const
|
|
1188
|
-
const {
|
|
1189
|
-
accessToken,
|
|
1190
|
-
params,
|
|
1191
|
-
refreshToken: updatedRefreshToken
|
|
1192
|
-
} = refreshTokenResponse;
|
|
1323
|
+
const { accessToken, refreshToken, params } = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
|
|
1193
1324
|
const fullProfile = await executeFetchUserProfileStrategy(this._strategy, accessToken);
|
|
1194
|
-
return
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1325
|
+
return {
|
|
1326
|
+
response: await this.handleResult({
|
|
1327
|
+
fullProfile,
|
|
1328
|
+
params,
|
|
1329
|
+
accessToken
|
|
1330
|
+
}),
|
|
1331
|
+
refreshToken
|
|
1332
|
+
};
|
|
1200
1333
|
}
|
|
1201
1334
|
async handleResult(result) {
|
|
1202
|
-
const
|
|
1335
|
+
const context = {
|
|
1336
|
+
logger: this.logger,
|
|
1337
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
1338
|
+
tokenIssuer: this.tokenIssuer
|
|
1339
|
+
};
|
|
1340
|
+
const { profile } = await this.authHandler(result, context);
|
|
1203
1341
|
const response = {
|
|
1204
1342
|
providerInfo: {
|
|
1205
1343
|
idToken: result.params.id_token,
|
|
1206
1344
|
accessToken: result.accessToken,
|
|
1207
1345
|
scope: result.params.scope,
|
|
1208
|
-
expiresInSeconds: result.params.expires_in
|
|
1209
|
-
refreshToken: result.refreshToken
|
|
1346
|
+
expiresInSeconds: result.params.expires_in
|
|
1210
1347
|
},
|
|
1211
1348
|
profile
|
|
1212
1349
|
};
|
|
@@ -1214,30 +1351,12 @@ class OAuth2AuthProvider {
|
|
|
1214
1351
|
response.backstageIdentity = await this.signInResolver({
|
|
1215
1352
|
result,
|
|
1216
1353
|
profile
|
|
1217
|
-
},
|
|
1218
|
-
tokenIssuer: this.tokenIssuer,
|
|
1219
|
-
catalogIdentityClient: this.catalogIdentityClient,
|
|
1220
|
-
logger: this.logger
|
|
1221
|
-
});
|
|
1354
|
+
}, context);
|
|
1222
1355
|
}
|
|
1223
1356
|
return response;
|
|
1224
1357
|
}
|
|
1225
|
-
encodeClientCredentials(clientID, clientSecret) {
|
|
1226
|
-
return Buffer.from(`${clientID}:${clientSecret}`).toString("base64");
|
|
1227
|
-
}
|
|
1228
1358
|
}
|
|
1229
|
-
const
|
|
1230
|
-
const { profile } = info;
|
|
1231
|
-
if (!profile.email) {
|
|
1232
|
-
throw new Error("Profile contained no email");
|
|
1233
|
-
}
|
|
1234
|
-
const userId = profile.email.split("@")[0];
|
|
1235
|
-
const token = await ctx.tokenIssuer.issueToken({
|
|
1236
|
-
claims: { sub: userId, ent: [`user:default/${userId}`] }
|
|
1237
|
-
});
|
|
1238
|
-
return { id: userId, token };
|
|
1239
|
-
};
|
|
1240
|
-
const createOAuth2Provider = (options) => {
|
|
1359
|
+
const createGitlabProvider = (options) => {
|
|
1241
1360
|
return ({
|
|
1242
1361
|
providerId,
|
|
1243
1362
|
globalConfig,
|
|
@@ -1249,126 +1368,102 @@ const createOAuth2Provider = (options) => {
|
|
|
1249
1368
|
var _a, _b, _c;
|
|
1250
1369
|
const clientId = envConfig.getString("clientId");
|
|
1251
1370
|
const clientSecret = envConfig.getString("clientSecret");
|
|
1371
|
+
const audience = envConfig.getOptionalString("audience");
|
|
1372
|
+
const baseUrl = audience || "https://gitlab.com";
|
|
1252
1373
|
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
1253
|
-
const authorizationUrl = envConfig.getString("authorizationUrl");
|
|
1254
|
-
const tokenUrl = envConfig.getString("tokenUrl");
|
|
1255
|
-
const scope = envConfig.getOptionalString("scope");
|
|
1256
|
-
const includeBasicAuth = envConfig.getOptionalBoolean("includeBasicAuth");
|
|
1257
|
-
const disableRefresh = (_a = envConfig.getOptionalBoolean("disableRefresh")) != null ? _a : false;
|
|
1258
1374
|
const catalogIdentityClient = new CatalogIdentityClient({
|
|
1259
1375
|
catalogApi,
|
|
1260
1376
|
tokenIssuer
|
|
1261
1377
|
});
|
|
1262
|
-
const authHandler = (options == null ? void 0 : options.authHandler) ?
|
|
1263
|
-
|
|
1264
|
-
});
|
|
1265
|
-
const signInResolverFn = (_c = (_b = options == null ? void 0 : options.signIn) == null ? void 0 : _b.resolver) != null ? _c : oAuth2DefaultSignInResolver$1;
|
|
1378
|
+
const authHandler = (_a = options == null ? void 0 : options.authHandler) != null ? _a : gitlabDefaultAuthHandler;
|
|
1379
|
+
const signInResolverFn = (_c = (_b = options == null ? void 0 : options.signIn) == null ? void 0 : _b.resolver) != null ? _c : gitlabDefaultSignInResolver;
|
|
1266
1380
|
const signInResolver = (info) => signInResolverFn(info, {
|
|
1267
1381
|
catalogIdentityClient,
|
|
1268
1382
|
tokenIssuer,
|
|
1269
1383
|
logger
|
|
1270
1384
|
});
|
|
1271
|
-
const provider = new
|
|
1385
|
+
const provider = new GitlabAuthProvider({
|
|
1272
1386
|
clientId,
|
|
1273
1387
|
clientSecret,
|
|
1274
|
-
tokenIssuer,
|
|
1275
|
-
catalogIdentityClient,
|
|
1276
1388
|
callbackUrl,
|
|
1277
|
-
|
|
1389
|
+
baseUrl,
|
|
1278
1390
|
authHandler,
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
scope,
|
|
1391
|
+
signInResolver,
|
|
1392
|
+
catalogIdentityClient,
|
|
1282
1393
|
logger,
|
|
1283
|
-
|
|
1394
|
+
tokenIssuer
|
|
1284
1395
|
});
|
|
1285
1396
|
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
1286
|
-
disableRefresh,
|
|
1397
|
+
disableRefresh: false,
|
|
1287
1398
|
providerId,
|
|
1288
1399
|
tokenIssuer
|
|
1289
1400
|
});
|
|
1290
1401
|
});
|
|
1291
1402
|
};
|
|
1292
1403
|
|
|
1293
|
-
class
|
|
1404
|
+
class GoogleAuthProvider {
|
|
1294
1405
|
constructor(options) {
|
|
1295
|
-
this.implementation = this.setupStrategy(options);
|
|
1296
|
-
this.scope = options.scope;
|
|
1297
|
-
this.prompt = options.prompt;
|
|
1298
1406
|
this.signInResolver = options.signInResolver;
|
|
1299
1407
|
this.authHandler = options.authHandler;
|
|
1300
1408
|
this.tokenIssuer = options.tokenIssuer;
|
|
1301
1409
|
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
1302
1410
|
this.logger = options.logger;
|
|
1411
|
+
this._strategy = new passportGoogleOauth20.Strategy({
|
|
1412
|
+
clientID: options.clientId,
|
|
1413
|
+
clientSecret: options.clientSecret,
|
|
1414
|
+
callbackURL: options.callbackUrl,
|
|
1415
|
+
passReqToCallback: false
|
|
1416
|
+
}, (accessToken, refreshToken, params, fullProfile, done) => {
|
|
1417
|
+
done(void 0, {
|
|
1418
|
+
fullProfile,
|
|
1419
|
+
params,
|
|
1420
|
+
accessToken,
|
|
1421
|
+
refreshToken
|
|
1422
|
+
}, {
|
|
1423
|
+
refreshToken
|
|
1424
|
+
});
|
|
1425
|
+
});
|
|
1303
1426
|
}
|
|
1304
1427
|
async start(req) {
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1428
|
+
return await executeRedirectStrategy(req, this._strategy, {
|
|
1429
|
+
accessType: "offline",
|
|
1430
|
+
prompt: "consent",
|
|
1431
|
+
scope: req.scope,
|
|
1308
1432
|
state: encodeState(req.state)
|
|
1309
|
-
};
|
|
1310
|
-
const prompt = this.prompt || "none";
|
|
1311
|
-
if (prompt !== "auto") {
|
|
1312
|
-
options.prompt = prompt;
|
|
1313
|
-
}
|
|
1314
|
-
return await executeRedirectStrategy(req, strategy, options);
|
|
1433
|
+
});
|
|
1315
1434
|
}
|
|
1316
1435
|
async handler(req) {
|
|
1317
|
-
const {
|
|
1318
|
-
const strategyResponse = await executeFrameHandlerStrategy(req, strategy);
|
|
1319
|
-
const {
|
|
1320
|
-
result: { userinfo, tokenset },
|
|
1321
|
-
privateInfo
|
|
1322
|
-
} = strategyResponse;
|
|
1323
|
-
const identityResponse = await this.handleResult({ tokenset, userinfo });
|
|
1436
|
+
const { result, privateInfo } = await executeFrameHandlerStrategy(req, this._strategy);
|
|
1324
1437
|
return {
|
|
1325
|
-
response:
|
|
1438
|
+
response: await this.handleResult(result),
|
|
1326
1439
|
refreshToken: privateInfo.refreshToken
|
|
1327
1440
|
};
|
|
1328
1441
|
}
|
|
1329
1442
|
async refresh(req) {
|
|
1330
|
-
const {
|
|
1331
|
-
const
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
const client = new issuer.Client({
|
|
1341
|
-
access_type: "offline",
|
|
1342
|
-
client_id: options.clientId,
|
|
1343
|
-
client_secret: options.clientSecret,
|
|
1344
|
-
redirect_uris: [options.callbackUrl],
|
|
1345
|
-
response_types: ["code"],
|
|
1346
|
-
id_token_signed_response_alg: options.tokenSignedResponseAlg || "RS256",
|
|
1347
|
-
scope: options.scope || ""
|
|
1348
|
-
});
|
|
1349
|
-
const strategy = new openidClient.Strategy({
|
|
1350
|
-
client,
|
|
1351
|
-
passReqToCallback: false
|
|
1352
|
-
}, (tokenset, userinfo, done) => {
|
|
1353
|
-
if (typeof done !== "function") {
|
|
1354
|
-
throw new Error("OIDC IdP must provide a userinfo_endpoint in the metadata response");
|
|
1355
|
-
}
|
|
1356
|
-
done(void 0, { tokenset, userinfo }, {
|
|
1357
|
-
refreshToken: tokenset.refresh_token
|
|
1358
|
-
});
|
|
1359
|
-
});
|
|
1360
|
-
strategy.error = console.error;
|
|
1361
|
-
return { strategy, client };
|
|
1443
|
+
const { accessToken, refreshToken, params } = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
|
|
1444
|
+
const fullProfile = await executeFetchUserProfileStrategy(this._strategy, accessToken);
|
|
1445
|
+
return {
|
|
1446
|
+
response: await this.handleResult({
|
|
1447
|
+
fullProfile,
|
|
1448
|
+
params,
|
|
1449
|
+
accessToken
|
|
1450
|
+
}),
|
|
1451
|
+
refreshToken
|
|
1452
|
+
};
|
|
1362
1453
|
}
|
|
1363
1454
|
async handleResult(result) {
|
|
1364
|
-
const
|
|
1455
|
+
const context = {
|
|
1456
|
+
logger: this.logger,
|
|
1457
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
1458
|
+
tokenIssuer: this.tokenIssuer
|
|
1459
|
+
};
|
|
1460
|
+
const { profile } = await this.authHandler(result, context);
|
|
1365
1461
|
const response = {
|
|
1366
1462
|
providerInfo: {
|
|
1367
|
-
idToken: result.
|
|
1368
|
-
accessToken: result.
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
expiresInSeconds: result.tokenset.expires_in
|
|
1463
|
+
idToken: result.params.id_token,
|
|
1464
|
+
accessToken: result.accessToken,
|
|
1465
|
+
scope: result.params.scope,
|
|
1466
|
+
expiresInSeconds: result.params.expires_in
|
|
1372
1467
|
},
|
|
1373
1468
|
profile
|
|
1374
1469
|
};
|
|
@@ -1376,27 +1471,48 @@ class OidcAuthProvider {
|
|
|
1376
1471
|
response.backstageIdentity = await this.signInResolver({
|
|
1377
1472
|
result,
|
|
1378
1473
|
profile
|
|
1379
|
-
},
|
|
1380
|
-
tokenIssuer: this.tokenIssuer,
|
|
1381
|
-
catalogIdentityClient: this.catalogIdentityClient,
|
|
1382
|
-
logger: this.logger
|
|
1383
|
-
});
|
|
1474
|
+
}, context);
|
|
1384
1475
|
}
|
|
1385
1476
|
return response;
|
|
1386
1477
|
}
|
|
1387
1478
|
}
|
|
1388
|
-
const
|
|
1479
|
+
const googleEmailSignInResolver = async (info, ctx) => {
|
|
1389
1480
|
const { profile } = info;
|
|
1390
1481
|
if (!profile.email) {
|
|
1391
|
-
throw new Error("
|
|
1482
|
+
throw new Error("Google profile contained no email");
|
|
1483
|
+
}
|
|
1484
|
+
const entity = await ctx.catalogIdentityClient.findUser({
|
|
1485
|
+
annotations: {
|
|
1486
|
+
"google.com/email": profile.email
|
|
1487
|
+
}
|
|
1488
|
+
});
|
|
1489
|
+
const claims = getEntityClaims(entity);
|
|
1490
|
+
const token = await ctx.tokenIssuer.issueToken({ claims });
|
|
1491
|
+
return { id: entity.metadata.name, entity, token };
|
|
1492
|
+
};
|
|
1493
|
+
const googleDefaultSignInResolver = async (info, ctx) => {
|
|
1494
|
+
const { profile } = info;
|
|
1495
|
+
if (!profile.email) {
|
|
1496
|
+
throw new Error("Google profile contained no email");
|
|
1497
|
+
}
|
|
1498
|
+
let userId;
|
|
1499
|
+
try {
|
|
1500
|
+
const entity = await ctx.catalogIdentityClient.findUser({
|
|
1501
|
+
annotations: {
|
|
1502
|
+
"google.com/email": profile.email
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
1505
|
+
userId = entity.metadata.name;
|
|
1506
|
+
} catch (error) {
|
|
1507
|
+
ctx.logger.warn(`Failed to look up user, ${error}, falling back to allowing login based on email pattern, this will probably break in the future`);
|
|
1508
|
+
userId = profile.email.split("@")[0];
|
|
1392
1509
|
}
|
|
1393
|
-
const userId = profile.email.split("@")[0];
|
|
1394
1510
|
const token = await ctx.tokenIssuer.issueToken({
|
|
1395
1511
|
claims: { sub: userId, ent: [`user:default/${userId}`] }
|
|
1396
1512
|
});
|
|
1397
1513
|
return { id: userId, token };
|
|
1398
1514
|
};
|
|
1399
|
-
const
|
|
1515
|
+
const createGoogleProvider = (options) => {
|
|
1400
1516
|
return ({
|
|
1401
1517
|
providerId,
|
|
1402
1518
|
globalConfig,
|
|
@@ -1409,40 +1525,28 @@ const createOidcProvider = (options) => {
|
|
|
1409
1525
|
const clientId = envConfig.getString("clientId");
|
|
1410
1526
|
const clientSecret = envConfig.getString("clientSecret");
|
|
1411
1527
|
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
1412
|
-
const metadataUrl = envConfig.getString("metadataUrl");
|
|
1413
|
-
const tokenSignedResponseAlg = envConfig.getOptionalString("tokenSignedResponseAlg");
|
|
1414
|
-
const scope = envConfig.getOptionalString("scope");
|
|
1415
|
-
const prompt = envConfig.getOptionalString("prompt");
|
|
1416
1528
|
const catalogIdentityClient = new CatalogIdentityClient({
|
|
1417
1529
|
catalogApi,
|
|
1418
1530
|
tokenIssuer
|
|
1419
1531
|
});
|
|
1420
|
-
const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({
|
|
1421
|
-
profile:
|
|
1422
|
-
displayName: userinfo.name,
|
|
1423
|
-
email: userinfo.email,
|
|
1424
|
-
picture: userinfo.picture
|
|
1425
|
-
}
|
|
1532
|
+
const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ fullProfile, params }) => ({
|
|
1533
|
+
profile: makeProfileInfo(fullProfile, params.id_token)
|
|
1426
1534
|
});
|
|
1427
|
-
const signInResolverFn = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b :
|
|
1535
|
+
const signInResolverFn = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : googleDefaultSignInResolver;
|
|
1428
1536
|
const signInResolver = (info) => signInResolverFn(info, {
|
|
1429
1537
|
catalogIdentityClient,
|
|
1430
1538
|
tokenIssuer,
|
|
1431
1539
|
logger
|
|
1432
1540
|
});
|
|
1433
|
-
const provider = new
|
|
1541
|
+
const provider = new GoogleAuthProvider({
|
|
1434
1542
|
clientId,
|
|
1435
1543
|
clientSecret,
|
|
1436
1544
|
callbackUrl,
|
|
1437
|
-
tokenSignedResponseAlg,
|
|
1438
|
-
metadataUrl,
|
|
1439
|
-
scope,
|
|
1440
|
-
prompt,
|
|
1441
1545
|
signInResolver,
|
|
1442
1546
|
authHandler,
|
|
1443
|
-
logger,
|
|
1444
1547
|
tokenIssuer,
|
|
1445
|
-
catalogIdentityClient
|
|
1548
|
+
catalogIdentityClient,
|
|
1549
|
+
logger
|
|
1446
1550
|
});
|
|
1447
1551
|
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
1448
1552
|
disableRefresh: false,
|
|
@@ -1452,44 +1556,26 @@ const createOidcProvider = (options) => {
|
|
|
1452
1556
|
});
|
|
1453
1557
|
};
|
|
1454
1558
|
|
|
1455
|
-
class
|
|
1559
|
+
class MicrosoftAuthProvider {
|
|
1456
1560
|
constructor(options) {
|
|
1457
|
-
this.
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
}
|
|
1464
|
-
};
|
|
1465
|
-
this._signInResolver = options.signInResolver;
|
|
1466
|
-
this._authHandler = options.authHandler;
|
|
1467
|
-
this._tokenIssuer = options.tokenIssuer;
|
|
1468
|
-
this._catalogIdentityClient = options.catalogIdentityClient;
|
|
1469
|
-
this._logger = options.logger;
|
|
1470
|
-
this._strategy = new passportOktaOauth.Strategy({
|
|
1561
|
+
this.signInResolver = options.signInResolver;
|
|
1562
|
+
this.authHandler = options.authHandler;
|
|
1563
|
+
this.tokenIssuer = options.tokenIssuer;
|
|
1564
|
+
this.logger = options.logger;
|
|
1565
|
+
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
1566
|
+
this._strategy = new passportMicrosoft.Strategy({
|
|
1471
1567
|
clientID: options.clientId,
|
|
1472
1568
|
clientSecret: options.clientSecret,
|
|
1473
1569
|
callbackURL: options.callbackUrl,
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
response_type: "code"
|
|
1570
|
+
authorizationURL: options.authorizationUrl,
|
|
1571
|
+
tokenURL: options.tokenUrl,
|
|
1572
|
+
passReqToCallback: false
|
|
1478
1573
|
}, (accessToken, refreshToken, params, fullProfile, done) => {
|
|
1479
|
-
done(void 0, {
|
|
1480
|
-
accessToken,
|
|
1481
|
-
refreshToken,
|
|
1482
|
-
params,
|
|
1483
|
-
fullProfile
|
|
1484
|
-
}, {
|
|
1485
|
-
refreshToken
|
|
1486
|
-
});
|
|
1574
|
+
done(void 0, { fullProfile, accessToken, params }, { refreshToken });
|
|
1487
1575
|
});
|
|
1488
1576
|
}
|
|
1489
1577
|
async start(req) {
|
|
1490
1578
|
return await executeRedirectStrategy(req, this._strategy, {
|
|
1491
|
-
accessType: "offline",
|
|
1492
|
-
prompt: "consent",
|
|
1493
1579
|
scope: req.scope,
|
|
1494
1580
|
state: encodeState(req.state)
|
|
1495
1581
|
});
|
|
@@ -1502,17 +1588,26 @@ class OktaAuthProvider {
|
|
|
1502
1588
|
};
|
|
1503
1589
|
}
|
|
1504
1590
|
async refresh(req) {
|
|
1505
|
-
const { accessToken, params } = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
|
|
1591
|
+
const { accessToken, refreshToken, params } = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
|
|
1506
1592
|
const fullProfile = await executeFetchUserProfileStrategy(this._strategy, accessToken);
|
|
1507
|
-
return
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1593
|
+
return {
|
|
1594
|
+
response: await this.handleResult({
|
|
1595
|
+
fullProfile,
|
|
1596
|
+
params,
|
|
1597
|
+
accessToken
|
|
1598
|
+
}),
|
|
1599
|
+
refreshToken
|
|
1600
|
+
};
|
|
1513
1601
|
}
|
|
1514
1602
|
async handleResult(result) {
|
|
1515
|
-
const
|
|
1603
|
+
const photo = await this.getUserPhoto(result.accessToken);
|
|
1604
|
+
result.fullProfile.photos = photo ? [{ value: photo }] : void 0;
|
|
1605
|
+
const context = {
|
|
1606
|
+
logger: this.logger,
|
|
1607
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
1608
|
+
tokenIssuer: this.tokenIssuer
|
|
1609
|
+
};
|
|
1610
|
+
const { profile } = await this.authHandler(result, context);
|
|
1516
1611
|
const response = {
|
|
1517
1612
|
providerInfo: {
|
|
1518
1613
|
idToken: result.params.id_token,
|
|
@@ -1522,37 +1617,48 @@ class OktaAuthProvider {
|
|
|
1522
1617
|
},
|
|
1523
1618
|
profile
|
|
1524
1619
|
};
|
|
1525
|
-
if (this.
|
|
1526
|
-
response.backstageIdentity = await this.
|
|
1620
|
+
if (this.signInResolver) {
|
|
1621
|
+
response.backstageIdentity = await this.signInResolver({
|
|
1527
1622
|
result,
|
|
1528
1623
|
profile
|
|
1529
|
-
},
|
|
1530
|
-
tokenIssuer: this._tokenIssuer,
|
|
1531
|
-
catalogIdentityClient: this._catalogIdentityClient,
|
|
1532
|
-
logger: this._logger
|
|
1533
|
-
});
|
|
1624
|
+
}, context);
|
|
1534
1625
|
}
|
|
1535
1626
|
return response;
|
|
1536
1627
|
}
|
|
1628
|
+
getUserPhoto(accessToken) {
|
|
1629
|
+
return new Promise((resolve) => {
|
|
1630
|
+
fetch__default["default"]("https://graph.microsoft.com/v1.0/me/photos/48x48/$value", {
|
|
1631
|
+
headers: {
|
|
1632
|
+
Authorization: `Bearer ${accessToken}`
|
|
1633
|
+
}
|
|
1634
|
+
}).then((response) => response.arrayBuffer()).then((arrayBuffer) => {
|
|
1635
|
+
const imageUrl = `data:image/jpeg;base64,${Buffer.from(arrayBuffer).toString("base64")}`;
|
|
1636
|
+
resolve(imageUrl);
|
|
1637
|
+
}).catch((error) => {
|
|
1638
|
+
this.logger.warn(`Could not retrieve user profile photo from Microsoft Graph API: ${error}`);
|
|
1639
|
+
resolve(void 0);
|
|
1640
|
+
});
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1537
1643
|
}
|
|
1538
|
-
const
|
|
1644
|
+
const microsoftEmailSignInResolver = async (info, ctx) => {
|
|
1539
1645
|
const { profile } = info;
|
|
1540
1646
|
if (!profile.email) {
|
|
1541
|
-
throw new Error("
|
|
1647
|
+
throw new Error("Microsoft profile contained no email");
|
|
1542
1648
|
}
|
|
1543
1649
|
const entity = await ctx.catalogIdentityClient.findUser({
|
|
1544
1650
|
annotations: {
|
|
1545
|
-
"
|
|
1651
|
+
"microsoft.com/email": profile.email
|
|
1546
1652
|
}
|
|
1547
1653
|
});
|
|
1548
1654
|
const claims = getEntityClaims(entity);
|
|
1549
1655
|
const token = await ctx.tokenIssuer.issueToken({ claims });
|
|
1550
1656
|
return { id: entity.metadata.name, entity, token };
|
|
1551
1657
|
};
|
|
1552
|
-
const
|
|
1658
|
+
const microsoftDefaultSignInResolver = async (info, ctx) => {
|
|
1553
1659
|
const { profile } = info;
|
|
1554
1660
|
if (!profile.email) {
|
|
1555
|
-
throw new Error("
|
|
1661
|
+
throw new Error("Profile contained no email");
|
|
1556
1662
|
}
|
|
1557
1663
|
const userId = profile.email.split("@")[0];
|
|
1558
1664
|
const token = await ctx.tokenIssuer.issueToken({
|
|
@@ -1560,7 +1666,7 @@ const oktaDefaultSignInResolver = async (info, ctx) => {
|
|
|
1560
1666
|
});
|
|
1561
1667
|
return { id: userId, token };
|
|
1562
1668
|
};
|
|
1563
|
-
const
|
|
1669
|
+
const createMicrosoftProvider = (options) => {
|
|
1564
1670
|
return ({
|
|
1565
1671
|
providerId,
|
|
1566
1672
|
globalConfig,
|
|
@@ -1572,34 +1678,34 @@ const createOktaProvider = (_options) => {
|
|
|
1572
1678
|
var _a, _b;
|
|
1573
1679
|
const clientId = envConfig.getString("clientId");
|
|
1574
1680
|
const clientSecret = envConfig.getString("clientSecret");
|
|
1575
|
-
const
|
|
1681
|
+
const tenantId = envConfig.getString("tenantId");
|
|
1576
1682
|
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
}
|
|
1683
|
+
const authorizationUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`;
|
|
1684
|
+
const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
|
|
1580
1685
|
const catalogIdentityClient = new CatalogIdentityClient({
|
|
1581
1686
|
catalogApi,
|
|
1582
1687
|
tokenIssuer
|
|
1583
1688
|
});
|
|
1584
|
-
const authHandler = (
|
|
1689
|
+
const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ fullProfile, params }) => ({
|
|
1585
1690
|
profile: makeProfileInfo(fullProfile, params.id_token)
|
|
1586
1691
|
});
|
|
1587
|
-
const signInResolverFn = (_b = (_a =
|
|
1692
|
+
const signInResolverFn = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : microsoftDefaultSignInResolver;
|
|
1588
1693
|
const signInResolver = (info) => signInResolverFn(info, {
|
|
1589
1694
|
catalogIdentityClient,
|
|
1590
1695
|
tokenIssuer,
|
|
1591
1696
|
logger
|
|
1592
1697
|
});
|
|
1593
|
-
const provider = new
|
|
1594
|
-
audience,
|
|
1698
|
+
const provider = new MicrosoftAuthProvider({
|
|
1595
1699
|
clientId,
|
|
1596
1700
|
clientSecret,
|
|
1597
1701
|
callbackUrl,
|
|
1702
|
+
authorizationUrl,
|
|
1703
|
+
tokenUrl,
|
|
1598
1704
|
authHandler,
|
|
1599
1705
|
signInResolver,
|
|
1600
|
-
tokenIssuer,
|
|
1601
1706
|
catalogIdentityClient,
|
|
1602
|
-
logger
|
|
1707
|
+
logger,
|
|
1708
|
+
tokenIssuer
|
|
1603
1709
|
});
|
|
1604
1710
|
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
1605
1711
|
disableRefresh: false,
|
|
@@ -1609,24 +1715,30 @@ const createOktaProvider = (_options) => {
|
|
|
1609
1715
|
});
|
|
1610
1716
|
};
|
|
1611
1717
|
|
|
1612
|
-
class
|
|
1718
|
+
class OAuth2AuthProvider {
|
|
1613
1719
|
constructor(options) {
|
|
1614
1720
|
this.signInResolver = options.signInResolver;
|
|
1615
1721
|
this.authHandler = options.authHandler;
|
|
1616
1722
|
this.tokenIssuer = options.tokenIssuer;
|
|
1617
1723
|
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
1618
1724
|
this.logger = options.logger;
|
|
1619
|
-
this._strategy = new
|
|
1725
|
+
this._strategy = new OAuth2Strategy.Strategy({
|
|
1620
1726
|
clientID: options.clientId,
|
|
1621
1727
|
clientSecret: options.clientSecret,
|
|
1622
1728
|
callbackURL: options.callbackUrl,
|
|
1623
|
-
|
|
1729
|
+
authorizationURL: options.authorizationUrl,
|
|
1730
|
+
tokenURL: options.tokenUrl,
|
|
1731
|
+
passReqToCallback: false,
|
|
1732
|
+
scope: options.scope,
|
|
1733
|
+
customHeaders: options.includeBasicAuth ? {
|
|
1734
|
+
Authorization: `Basic ${this.encodeClientCredentials(options.clientId, options.clientSecret)}`
|
|
1735
|
+
} : void 0
|
|
1624
1736
|
}, (accessToken, refreshToken, params, fullProfile, done) => {
|
|
1625
1737
|
done(void 0, {
|
|
1626
1738
|
fullProfile,
|
|
1627
|
-
params,
|
|
1628
1739
|
accessToken,
|
|
1629
|
-
refreshToken
|
|
1740
|
+
refreshToken,
|
|
1741
|
+
params
|
|
1630
1742
|
}, {
|
|
1631
1743
|
refreshToken
|
|
1632
1744
|
});
|
|
@@ -1648,18 +1760,25 @@ class BitbucketAuthProvider {
|
|
|
1648
1760
|
};
|
|
1649
1761
|
}
|
|
1650
1762
|
async refresh(req) {
|
|
1651
|
-
const
|
|
1763
|
+
const refreshTokenResponse = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
|
|
1764
|
+
const { accessToken, params, refreshToken } = refreshTokenResponse;
|
|
1652
1765
|
const fullProfile = await executeFetchUserProfileStrategy(this._strategy, accessToken);
|
|
1653
|
-
return
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1766
|
+
return {
|
|
1767
|
+
response: await this.handleResult({
|
|
1768
|
+
fullProfile,
|
|
1769
|
+
params,
|
|
1770
|
+
accessToken
|
|
1771
|
+
}),
|
|
1772
|
+
refreshToken
|
|
1773
|
+
};
|
|
1659
1774
|
}
|
|
1660
1775
|
async handleResult(result) {
|
|
1661
|
-
|
|
1662
|
-
|
|
1776
|
+
const context = {
|
|
1777
|
+
logger: this.logger,
|
|
1778
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
1779
|
+
tokenIssuer: this.tokenIssuer
|
|
1780
|
+
};
|
|
1781
|
+
const { profile } = await this.authHandler(result, context);
|
|
1663
1782
|
const response = {
|
|
1664
1783
|
providerInfo: {
|
|
1665
1784
|
idToken: result.params.id_token,
|
|
@@ -1673,44 +1792,26 @@ class BitbucketAuthProvider {
|
|
|
1673
1792
|
response.backstageIdentity = await this.signInResolver({
|
|
1674
1793
|
result,
|
|
1675
1794
|
profile
|
|
1676
|
-
},
|
|
1677
|
-
tokenIssuer: this.tokenIssuer,
|
|
1678
|
-
catalogIdentityClient: this.catalogIdentityClient,
|
|
1679
|
-
logger: this.logger
|
|
1680
|
-
});
|
|
1795
|
+
}, context);
|
|
1681
1796
|
}
|
|
1682
1797
|
return response;
|
|
1683
1798
|
}
|
|
1799
|
+
encodeClientCredentials(clientID, clientSecret) {
|
|
1800
|
+
return Buffer.from(`${clientID}:${clientSecret}`).toString("base64");
|
|
1801
|
+
}
|
|
1684
1802
|
}
|
|
1685
|
-
const
|
|
1686
|
-
const {
|
|
1687
|
-
if (!
|
|
1688
|
-
throw new Error("
|
|
1803
|
+
const oAuth2DefaultSignInResolver$1 = async (info, ctx) => {
|
|
1804
|
+
const { profile } = info;
|
|
1805
|
+
if (!profile.email) {
|
|
1806
|
+
throw new Error("Profile contained no email");
|
|
1689
1807
|
}
|
|
1690
|
-
const
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
}
|
|
1694
|
-
});
|
|
1695
|
-
const claims = getEntityClaims(entity);
|
|
1696
|
-
const token = await ctx.tokenIssuer.issueToken({ claims });
|
|
1697
|
-
return { id: entity.metadata.name, entity, token };
|
|
1698
|
-
};
|
|
1699
|
-
const bitbucketUserIdSignInResolver = async (info, ctx) => {
|
|
1700
|
-
const { result } = info;
|
|
1701
|
-
if (!result.fullProfile.id) {
|
|
1702
|
-
throw new Error("Bitbucket profile contained no User ID");
|
|
1703
|
-
}
|
|
1704
|
-
const entity = await ctx.catalogIdentityClient.findUser({
|
|
1705
|
-
annotations: {
|
|
1706
|
-
"bitbucket.org/user-id": result.fullProfile.id
|
|
1707
|
-
}
|
|
1808
|
+
const userId = profile.email.split("@")[0];
|
|
1809
|
+
const token = await ctx.tokenIssuer.issueToken({
|
|
1810
|
+
claims: { sub: userId, ent: [`user:default/${userId}`] }
|
|
1708
1811
|
});
|
|
1709
|
-
|
|
1710
|
-
const token = await ctx.tokenIssuer.issueToken({ claims });
|
|
1711
|
-
return { id: entity.metadata.name, entity, token };
|
|
1812
|
+
return { id: userId, token };
|
|
1712
1813
|
};
|
|
1713
|
-
const
|
|
1814
|
+
const createOAuth2Provider = (options) => {
|
|
1714
1815
|
return ({
|
|
1715
1816
|
providerId,
|
|
1716
1817
|
globalConfig,
|
|
@@ -1719,10 +1820,15 @@ const createBitbucketProvider = (options) => {
|
|
|
1719
1820
|
catalogApi,
|
|
1720
1821
|
logger
|
|
1721
1822
|
}) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
|
|
1722
|
-
var _a;
|
|
1823
|
+
var _a, _b, _c;
|
|
1723
1824
|
const clientId = envConfig.getString("clientId");
|
|
1724
1825
|
const clientSecret = envConfig.getString("clientSecret");
|
|
1725
1826
|
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
1827
|
+
const authorizationUrl = envConfig.getString("authorizationUrl");
|
|
1828
|
+
const tokenUrl = envConfig.getString("tokenUrl");
|
|
1829
|
+
const scope = envConfig.getOptionalString("scope");
|
|
1830
|
+
const includeBasicAuth = envConfig.getOptionalBoolean("includeBasicAuth");
|
|
1831
|
+
const disableRefresh = (_a = envConfig.getOptionalBoolean("disableRefresh")) != null ? _a : false;
|
|
1726
1832
|
const catalogIdentityClient = new CatalogIdentityClient({
|
|
1727
1833
|
catalogApi,
|
|
1728
1834
|
tokenIssuer
|
|
@@ -1730,222 +1836,393 @@ const createBitbucketProvider = (options) => {
|
|
|
1730
1836
|
const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ fullProfile, params }) => ({
|
|
1731
1837
|
profile: makeProfileInfo(fullProfile, params.id_token)
|
|
1732
1838
|
});
|
|
1733
|
-
const
|
|
1839
|
+
const signInResolverFn = (_c = (_b = options == null ? void 0 : options.signIn) == null ? void 0 : _b.resolver) != null ? _c : oAuth2DefaultSignInResolver$1;
|
|
1840
|
+
const signInResolver = (info) => signInResolverFn(info, {
|
|
1841
|
+
catalogIdentityClient,
|
|
1842
|
+
tokenIssuer,
|
|
1843
|
+
logger
|
|
1844
|
+
});
|
|
1845
|
+
const provider = new OAuth2AuthProvider({
|
|
1734
1846
|
clientId,
|
|
1735
1847
|
clientSecret,
|
|
1736
|
-
callbackUrl,
|
|
1737
|
-
signInResolver: (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver,
|
|
1738
|
-
authHandler,
|
|
1739
1848
|
tokenIssuer,
|
|
1740
1849
|
catalogIdentityClient,
|
|
1741
|
-
|
|
1850
|
+
callbackUrl,
|
|
1851
|
+
signInResolver,
|
|
1852
|
+
authHandler,
|
|
1853
|
+
authorizationUrl,
|
|
1854
|
+
tokenUrl,
|
|
1855
|
+
scope,
|
|
1856
|
+
logger,
|
|
1857
|
+
includeBasicAuth
|
|
1742
1858
|
});
|
|
1743
1859
|
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
1744
|
-
disableRefresh
|
|
1860
|
+
disableRefresh,
|
|
1745
1861
|
providerId,
|
|
1746
1862
|
tokenIssuer
|
|
1747
1863
|
});
|
|
1748
1864
|
});
|
|
1749
1865
|
};
|
|
1750
1866
|
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1867
|
+
function createOidcRouter(options) {
|
|
1868
|
+
const { baseUrl, tokenIssuer } = options;
|
|
1869
|
+
const router = Router__default["default"]();
|
|
1870
|
+
const config = {
|
|
1871
|
+
issuer: baseUrl,
|
|
1872
|
+
token_endpoint: `${baseUrl}/v1/token`,
|
|
1873
|
+
userinfo_endpoint: `${baseUrl}/v1/userinfo`,
|
|
1874
|
+
jwks_uri: `${baseUrl}/.well-known/jwks.json`,
|
|
1875
|
+
response_types_supported: ["id_token"],
|
|
1876
|
+
subject_types_supported: ["public"],
|
|
1877
|
+
id_token_signing_alg_values_supported: ["RS256"],
|
|
1878
|
+
scopes_supported: ["openid"],
|
|
1879
|
+
token_endpoint_auth_methods_supported: [],
|
|
1880
|
+
claims_supported: ["sub"],
|
|
1881
|
+
grant_types_supported: []
|
|
1882
|
+
};
|
|
1883
|
+
router.get("/.well-known/openid-configuration", (_req, res) => {
|
|
1884
|
+
res.json(config);
|
|
1885
|
+
});
|
|
1886
|
+
router.get("/.well-known/jwks.json", async (_req, res) => {
|
|
1887
|
+
const { keys } = await tokenIssuer.listPublicKeys();
|
|
1888
|
+
res.json({ keys });
|
|
1889
|
+
});
|
|
1890
|
+
router.get("/v1/token", (_req, res) => {
|
|
1891
|
+
res.status(501).send("Not Implemented");
|
|
1892
|
+
});
|
|
1893
|
+
router.get("/v1/userinfo", (_req, res) => {
|
|
1894
|
+
res.status(501).send("Not Implemented");
|
|
1895
|
+
});
|
|
1896
|
+
return router;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
const CLOCK_MARGIN_S = 10;
|
|
1900
|
+
class IdentityClient {
|
|
1901
|
+
constructor(options) {
|
|
1902
|
+
this.discovery = options.discovery;
|
|
1903
|
+
this.issuer = options.issuer;
|
|
1904
|
+
this.keyStore = new jose.JWKS.KeyStore();
|
|
1905
|
+
this.keyStoreUpdated = 0;
|
|
1906
|
+
}
|
|
1907
|
+
async authenticate(token) {
|
|
1908
|
+
var _a;
|
|
1909
|
+
if (!token) {
|
|
1910
|
+
throw new errors.AuthenticationError("No token specified");
|
|
1756
1911
|
}
|
|
1757
|
-
const
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1912
|
+
const key = await this.getKey(token);
|
|
1913
|
+
if (!key) {
|
|
1914
|
+
throw new errors.AuthenticationError("No signing key matching token found");
|
|
1915
|
+
}
|
|
1916
|
+
const decoded = jose.JWT.IdToken.verify(token, key, {
|
|
1917
|
+
algorithms: ["ES256"],
|
|
1918
|
+
audience: "backstage",
|
|
1919
|
+
issuer: this.issuer
|
|
1920
|
+
});
|
|
1921
|
+
if (!decoded.sub) {
|
|
1922
|
+
throw new errors.AuthenticationError("No user sub found in token");
|
|
1923
|
+
}
|
|
1924
|
+
const user = {
|
|
1925
|
+
id: decoded.sub,
|
|
1926
|
+
token,
|
|
1927
|
+
identity: {
|
|
1928
|
+
type: "user",
|
|
1929
|
+
userEntityRef: decoded.sub,
|
|
1930
|
+
ownershipEntityRefs: (_a = decoded.ent) != null ? _a : []
|
|
1931
|
+
}
|
|
1763
1932
|
};
|
|
1764
|
-
|
|
1765
|
-
this.profileURL = "https://api.atlassian.com/me";
|
|
1766
|
-
this.name = "atlassian";
|
|
1767
|
-
this._oauth2.useAuthorizationHeaderforGET(true);
|
|
1933
|
+
return user;
|
|
1768
1934
|
}
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1935
|
+
static getBearerToken(authorizationHeader) {
|
|
1936
|
+
if (typeof authorizationHeader !== "string") {
|
|
1937
|
+
return void 0;
|
|
1938
|
+
}
|
|
1939
|
+
const matches = authorizationHeader.match(/Bearer\s+(\S+)/i);
|
|
1940
|
+
return matches == null ? void 0 : matches[1];
|
|
1774
1941
|
}
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
return done(new OAuth2Strategy.InternalOAuthError("Failed to fetch user profile", err.statusCode));
|
|
1779
|
-
}
|
|
1780
|
-
if (!body) {
|
|
1781
|
-
return done(new Error("Failed to fetch user profile, body cannot be empty"));
|
|
1782
|
-
}
|
|
1783
|
-
try {
|
|
1784
|
-
const json = typeof body !== "string" ? body.toString() : body;
|
|
1785
|
-
const profile = AtlassianStrategy.parse(json);
|
|
1786
|
-
return done(null, profile);
|
|
1787
|
-
} catch (e) {
|
|
1788
|
-
return done(new Error("Failed to parse user profile"));
|
|
1789
|
-
}
|
|
1942
|
+
async getKey(rawJwtToken) {
|
|
1943
|
+
const { header, payload } = jose.JWT.decode(rawJwtToken, {
|
|
1944
|
+
complete: true
|
|
1790
1945
|
});
|
|
1946
|
+
const keyStoreHasKey = !!this.keyStore.get({ kid: header.kid });
|
|
1947
|
+
const issuedAfterLastRefresh = (payload == null ? void 0 : payload.iat) && payload.iat > this.keyStoreUpdated - CLOCK_MARGIN_S;
|
|
1948
|
+
if (!keyStoreHasKey && issuedAfterLastRefresh) {
|
|
1949
|
+
await this.refreshKeyStore();
|
|
1950
|
+
}
|
|
1951
|
+
return this.keyStore.get({ kid: header.kid });
|
|
1791
1952
|
}
|
|
1792
|
-
|
|
1793
|
-
const
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1953
|
+
async listPublicKeys() {
|
|
1954
|
+
const url = `${await this.discovery.getBaseUrl("auth")}/.well-known/jwks.json`;
|
|
1955
|
+
const response = await fetch__default["default"](url);
|
|
1956
|
+
if (!response.ok) {
|
|
1957
|
+
const payload = await response.text();
|
|
1958
|
+
const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
|
|
1959
|
+
throw new Error(message);
|
|
1960
|
+
}
|
|
1961
|
+
const publicKeys = await response.json();
|
|
1962
|
+
return publicKeys;
|
|
1963
|
+
}
|
|
1964
|
+
async refreshKeyStore() {
|
|
1965
|
+
const now = Date.now() / 1e3;
|
|
1966
|
+
const publicKeys = await this.listPublicKeys();
|
|
1967
|
+
this.keyStore = jose.JWKS.asKeyStore({
|
|
1968
|
+
keys: publicKeys.keys.map((key) => key)
|
|
1969
|
+
});
|
|
1970
|
+
this.keyStoreUpdated = now;
|
|
1802
1971
|
}
|
|
1803
1972
|
}
|
|
1804
1973
|
|
|
1805
|
-
const
|
|
1806
|
-
|
|
1807
|
-
params
|
|
1808
|
-
}) => ({
|
|
1809
|
-
profile: makeProfileInfo(fullProfile, params.id_token)
|
|
1810
|
-
});
|
|
1811
|
-
class AtlassianAuthProvider {
|
|
1974
|
+
const MS_IN_S = 1e3;
|
|
1975
|
+
class TokenFactory {
|
|
1812
1976
|
constructor(options) {
|
|
1813
|
-
this.
|
|
1977
|
+
this.issuer = options.issuer;
|
|
1814
1978
|
this.logger = options.logger;
|
|
1815
|
-
this.
|
|
1816
|
-
this.
|
|
1817
|
-
this.signInResolver = options.signInResolver;
|
|
1818
|
-
this._strategy = new AtlassianStrategy({
|
|
1819
|
-
clientID: options.clientId,
|
|
1820
|
-
clientSecret: options.clientSecret,
|
|
1821
|
-
callbackURL: options.callbackUrl,
|
|
1822
|
-
scope: options.scopes
|
|
1823
|
-
}, (accessToken, refreshToken, params, fullProfile, done) => {
|
|
1824
|
-
done(void 0, {
|
|
1825
|
-
fullProfile,
|
|
1826
|
-
accessToken,
|
|
1827
|
-
refreshToken,
|
|
1828
|
-
params
|
|
1829
|
-
});
|
|
1830
|
-
});
|
|
1979
|
+
this.keyStore = options.keyStore;
|
|
1980
|
+
this.keyDurationSeconds = options.keyDurationSeconds;
|
|
1831
1981
|
}
|
|
1832
|
-
async
|
|
1833
|
-
|
|
1834
|
-
|
|
1982
|
+
async issueToken(params) {
|
|
1983
|
+
const key = await this.getKey();
|
|
1984
|
+
const iss = this.issuer;
|
|
1985
|
+
const sub = params.claims.sub;
|
|
1986
|
+
const ent = params.claims.ent;
|
|
1987
|
+
const aud = "backstage";
|
|
1988
|
+
const iat = Math.floor(Date.now() / MS_IN_S);
|
|
1989
|
+
const exp = iat + this.keyDurationSeconds;
|
|
1990
|
+
this.logger.info(`Issuing token for ${sub}, with entities ${ent != null ? ent : []}`);
|
|
1991
|
+
return jose.JWS.sign({ iss, sub, aud, iat, exp, ent }, key, {
|
|
1992
|
+
alg: key.alg,
|
|
1993
|
+
kid: key.kid
|
|
1835
1994
|
});
|
|
1836
1995
|
}
|
|
1837
|
-
async
|
|
1838
|
-
|
|
1839
|
-
const
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
}
|
|
1845
|
-
async handleResult(result) {
|
|
1846
|
-
const { profile } = await this.authHandler(result);
|
|
1847
|
-
const response = {
|
|
1848
|
-
providerInfo: {
|
|
1849
|
-
idToken: result.params.id_token,
|
|
1850
|
-
accessToken: result.accessToken,
|
|
1851
|
-
refreshToken: result.refreshToken,
|
|
1852
|
-
scope: result.params.scope,
|
|
1853
|
-
expiresInSeconds: result.params.expires_in
|
|
1854
|
-
},
|
|
1855
|
-
profile
|
|
1856
|
-
};
|
|
1857
|
-
if (this.signInResolver) {
|
|
1858
|
-
response.backstageIdentity = await this.signInResolver({
|
|
1859
|
-
result,
|
|
1860
|
-
profile
|
|
1861
|
-
}, {
|
|
1862
|
-
tokenIssuer: this.tokenIssuer,
|
|
1863
|
-
catalogIdentityClient: this.catalogIdentityClient,
|
|
1864
|
-
logger: this.logger
|
|
1996
|
+
async listPublicKeys() {
|
|
1997
|
+
const { items: keys } = await this.keyStore.listKeys();
|
|
1998
|
+
const validKeys = [];
|
|
1999
|
+
const expiredKeys = [];
|
|
2000
|
+
for (const key of keys) {
|
|
2001
|
+
const expireAt = luxon.DateTime.fromJSDate(key.createdAt).plus({
|
|
2002
|
+
seconds: 3 * this.keyDurationSeconds
|
|
1865
2003
|
});
|
|
2004
|
+
if (expireAt < luxon.DateTime.local()) {
|
|
2005
|
+
expiredKeys.push(key);
|
|
2006
|
+
} else {
|
|
2007
|
+
validKeys.push(key);
|
|
2008
|
+
}
|
|
1866
2009
|
}
|
|
1867
|
-
|
|
2010
|
+
if (expiredKeys.length > 0) {
|
|
2011
|
+
const kids = expiredKeys.map(({ key }) => key.kid);
|
|
2012
|
+
this.logger.info(`Removing expired signing keys, '${kids.join("', '")}'`);
|
|
2013
|
+
this.keyStore.removeKeys(kids).catch((error) => {
|
|
2014
|
+
this.logger.error(`Failed to remove expired keys, ${error}`);
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
return { keys: validKeys.map(({ key }) => key) };
|
|
1868
2018
|
}
|
|
1869
|
-
async
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
2019
|
+
async getKey() {
|
|
2020
|
+
if (this.privateKeyPromise) {
|
|
2021
|
+
if (this.keyExpiry && luxon.DateTime.fromJSDate(this.keyExpiry) > luxon.DateTime.local()) {
|
|
2022
|
+
return this.privateKeyPromise;
|
|
2023
|
+
}
|
|
2024
|
+
this.logger.info(`Signing key has expired, generating new key`);
|
|
2025
|
+
delete this.privateKeyPromise;
|
|
2026
|
+
}
|
|
2027
|
+
this.keyExpiry = luxon.DateTime.utc().plus({
|
|
2028
|
+
seconds: this.keyDurationSeconds
|
|
2029
|
+
}).toJSDate();
|
|
2030
|
+
const promise = (async () => {
|
|
2031
|
+
const key = await jose.JWK.generate("EC", "P-256", {
|
|
2032
|
+
use: "sig",
|
|
2033
|
+
kid: uuid.v4(),
|
|
2034
|
+
alg: "ES256"
|
|
2035
|
+
});
|
|
2036
|
+
this.logger.info(`Created new signing key ${key.kid}`);
|
|
2037
|
+
await this.keyStore.addKey(key.toJWK(false));
|
|
2038
|
+
return key;
|
|
2039
|
+
})();
|
|
2040
|
+
this.privateKeyPromise = promise;
|
|
2041
|
+
try {
|
|
2042
|
+
await promise;
|
|
2043
|
+
} catch (error) {
|
|
2044
|
+
this.logger.error(`Failed to generate new signing key, ${error}`);
|
|
2045
|
+
delete this.keyExpiry;
|
|
2046
|
+
delete this.privateKeyPromise;
|
|
2047
|
+
}
|
|
2048
|
+
return promise;
|
|
1882
2049
|
}
|
|
1883
2050
|
}
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
const
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
catalogApi,
|
|
1900
|
-
tokenIssuer
|
|
2051
|
+
|
|
2052
|
+
const migrationsDir = backendCommon.resolvePackagePath("@backstage/plugin-auth-backend", "migrations");
|
|
2053
|
+
const TABLE = "signing_keys";
|
|
2054
|
+
const parseDate = (date) => {
|
|
2055
|
+
const parsedDate = typeof date === "string" ? luxon.DateTime.fromSQL(date, { zone: "UTC" }) : luxon.DateTime.fromJSDate(date);
|
|
2056
|
+
if (!parsedDate.isValid) {
|
|
2057
|
+
throw new Error(`Failed to parse date, reason: ${parsedDate.invalidReason}, explanation: ${parsedDate.invalidExplanation}`);
|
|
2058
|
+
}
|
|
2059
|
+
return parsedDate.toJSDate();
|
|
2060
|
+
};
|
|
2061
|
+
class DatabaseKeyStore {
|
|
2062
|
+
static async create(options) {
|
|
2063
|
+
const { database } = options;
|
|
2064
|
+
await database.migrate.latest({
|
|
2065
|
+
directory: migrationsDir
|
|
1901
2066
|
});
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
logger,
|
|
1912
|
-
tokenIssuer
|
|
2067
|
+
return new DatabaseKeyStore(options);
|
|
2068
|
+
}
|
|
2069
|
+
constructor(options) {
|
|
2070
|
+
this.database = options.database;
|
|
2071
|
+
}
|
|
2072
|
+
async addKey(key) {
|
|
2073
|
+
await this.database(TABLE).insert({
|
|
2074
|
+
kid: key.kid,
|
|
2075
|
+
key: JSON.stringify(key)
|
|
1913
2076
|
});
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
2077
|
+
}
|
|
2078
|
+
async listKeys() {
|
|
2079
|
+
const rows = await this.database(TABLE).select();
|
|
2080
|
+
return {
|
|
2081
|
+
items: rows.map((row) => ({
|
|
2082
|
+
key: JSON.parse(row.key),
|
|
2083
|
+
createdAt: parseDate(row.created_at)
|
|
2084
|
+
}))
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
async removeKeys(kids) {
|
|
2088
|
+
await this.database(TABLE).delete().whereIn("kid", kids);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
class MemoryKeyStore {
|
|
2093
|
+
constructor() {
|
|
2094
|
+
this.keys = /* @__PURE__ */ new Map();
|
|
2095
|
+
}
|
|
2096
|
+
async addKey(key) {
|
|
2097
|
+
this.keys.set(key.kid, {
|
|
2098
|
+
createdAt: luxon.DateTime.utc().toJSDate(),
|
|
2099
|
+
key: JSON.stringify(key)
|
|
1918
2100
|
});
|
|
1919
|
-
}
|
|
1920
|
-
|
|
2101
|
+
}
|
|
2102
|
+
async removeKeys(kids) {
|
|
2103
|
+
for (const kid of kids) {
|
|
2104
|
+
this.keys.delete(kid);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
async listKeys() {
|
|
2108
|
+
return {
|
|
2109
|
+
items: Array.from(this.keys).map(([, { createdAt, key: keyStr }]) => ({
|
|
2110
|
+
createdAt,
|
|
2111
|
+
key: JSON.parse(keyStr)
|
|
2112
|
+
}))
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
1921
2116
|
|
|
1922
|
-
const
|
|
1923
|
-
const
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
2117
|
+
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
2118
|
+
const DEFAULT_DOCUMENT_PATH = "sessions";
|
|
2119
|
+
class FirestoreKeyStore {
|
|
2120
|
+
constructor(database, path, timeout) {
|
|
2121
|
+
this.database = database;
|
|
2122
|
+
this.path = path;
|
|
2123
|
+
this.timeout = timeout;
|
|
2124
|
+
}
|
|
2125
|
+
static async create(settings) {
|
|
2126
|
+
const { path, timeout, ...firestoreSettings } = settings != null ? settings : {};
|
|
2127
|
+
const database = new firestore.Firestore(firestoreSettings);
|
|
2128
|
+
return new FirestoreKeyStore(database, path != null ? path : DEFAULT_DOCUMENT_PATH, timeout != null ? timeout : DEFAULT_TIMEOUT_MS);
|
|
2129
|
+
}
|
|
2130
|
+
static async verifyConnection(keyStore, logger) {
|
|
2131
|
+
try {
|
|
2132
|
+
await keyStore.verify();
|
|
2133
|
+
} catch (error) {
|
|
2134
|
+
if (process.env.NODE_ENV !== "development") {
|
|
2135
|
+
throw new Error(`Failed to connect to database: ${error.message}`);
|
|
2136
|
+
}
|
|
2137
|
+
logger == null ? void 0 : logger.warn(`Failed to connect to database: ${error.message}`);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
async addKey(key) {
|
|
2141
|
+
await this.withTimeout(this.database.collection(this.path).doc(key.kid).set({
|
|
2142
|
+
kid: key.kid,
|
|
2143
|
+
key: JSON.stringify(key)
|
|
2144
|
+
}));
|
|
2145
|
+
}
|
|
2146
|
+
async listKeys() {
|
|
2147
|
+
const keys = await this.withTimeout(this.database.collection(this.path).get());
|
|
2148
|
+
return {
|
|
2149
|
+
items: keys.docs.map((key) => ({
|
|
2150
|
+
key: key.data(),
|
|
2151
|
+
createdAt: key.createTime.toDate()
|
|
2152
|
+
}))
|
|
2153
|
+
};
|
|
2154
|
+
}
|
|
2155
|
+
async removeKeys(kids) {
|
|
2156
|
+
for (const kid of kids) {
|
|
2157
|
+
await this.withTimeout(this.database.collection(this.path).doc(kid).delete());
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
async withTimeout(operation) {
|
|
2161
|
+
const timer = new Promise((_, reject) => setTimeout(() => {
|
|
2162
|
+
reject(new Error(`Operation timed out after ${this.timeout}ms`));
|
|
2163
|
+
}, this.timeout));
|
|
2164
|
+
return Promise.race([operation, timer]);
|
|
2165
|
+
}
|
|
2166
|
+
async verify() {
|
|
2167
|
+
await this.withTimeout(this.database.collection(this.path).limit(1).get());
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
class KeyStores {
|
|
2172
|
+
static async fromConfig(config, options) {
|
|
2173
|
+
var _a;
|
|
2174
|
+
const { logger, database } = options != null ? options : {};
|
|
2175
|
+
const ks = config.getOptionalConfig("auth.keyStore");
|
|
2176
|
+
const provider = (_a = ks == null ? void 0 : ks.getOptionalString("provider")) != null ? _a : "database";
|
|
2177
|
+
logger == null ? void 0 : logger.info(`Configuring "${provider}" as KeyStore provider`);
|
|
2178
|
+
if (provider === "database") {
|
|
2179
|
+
if (!database) {
|
|
2180
|
+
throw new Error("This KeyStore provider requires a database");
|
|
2181
|
+
}
|
|
2182
|
+
return await DatabaseKeyStore.create({
|
|
2183
|
+
database: await database.getClient()
|
|
2184
|
+
});
|
|
2185
|
+
}
|
|
2186
|
+
if (provider === "memory") {
|
|
2187
|
+
return new MemoryKeyStore();
|
|
2188
|
+
}
|
|
2189
|
+
if (provider === "firestore") {
|
|
2190
|
+
const settings = ks == null ? void 0 : ks.getConfig(provider);
|
|
2191
|
+
const keyStore = await FirestoreKeyStore.create(lodash.pickBy({
|
|
2192
|
+
projectId: settings == null ? void 0 : settings.getOptionalString("projectId"),
|
|
2193
|
+
keyFilename: settings == null ? void 0 : settings.getOptionalString("keyFilename"),
|
|
2194
|
+
host: settings == null ? void 0 : settings.getOptionalString("host"),
|
|
2195
|
+
port: settings == null ? void 0 : settings.getOptionalNumber("port"),
|
|
2196
|
+
ssl: settings == null ? void 0 : settings.getOptionalBoolean("ssl"),
|
|
2197
|
+
path: settings == null ? void 0 : settings.getOptionalString("path"),
|
|
2198
|
+
timeout: settings == null ? void 0 : settings.getOptionalNumber("timeout")
|
|
2199
|
+
}, (value) => value !== void 0));
|
|
2200
|
+
await FirestoreKeyStore.verifyConnection(keyStore, logger);
|
|
2201
|
+
return keyStore;
|
|
2202
|
+
}
|
|
2203
|
+
throw new Error(`Unknown KeyStore provider: ${provider}`);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
const OAUTH2_PROXY_JWT_HEADER = "X-OAUTH2-PROXY-ID-TOKEN";
|
|
2208
|
+
class Oauth2ProxyAuthProvider {
|
|
1929
2209
|
constructor(options) {
|
|
1930
|
-
this.region = options.region;
|
|
1931
|
-
this.issuer = options.issuer;
|
|
1932
|
-
this.authHandler = options.authHandler;
|
|
1933
|
-
this.signInResolver = options.signInResolver;
|
|
1934
|
-
this.tokenIssuer = options.tokenIssuer;
|
|
1935
2210
|
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
1936
2211
|
this.logger = options.logger;
|
|
1937
|
-
this.
|
|
2212
|
+
this.tokenIssuer = options.tokenIssuer;
|
|
2213
|
+
this.signInResolver = options.signInResolver;
|
|
2214
|
+
this.authHandler = options.authHandler;
|
|
1938
2215
|
}
|
|
1939
2216
|
frameHandler() {
|
|
1940
2217
|
return Promise.resolve(void 0);
|
|
1941
2218
|
}
|
|
1942
2219
|
async refresh(req, res) {
|
|
1943
2220
|
try {
|
|
1944
|
-
const result =
|
|
2221
|
+
const result = this.getResult(req);
|
|
1945
2222
|
const response = await this.handleResult(result);
|
|
1946
2223
|
res.json(response);
|
|
1947
2224
|
} catch (e) {
|
|
1948
|
-
this.logger.error(
|
|
2225
|
+
this.logger.error(`Exception occurred during ${OAUTH2_PROXY_JWT_HEADER} refresh`, e);
|
|
1949
2226
|
res.status(401);
|
|
1950
2227
|
res.end();
|
|
1951
2228
|
}
|
|
@@ -1953,159 +2230,159 @@ class AwsAlbAuthProvider {
|
|
|
1953
2230
|
start() {
|
|
1954
2231
|
return Promise.resolve(void 0);
|
|
1955
2232
|
}
|
|
1956
|
-
async getResult(req) {
|
|
1957
|
-
const jwt = req.header(ALB_JWT_HEADER);
|
|
1958
|
-
const accessToken = req.header(ALB_ACCESSTOKEN_HEADER);
|
|
1959
|
-
if (jwt === void 0) {
|
|
1960
|
-
throw new errors.AuthenticationError(`Missing ALB OIDC header: ${ALB_JWT_HEADER}`);
|
|
1961
|
-
}
|
|
1962
|
-
if (accessToken === void 0) {
|
|
1963
|
-
throw new errors.AuthenticationError(`Missing ALB OIDC header: ${ALB_ACCESSTOKEN_HEADER}`);
|
|
1964
|
-
}
|
|
1965
|
-
try {
|
|
1966
|
-
const headers = getJWTHeaders(jwt);
|
|
1967
|
-
const key = await this.getKey(headers.kid);
|
|
1968
|
-
const claims = jose.JWT.verify(jwt, key);
|
|
1969
|
-
if (this.issuer && claims.iss !== this.issuer) {
|
|
1970
|
-
throw new errors.AuthenticationError("Issuer mismatch on JWT token");
|
|
1971
|
-
}
|
|
1972
|
-
const fullProfile = {
|
|
1973
|
-
provider: "unknown",
|
|
1974
|
-
id: claims.sub,
|
|
1975
|
-
displayName: claims.name,
|
|
1976
|
-
username: claims.email.split("@")[0].toLowerCase(),
|
|
1977
|
-
name: {
|
|
1978
|
-
familyName: claims.family_name,
|
|
1979
|
-
givenName: claims.given_name
|
|
1980
|
-
},
|
|
1981
|
-
emails: [{ value: claims.email.toLowerCase() }],
|
|
1982
|
-
photos: [{ value: claims.picture }]
|
|
1983
|
-
};
|
|
1984
|
-
return {
|
|
1985
|
-
fullProfile,
|
|
1986
|
-
expiresInSeconds: claims.exp,
|
|
1987
|
-
accessToken
|
|
1988
|
-
};
|
|
1989
|
-
} catch (e) {
|
|
1990
|
-
throw new Error(`Exception occurred during JWT processing: ${e}`);
|
|
1991
|
-
}
|
|
1992
|
-
}
|
|
1993
2233
|
async handleResult(result) {
|
|
1994
|
-
const
|
|
1995
|
-
|
|
2234
|
+
const ctx = {
|
|
2235
|
+
logger: this.logger,
|
|
2236
|
+
tokenIssuer: this.tokenIssuer,
|
|
2237
|
+
catalogIdentityClient: this.catalogIdentityClient
|
|
2238
|
+
};
|
|
2239
|
+
const { profile } = await this.authHandler(result, ctx);
|
|
2240
|
+
const backstageSignInResult = await this.signInResolver({
|
|
1996
2241
|
result,
|
|
1997
2242
|
profile
|
|
1998
|
-
},
|
|
1999
|
-
tokenIssuer: this.tokenIssuer,
|
|
2000
|
-
catalogIdentityClient: this.catalogIdentityClient,
|
|
2001
|
-
logger: this.logger
|
|
2002
|
-
});
|
|
2243
|
+
}, ctx);
|
|
2003
2244
|
return {
|
|
2004
2245
|
providerInfo: {
|
|
2005
|
-
accessToken: result.accessToken
|
|
2006
|
-
expiresInSeconds: result.expiresInSeconds
|
|
2246
|
+
accessToken: result.accessToken
|
|
2007
2247
|
},
|
|
2008
|
-
backstageIdentity: prepareBackstageIdentityResponse(
|
|
2248
|
+
backstageIdentity: prepareBackstageIdentityResponse(backstageSignInResult),
|
|
2009
2249
|
profile
|
|
2010
2250
|
};
|
|
2011
2251
|
}
|
|
2012
|
-
|
|
2013
|
-
const
|
|
2014
|
-
|
|
2015
|
-
|
|
2252
|
+
getResult(req) {
|
|
2253
|
+
const authHeader = req.header(OAUTH2_PROXY_JWT_HEADER);
|
|
2254
|
+
const jwt = IdentityClient.getBearerToken(authHeader);
|
|
2255
|
+
if (!jwt) {
|
|
2256
|
+
throw new errors.AuthenticationError(`Missing or in incorrect format - Oauth2Proxy OIDC header: ${OAUTH2_PROXY_JWT_HEADER}`);
|
|
2016
2257
|
}
|
|
2017
|
-
const
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2258
|
+
const decodedJWT = jose.JWT.decode(jwt);
|
|
2259
|
+
return {
|
|
2260
|
+
fullProfile: decodedJWT,
|
|
2261
|
+
accessToken: jwt
|
|
2262
|
+
};
|
|
2021
2263
|
}
|
|
2022
2264
|
}
|
|
2023
|
-
const
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
const signInResolver = options == null ? void 0 : options.signIn.resolver;
|
|
2038
|
-
return new AwsAlbAuthProvider({
|
|
2039
|
-
region,
|
|
2040
|
-
issuer,
|
|
2041
|
-
signInResolver,
|
|
2042
|
-
authHandler,
|
|
2043
|
-
tokenIssuer,
|
|
2044
|
-
catalogIdentityClient,
|
|
2045
|
-
logger
|
|
2046
|
-
});
|
|
2047
|
-
};
|
|
2265
|
+
const createOauth2ProxyProvider = (options) => ({ catalogApi, logger, tokenIssuer }) => {
|
|
2266
|
+
const signInResolver = options.signIn.resolver;
|
|
2267
|
+
const authHandler = options.authHandler;
|
|
2268
|
+
const catalogIdentityClient = new CatalogIdentityClient({
|
|
2269
|
+
catalogApi,
|
|
2270
|
+
tokenIssuer
|
|
2271
|
+
});
|
|
2272
|
+
return new Oauth2ProxyAuthProvider({
|
|
2273
|
+
logger,
|
|
2274
|
+
signInResolver,
|
|
2275
|
+
authHandler,
|
|
2276
|
+
tokenIssuer,
|
|
2277
|
+
catalogIdentityClient
|
|
2278
|
+
});
|
|
2048
2279
|
};
|
|
2049
2280
|
|
|
2050
|
-
class
|
|
2281
|
+
class OidcAuthProvider {
|
|
2051
2282
|
constructor(options) {
|
|
2052
|
-
this.
|
|
2283
|
+
this.implementation = this.setupStrategy(options);
|
|
2284
|
+
this.scope = options.scope;
|
|
2285
|
+
this.prompt = options.prompt;
|
|
2053
2286
|
this.signInResolver = options.signInResolver;
|
|
2054
2287
|
this.authHandler = options.authHandler;
|
|
2055
2288
|
this.tokenIssuer = options.tokenIssuer;
|
|
2056
2289
|
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
2057
2290
|
this.logger = options.logger;
|
|
2058
|
-
this.strategy = new passportSaml.Strategy({ ...options }, (fullProfile, done) => {
|
|
2059
|
-
done(void 0, { fullProfile });
|
|
2060
|
-
});
|
|
2061
2291
|
}
|
|
2062
|
-
async start(req
|
|
2063
|
-
const {
|
|
2064
|
-
|
|
2292
|
+
async start(req) {
|
|
2293
|
+
const { strategy } = await this.implementation;
|
|
2294
|
+
const options = {
|
|
2295
|
+
scope: req.scope || this.scope || "openid profile email",
|
|
2296
|
+
state: encodeState(req.state)
|
|
2297
|
+
};
|
|
2298
|
+
const prompt = this.prompt || "none";
|
|
2299
|
+
if (prompt !== "auto") {
|
|
2300
|
+
options.prompt = prompt;
|
|
2301
|
+
}
|
|
2302
|
+
return await executeRedirectStrategy(req, strategy, options);
|
|
2065
2303
|
}
|
|
2066
|
-
async
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2304
|
+
async handler(req) {
|
|
2305
|
+
const { strategy } = await this.implementation;
|
|
2306
|
+
const { result, privateInfo } = await executeFrameHandlerStrategy(req, strategy);
|
|
2307
|
+
return {
|
|
2308
|
+
response: await this.handleResult(result),
|
|
2309
|
+
refreshToken: privateInfo.refreshToken
|
|
2310
|
+
};
|
|
2311
|
+
}
|
|
2312
|
+
async refresh(req) {
|
|
2313
|
+
const { client } = await this.implementation;
|
|
2314
|
+
const tokenset = await client.refresh(req.refreshToken);
|
|
2315
|
+
if (!tokenset.access_token) {
|
|
2316
|
+
throw new Error("Refresh failed");
|
|
2317
|
+
}
|
|
2318
|
+
const userinfo = await client.userinfo(tokenset.access_token);
|
|
2319
|
+
return {
|
|
2320
|
+
response: await this.handleResult({ tokenset, userinfo }),
|
|
2321
|
+
refreshToken: tokenset.refresh_token
|
|
2322
|
+
};
|
|
2323
|
+
}
|
|
2324
|
+
async setupStrategy(options) {
|
|
2325
|
+
const issuer = await openidClient.Issuer.discover(options.metadataUrl);
|
|
2326
|
+
const client = new issuer.Client({
|
|
2327
|
+
access_type: "offline",
|
|
2328
|
+
client_id: options.clientId,
|
|
2329
|
+
client_secret: options.clientSecret,
|
|
2330
|
+
redirect_uris: [options.callbackUrl],
|
|
2331
|
+
response_types: ["code"],
|
|
2332
|
+
id_token_signed_response_alg: options.tokenSignedResponseAlg || "RS256",
|
|
2333
|
+
scope: options.scope || ""
|
|
2334
|
+
});
|
|
2335
|
+
const strategy = new openidClient.Strategy({
|
|
2336
|
+
client,
|
|
2337
|
+
passReqToCallback: false
|
|
2338
|
+
}, (tokenset, userinfo, done) => {
|
|
2339
|
+
if (typeof done !== "function") {
|
|
2340
|
+
throw new Error("OIDC IdP must provide a userinfo_endpoint in the metadata response");
|
|
2084
2341
|
}
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
response
|
|
2088
|
-
});
|
|
2089
|
-
} catch (error) {
|
|
2090
|
-
const { name, message } = errors.isError(error) ? error : new Error("Encountered invalid error");
|
|
2091
|
-
return postMessageResponse(res, this.appUrl, {
|
|
2092
|
-
type: "authorization_response",
|
|
2093
|
-
error: { name, message }
|
|
2342
|
+
done(void 0, { tokenset, userinfo }, {
|
|
2343
|
+
refreshToken: tokenset.refresh_token
|
|
2094
2344
|
});
|
|
2095
|
-
}
|
|
2345
|
+
});
|
|
2346
|
+
strategy.error = console.error;
|
|
2347
|
+
return { strategy, client };
|
|
2096
2348
|
}
|
|
2097
|
-
async
|
|
2098
|
-
|
|
2349
|
+
async handleResult(result) {
|
|
2350
|
+
const context = {
|
|
2351
|
+
logger: this.logger,
|
|
2352
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
2353
|
+
tokenIssuer: this.tokenIssuer
|
|
2354
|
+
};
|
|
2355
|
+
const { profile } = await this.authHandler(result, context);
|
|
2356
|
+
const response = {
|
|
2357
|
+
providerInfo: {
|
|
2358
|
+
idToken: result.tokenset.id_token,
|
|
2359
|
+
accessToken: result.tokenset.access_token,
|
|
2360
|
+
scope: result.tokenset.scope,
|
|
2361
|
+
expiresInSeconds: result.tokenset.expires_in
|
|
2362
|
+
},
|
|
2363
|
+
profile
|
|
2364
|
+
};
|
|
2365
|
+
if (this.signInResolver) {
|
|
2366
|
+
response.backstageIdentity = await this.signInResolver({
|
|
2367
|
+
result,
|
|
2368
|
+
profile
|
|
2369
|
+
}, context);
|
|
2370
|
+
}
|
|
2371
|
+
return response;
|
|
2099
2372
|
}
|
|
2100
2373
|
}
|
|
2101
|
-
const
|
|
2102
|
-
const
|
|
2374
|
+
const oAuth2DefaultSignInResolver = async (info, ctx) => {
|
|
2375
|
+
const { profile } = info;
|
|
2376
|
+
if (!profile.email) {
|
|
2377
|
+
throw new Error("Profile contained no email");
|
|
2378
|
+
}
|
|
2379
|
+
const userId = profile.email.split("@")[0];
|
|
2103
2380
|
const token = await ctx.tokenIssuer.issueToken({
|
|
2104
|
-
claims: { sub:
|
|
2381
|
+
claims: { sub: userId, ent: [`user:default/${userId}`] }
|
|
2105
2382
|
});
|
|
2106
|
-
return { id, token };
|
|
2383
|
+
return { id: userId, token };
|
|
2107
2384
|
};
|
|
2108
|
-
const
|
|
2385
|
+
const createOidcProvider = (options) => {
|
|
2109
2386
|
return ({
|
|
2110
2387
|
providerId,
|
|
2111
2388
|
globalConfig,
|
|
@@ -2113,75 +2390,83 @@ const createSamlProvider = (options) => {
|
|
|
2113
2390
|
tokenIssuer,
|
|
2114
2391
|
catalogApi,
|
|
2115
2392
|
logger
|
|
2116
|
-
}) => {
|
|
2393
|
+
}) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
|
|
2117
2394
|
var _a, _b;
|
|
2395
|
+
const clientId = envConfig.getString("clientId");
|
|
2396
|
+
const clientSecret = envConfig.getString("clientSecret");
|
|
2397
|
+
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
2398
|
+
const metadataUrl = envConfig.getString("metadataUrl");
|
|
2399
|
+
const tokenSignedResponseAlg = envConfig.getOptionalString("tokenSignedResponseAlg");
|
|
2400
|
+
const scope = envConfig.getOptionalString("scope");
|
|
2401
|
+
const prompt = envConfig.getOptionalString("prompt");
|
|
2118
2402
|
const catalogIdentityClient = new CatalogIdentityClient({
|
|
2119
2403
|
catalogApi,
|
|
2120
2404
|
tokenIssuer
|
|
2121
2405
|
});
|
|
2122
|
-
const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({
|
|
2406
|
+
const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ userinfo }) => ({
|
|
2123
2407
|
profile: {
|
|
2124
|
-
|
|
2125
|
-
|
|
2408
|
+
displayName: userinfo.name,
|
|
2409
|
+
email: userinfo.email,
|
|
2410
|
+
picture: userinfo.picture
|
|
2126
2411
|
}
|
|
2127
2412
|
});
|
|
2128
|
-
const signInResolverFn = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b :
|
|
2413
|
+
const signInResolverFn = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : oAuth2DefaultSignInResolver;
|
|
2129
2414
|
const signInResolver = (info) => signInResolverFn(info, {
|
|
2130
2415
|
catalogIdentityClient,
|
|
2131
2416
|
tokenIssuer,
|
|
2132
2417
|
logger
|
|
2133
2418
|
});
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
authnContext: config.getOptionalStringArray("authnContext"),
|
|
2143
|
-
identifierFormat: config.getOptionalString("identifierFormat"),
|
|
2144
|
-
decryptionPvk: config.getOptionalString("decryptionPvk"),
|
|
2145
|
-
signatureAlgorithm: config.getOptionalString("signatureAlgorithm"),
|
|
2146
|
-
digestAlgorithm: config.getOptionalString("digestAlgorithm"),
|
|
2147
|
-
acceptedClockSkewMs: config.getOptionalNumber("acceptedClockSkewMs"),
|
|
2148
|
-
tokenIssuer,
|
|
2149
|
-
appUrl: globalConfig.appUrl,
|
|
2150
|
-
authHandler,
|
|
2419
|
+
const provider = new OidcAuthProvider({
|
|
2420
|
+
clientId,
|
|
2421
|
+
clientSecret,
|
|
2422
|
+
callbackUrl,
|
|
2423
|
+
tokenSignedResponseAlg,
|
|
2424
|
+
metadataUrl,
|
|
2425
|
+
scope,
|
|
2426
|
+
prompt,
|
|
2151
2427
|
signInResolver,
|
|
2428
|
+
authHandler,
|
|
2152
2429
|
logger,
|
|
2430
|
+
tokenIssuer,
|
|
2153
2431
|
catalogIdentityClient
|
|
2154
2432
|
});
|
|
2155
|
-
|
|
2433
|
+
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
2434
|
+
disableRefresh: false,
|
|
2435
|
+
providerId,
|
|
2436
|
+
tokenIssuer
|
|
2437
|
+
});
|
|
2438
|
+
});
|
|
2156
2439
|
};
|
|
2157
2440
|
|
|
2158
|
-
class
|
|
2159
|
-
constructor(options, verify) {
|
|
2160
|
-
const optionsWithURLs = {
|
|
2161
|
-
...options,
|
|
2162
|
-
authorizationURL: `https://${options.domain}/authorize`,
|
|
2163
|
-
tokenURL: `https://${options.domain}/oauth/token`,
|
|
2164
|
-
userInfoURL: `https://${options.domain}/userinfo`,
|
|
2165
|
-
apiUrl: `https://${options.domain}/api`
|
|
2166
|
-
};
|
|
2167
|
-
super(optionsWithURLs, verify);
|
|
2168
|
-
}
|
|
2169
|
-
}
|
|
2170
|
-
|
|
2171
|
-
class Auth0AuthProvider {
|
|
2441
|
+
class OktaAuthProvider {
|
|
2172
2442
|
constructor(options) {
|
|
2173
|
-
this.
|
|
2443
|
+
this._store = {
|
|
2444
|
+
store(_req, cb) {
|
|
2445
|
+
cb(null, null);
|
|
2446
|
+
},
|
|
2447
|
+
verify(_req, _state, cb) {
|
|
2448
|
+
cb(null, true);
|
|
2449
|
+
}
|
|
2450
|
+
};
|
|
2451
|
+
this._signInResolver = options.signInResolver;
|
|
2452
|
+
this._authHandler = options.authHandler;
|
|
2453
|
+
this._tokenIssuer = options.tokenIssuer;
|
|
2454
|
+
this._catalogIdentityClient = options.catalogIdentityClient;
|
|
2455
|
+
this._logger = options.logger;
|
|
2456
|
+
this._strategy = new passportOktaOauth.Strategy({
|
|
2174
2457
|
clientID: options.clientId,
|
|
2175
2458
|
clientSecret: options.clientSecret,
|
|
2176
2459
|
callbackURL: options.callbackUrl,
|
|
2177
|
-
|
|
2178
|
-
passReqToCallback: false
|
|
2460
|
+
audience: options.audience,
|
|
2461
|
+
passReqToCallback: false,
|
|
2462
|
+
store: this._store,
|
|
2463
|
+
response_type: "code"
|
|
2179
2464
|
}, (accessToken, refreshToken, params, fullProfile, done) => {
|
|
2180
2465
|
done(void 0, {
|
|
2181
|
-
fullProfile,
|
|
2182
2466
|
accessToken,
|
|
2183
2467
|
refreshToken,
|
|
2184
|
-
params
|
|
2468
|
+
params,
|
|
2469
|
+
fullProfile
|
|
2185
2470
|
}, {
|
|
2186
2471
|
refreshToken
|
|
2187
2472
|
});
|
|
@@ -2197,57 +2482,116 @@ class Auth0AuthProvider {
|
|
|
2197
2482
|
}
|
|
2198
2483
|
async handler(req) {
|
|
2199
2484
|
const { result, privateInfo } = await executeFrameHandlerStrategy(req, this._strategy);
|
|
2200
|
-
const profile = makeProfileInfo(result.fullProfile, result.params.id_token);
|
|
2201
2485
|
return {
|
|
2202
|
-
response: await this.
|
|
2203
|
-
profile,
|
|
2204
|
-
providerInfo: {
|
|
2205
|
-
idToken: result.params.id_token,
|
|
2206
|
-
accessToken: result.accessToken,
|
|
2207
|
-
scope: result.params.scope,
|
|
2208
|
-
expiresInSeconds: result.params.expires_in
|
|
2209
|
-
}
|
|
2210
|
-
}),
|
|
2486
|
+
response: await this.handleResult(result),
|
|
2211
2487
|
refreshToken: privateInfo.refreshToken
|
|
2212
2488
|
};
|
|
2213
2489
|
}
|
|
2214
2490
|
async refresh(req) {
|
|
2215
|
-
const { accessToken, params } = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
|
|
2491
|
+
const { accessToken, refreshToken, params } = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
|
|
2216
2492
|
const fullProfile = await executeFetchUserProfileStrategy(this._strategy, accessToken);
|
|
2217
|
-
|
|
2218
|
-
|
|
2493
|
+
return {
|
|
2494
|
+
response: await this.handleResult({
|
|
2495
|
+
fullProfile,
|
|
2496
|
+
params,
|
|
2497
|
+
accessToken
|
|
2498
|
+
}),
|
|
2499
|
+
refreshToken
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2502
|
+
async handleResult(result) {
|
|
2503
|
+
const context = {
|
|
2504
|
+
logger: this._logger,
|
|
2505
|
+
catalogIdentityClient: this._catalogIdentityClient,
|
|
2506
|
+
tokenIssuer: this._tokenIssuer
|
|
2507
|
+
};
|
|
2508
|
+
const { profile } = await this._authHandler(result, context);
|
|
2509
|
+
const response = {
|
|
2219
2510
|
providerInfo: {
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2511
|
+
idToken: result.params.id_token,
|
|
2512
|
+
accessToken: result.accessToken,
|
|
2513
|
+
scope: result.params.scope,
|
|
2514
|
+
expiresInSeconds: result.params.expires_in
|
|
2224
2515
|
},
|
|
2225
2516
|
profile
|
|
2226
|
-
}
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2517
|
+
};
|
|
2518
|
+
if (this._signInResolver) {
|
|
2519
|
+
response.backstageIdentity = await this._signInResolver({
|
|
2520
|
+
result,
|
|
2521
|
+
profile
|
|
2522
|
+
}, context);
|
|
2232
2523
|
}
|
|
2233
|
-
|
|
2234
|
-
return { ...response, backstageIdentity: { id, token: "" } };
|
|
2524
|
+
return response;
|
|
2235
2525
|
}
|
|
2236
2526
|
}
|
|
2237
|
-
const
|
|
2238
|
-
|
|
2527
|
+
const oktaEmailSignInResolver = async (info, ctx) => {
|
|
2528
|
+
const { profile } = info;
|
|
2529
|
+
if (!profile.email) {
|
|
2530
|
+
throw new Error("Okta profile contained no email");
|
|
2531
|
+
}
|
|
2532
|
+
const entity = await ctx.catalogIdentityClient.findUser({
|
|
2533
|
+
annotations: {
|
|
2534
|
+
"okta.com/email": profile.email
|
|
2535
|
+
}
|
|
2536
|
+
});
|
|
2537
|
+
const claims = getEntityClaims(entity);
|
|
2538
|
+
const token = await ctx.tokenIssuer.issueToken({ claims });
|
|
2539
|
+
return { id: entity.metadata.name, entity, token };
|
|
2540
|
+
};
|
|
2541
|
+
const oktaDefaultSignInResolver = async (info, ctx) => {
|
|
2542
|
+
const { profile } = info;
|
|
2543
|
+
if (!profile.email) {
|
|
2544
|
+
throw new Error("Okta profile contained no email");
|
|
2545
|
+
}
|
|
2546
|
+
const userId = profile.email.split("@")[0];
|
|
2547
|
+
const token = await ctx.tokenIssuer.issueToken({
|
|
2548
|
+
claims: { sub: userId, ent: [`user:default/${userId}`] }
|
|
2549
|
+
});
|
|
2550
|
+
return { id: userId, token };
|
|
2551
|
+
};
|
|
2552
|
+
const createOktaProvider = (_options) => {
|
|
2553
|
+
return ({
|
|
2554
|
+
providerId,
|
|
2555
|
+
globalConfig,
|
|
2556
|
+
config,
|
|
2557
|
+
tokenIssuer,
|
|
2558
|
+
catalogApi,
|
|
2559
|
+
logger
|
|
2560
|
+
}) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
|
|
2561
|
+
var _a, _b;
|
|
2239
2562
|
const clientId = envConfig.getString("clientId");
|
|
2240
2563
|
const clientSecret = envConfig.getString("clientSecret");
|
|
2241
|
-
const
|
|
2564
|
+
const audience = envConfig.getString("audience");
|
|
2242
2565
|
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
2243
|
-
|
|
2566
|
+
if (!audience.startsWith("https://")) {
|
|
2567
|
+
throw new Error("URL for 'audience' must start with 'https://'.");
|
|
2568
|
+
}
|
|
2569
|
+
const catalogIdentityClient = new CatalogIdentityClient({
|
|
2570
|
+
catalogApi,
|
|
2571
|
+
tokenIssuer
|
|
2572
|
+
});
|
|
2573
|
+
const authHandler = (_options == null ? void 0 : _options.authHandler) ? _options.authHandler : async ({ fullProfile, params }) => ({
|
|
2574
|
+
profile: makeProfileInfo(fullProfile, params.id_token)
|
|
2575
|
+
});
|
|
2576
|
+
const signInResolverFn = (_b = (_a = _options == null ? void 0 : _options.signIn) == null ? void 0 : _a.resolver) != null ? _b : oktaDefaultSignInResolver;
|
|
2577
|
+
const signInResolver = (info) => signInResolverFn(info, {
|
|
2578
|
+
catalogIdentityClient,
|
|
2579
|
+
tokenIssuer,
|
|
2580
|
+
logger
|
|
2581
|
+
});
|
|
2582
|
+
const provider = new OktaAuthProvider({
|
|
2583
|
+
audience,
|
|
2244
2584
|
clientId,
|
|
2245
2585
|
clientSecret,
|
|
2246
2586
|
callbackUrl,
|
|
2247
|
-
|
|
2587
|
+
authHandler,
|
|
2588
|
+
signInResolver,
|
|
2589
|
+
tokenIssuer,
|
|
2590
|
+
catalogIdentityClient,
|
|
2591
|
+
logger
|
|
2248
2592
|
});
|
|
2249
2593
|
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
2250
|
-
disableRefresh:
|
|
2594
|
+
disableRefresh: false,
|
|
2251
2595
|
providerId,
|
|
2252
2596
|
tokenIssuer
|
|
2253
2597
|
});
|
|
@@ -2256,6 +2600,11 @@ const createAuth0Provider = (_options) => {
|
|
|
2256
2600
|
|
|
2257
2601
|
class OneLoginProvider {
|
|
2258
2602
|
constructor(options) {
|
|
2603
|
+
this.signInResolver = options.signInResolver;
|
|
2604
|
+
this.authHandler = options.authHandler;
|
|
2605
|
+
this.tokenIssuer = options.tokenIssuer;
|
|
2606
|
+
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
2607
|
+
this.logger = options.logger;
|
|
2259
2608
|
this._strategy = new passportOneloginOauth.Strategy({
|
|
2260
2609
|
issuer: options.issuer,
|
|
2261
2610
|
clientID: options.clientId,
|
|
@@ -2283,54 +2632,88 @@ class OneLoginProvider {
|
|
|
2283
2632
|
}
|
|
2284
2633
|
async handler(req) {
|
|
2285
2634
|
const { result, privateInfo } = await executeFrameHandlerStrategy(req, this._strategy);
|
|
2286
|
-
const profile = makeProfileInfo(result.fullProfile, result.params.id_token);
|
|
2287
2635
|
return {
|
|
2288
|
-
response: await this.
|
|
2289
|
-
profile,
|
|
2290
|
-
providerInfo: {
|
|
2291
|
-
idToken: result.params.id_token,
|
|
2292
|
-
accessToken: result.accessToken,
|
|
2293
|
-
scope: result.params.scope,
|
|
2294
|
-
expiresInSeconds: result.params.expires_in
|
|
2295
|
-
}
|
|
2296
|
-
}),
|
|
2636
|
+
response: await this.handleResult(result),
|
|
2297
2637
|
refreshToken: privateInfo.refreshToken
|
|
2298
2638
|
};
|
|
2299
2639
|
}
|
|
2300
2640
|
async refresh(req) {
|
|
2301
|
-
const { accessToken, params } = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
|
|
2641
|
+
const { accessToken, refreshToken, params } = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
|
|
2302
2642
|
const fullProfile = await executeFetchUserProfileStrategy(this._strategy, accessToken);
|
|
2303
|
-
|
|
2304
|
-
|
|
2643
|
+
return {
|
|
2644
|
+
response: await this.handleResult({
|
|
2645
|
+
fullProfile,
|
|
2646
|
+
params,
|
|
2647
|
+
accessToken
|
|
2648
|
+
}),
|
|
2649
|
+
refreshToken
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2652
|
+
async handleResult(result) {
|
|
2653
|
+
const context = {
|
|
2654
|
+
logger: this.logger,
|
|
2655
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
2656
|
+
tokenIssuer: this.tokenIssuer
|
|
2657
|
+
};
|
|
2658
|
+
const { profile } = await this.authHandler(result, context);
|
|
2659
|
+
const response = {
|
|
2305
2660
|
providerInfo: {
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2661
|
+
idToken: result.params.id_token,
|
|
2662
|
+
accessToken: result.accessToken,
|
|
2663
|
+
scope: result.params.scope,
|
|
2664
|
+
expiresInSeconds: result.params.expires_in
|
|
2310
2665
|
},
|
|
2311
2666
|
profile
|
|
2312
|
-
}
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2667
|
+
};
|
|
2668
|
+
if (this.signInResolver) {
|
|
2669
|
+
response.backstageIdentity = await this.signInResolver({
|
|
2670
|
+
result,
|
|
2671
|
+
profile
|
|
2672
|
+
}, context);
|
|
2318
2673
|
}
|
|
2319
|
-
|
|
2320
|
-
return { ...response, backstageIdentity: { id, token: "" } };
|
|
2674
|
+
return response;
|
|
2321
2675
|
}
|
|
2322
2676
|
}
|
|
2323
|
-
const
|
|
2324
|
-
|
|
2677
|
+
const defaultSignInResolver = async (info) => {
|
|
2678
|
+
const { profile } = info;
|
|
2679
|
+
if (!profile.email) {
|
|
2680
|
+
throw new Error("OIDC profile contained no email");
|
|
2681
|
+
}
|
|
2682
|
+
const id = profile.email.split("@")[0];
|
|
2683
|
+
return { id, token: "" };
|
|
2684
|
+
};
|
|
2685
|
+
const createOneLoginProvider = (options) => {
|
|
2686
|
+
return ({
|
|
2687
|
+
providerId,
|
|
2688
|
+
globalConfig,
|
|
2689
|
+
config,
|
|
2690
|
+
tokenIssuer,
|
|
2691
|
+
catalogApi,
|
|
2692
|
+
logger
|
|
2693
|
+
}) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
|
|
2694
|
+
var _a, _b;
|
|
2325
2695
|
const clientId = envConfig.getString("clientId");
|
|
2326
2696
|
const clientSecret = envConfig.getString("clientSecret");
|
|
2327
2697
|
const issuer = envConfig.getString("issuer");
|
|
2328
2698
|
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
2699
|
+
const catalogIdentityClient = new CatalogIdentityClient({
|
|
2700
|
+
catalogApi,
|
|
2701
|
+
tokenIssuer
|
|
2702
|
+
});
|
|
2703
|
+
const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ fullProfile, params }) => ({
|
|
2704
|
+
profile: makeProfileInfo(fullProfile, params.id_token)
|
|
2705
|
+
});
|
|
2706
|
+
const signInResolver = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : defaultSignInResolver;
|
|
2329
2707
|
const provider = new OneLoginProvider({
|
|
2330
2708
|
clientId,
|
|
2331
2709
|
clientSecret,
|
|
2332
2710
|
callbackUrl,
|
|
2333
|
-
issuer
|
|
2711
|
+
issuer,
|
|
2712
|
+
authHandler,
|
|
2713
|
+
signInResolver,
|
|
2714
|
+
tokenIssuer,
|
|
2715
|
+
catalogIdentityClient,
|
|
2716
|
+
logger
|
|
2334
2717
|
});
|
|
2335
2718
|
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
2336
2719
|
disableRefresh: false,
|
|
@@ -2340,362 +2723,222 @@ const createOneLoginProvider = (_options) => {
|
|
|
2340
2723
|
});
|
|
2341
2724
|
};
|
|
2342
2725
|
|
|
2343
|
-
|
|
2344
|
-
google: createGoogleProvider(),
|
|
2345
|
-
github: createGithubProvider(),
|
|
2346
|
-
gitlab: createGitlabProvider(),
|
|
2347
|
-
saml: createSamlProvider(),
|
|
2348
|
-
okta: createOktaProvider(),
|
|
2349
|
-
auth0: createAuth0Provider(),
|
|
2350
|
-
microsoft: createMicrosoftProvider(),
|
|
2351
|
-
oauth2: createOAuth2Provider(),
|
|
2352
|
-
oidc: createOidcProvider(),
|
|
2353
|
-
onelogin: createOneLoginProvider(),
|
|
2354
|
-
awsalb: createAwsAlbProvider(),
|
|
2355
|
-
bitbucket: createBitbucketProvider(),
|
|
2356
|
-
atlassian: createAtlassianProvider()
|
|
2357
|
-
};
|
|
2358
|
-
|
|
2359
|
-
function createOidcRouter(options) {
|
|
2360
|
-
const { baseUrl, tokenIssuer } = options;
|
|
2361
|
-
const router = Router__default["default"]();
|
|
2362
|
-
const config = {
|
|
2363
|
-
issuer: baseUrl,
|
|
2364
|
-
token_endpoint: `${baseUrl}/v1/token`,
|
|
2365
|
-
userinfo_endpoint: `${baseUrl}/v1/userinfo`,
|
|
2366
|
-
jwks_uri: `${baseUrl}/.well-known/jwks.json`,
|
|
2367
|
-
response_types_supported: ["id_token"],
|
|
2368
|
-
subject_types_supported: ["public"],
|
|
2369
|
-
id_token_signing_alg_values_supported: ["RS256"],
|
|
2370
|
-
scopes_supported: ["openid"],
|
|
2371
|
-
token_endpoint_auth_methods_supported: [],
|
|
2372
|
-
claims_supported: ["sub"],
|
|
2373
|
-
grant_types_supported: []
|
|
2374
|
-
};
|
|
2375
|
-
router.get("/.well-known/openid-configuration", (_req, res) => {
|
|
2376
|
-
res.json(config);
|
|
2377
|
-
});
|
|
2378
|
-
router.get("/.well-known/jwks.json", async (_req, res) => {
|
|
2379
|
-
const { keys } = await tokenIssuer.listPublicKeys();
|
|
2380
|
-
res.json({ keys });
|
|
2381
|
-
});
|
|
2382
|
-
router.get("/v1/token", (_req, res) => {
|
|
2383
|
-
res.status(501).send("Not Implemented");
|
|
2384
|
-
});
|
|
2385
|
-
router.get("/v1/userinfo", (_req, res) => {
|
|
2386
|
-
res.status(501).send("Not Implemented");
|
|
2387
|
-
});
|
|
2388
|
-
return router;
|
|
2389
|
-
}
|
|
2390
|
-
|
|
2391
|
-
const CLOCK_MARGIN_S = 10;
|
|
2392
|
-
class IdentityClient {
|
|
2393
|
-
constructor(options) {
|
|
2394
|
-
this.discovery = options.discovery;
|
|
2395
|
-
this.issuer = options.issuer;
|
|
2396
|
-
this.keyStore = new jose.JWKS.KeyStore();
|
|
2397
|
-
this.keyStoreUpdated = 0;
|
|
2398
|
-
}
|
|
2399
|
-
async authenticate(token) {
|
|
2400
|
-
var _a;
|
|
2401
|
-
if (!token) {
|
|
2402
|
-
throw new errors.AuthenticationError("No token specified");
|
|
2403
|
-
}
|
|
2404
|
-
const key = await this.getKey(token);
|
|
2405
|
-
if (!key) {
|
|
2406
|
-
throw new errors.AuthenticationError("No signing key matching token found");
|
|
2407
|
-
}
|
|
2408
|
-
const decoded = jose.JWT.IdToken.verify(token, key, {
|
|
2409
|
-
algorithms: ["ES256"],
|
|
2410
|
-
audience: "backstage",
|
|
2411
|
-
issuer: this.issuer
|
|
2412
|
-
});
|
|
2413
|
-
if (!decoded.sub) {
|
|
2414
|
-
throw new errors.AuthenticationError("No user sub found in token");
|
|
2415
|
-
}
|
|
2416
|
-
const user = {
|
|
2417
|
-
id: decoded.sub,
|
|
2418
|
-
token,
|
|
2419
|
-
identity: {
|
|
2420
|
-
type: "user",
|
|
2421
|
-
userEntityRef: decoded.sub,
|
|
2422
|
-
ownershipEntityRefs: (_a = decoded.ent) != null ? _a : []
|
|
2423
|
-
}
|
|
2424
|
-
};
|
|
2425
|
-
return user;
|
|
2426
|
-
}
|
|
2427
|
-
static getBearerToken(authorizationHeader) {
|
|
2428
|
-
if (typeof authorizationHeader !== "string") {
|
|
2429
|
-
return void 0;
|
|
2430
|
-
}
|
|
2431
|
-
const matches = authorizationHeader.match(/Bearer\s+(\S+)/i);
|
|
2432
|
-
return matches == null ? void 0 : matches[1];
|
|
2433
|
-
}
|
|
2434
|
-
async getKey(rawJwtToken) {
|
|
2435
|
-
const { header, payload } = jose.JWT.decode(rawJwtToken, {
|
|
2436
|
-
complete: true
|
|
2437
|
-
});
|
|
2438
|
-
const keyStoreHasKey = !!this.keyStore.get({ kid: header.kid });
|
|
2439
|
-
const issuedAfterLastRefresh = (payload == null ? void 0 : payload.iat) && payload.iat > this.keyStoreUpdated - CLOCK_MARGIN_S;
|
|
2440
|
-
if (!keyStoreHasKey && issuedAfterLastRefresh) {
|
|
2441
|
-
await this.refreshKeyStore();
|
|
2442
|
-
}
|
|
2443
|
-
return this.keyStore.get({ kid: header.kid });
|
|
2444
|
-
}
|
|
2445
|
-
async listPublicKeys() {
|
|
2446
|
-
const url = `${await this.discovery.getBaseUrl("auth")}/.well-known/jwks.json`;
|
|
2447
|
-
const response = await fetch__default["default"](url);
|
|
2448
|
-
if (!response.ok) {
|
|
2449
|
-
const payload = await response.text();
|
|
2450
|
-
const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
|
|
2451
|
-
throw new Error(message);
|
|
2452
|
-
}
|
|
2453
|
-
const publicKeys = await response.json();
|
|
2454
|
-
return publicKeys;
|
|
2455
|
-
}
|
|
2456
|
-
async refreshKeyStore() {
|
|
2457
|
-
const now = Date.now() / 1e3;
|
|
2458
|
-
const publicKeys = await this.listPublicKeys();
|
|
2459
|
-
this.keyStore = jose.JWKS.asKeyStore({
|
|
2460
|
-
keys: publicKeys.keys.map((key) => key)
|
|
2461
|
-
});
|
|
2462
|
-
this.keyStoreUpdated = now;
|
|
2463
|
-
}
|
|
2464
|
-
}
|
|
2465
|
-
|
|
2466
|
-
const MS_IN_S = 1e3;
|
|
2467
|
-
class TokenFactory {
|
|
2726
|
+
class SamlAuthProvider {
|
|
2468
2727
|
constructor(options) {
|
|
2469
|
-
this.
|
|
2728
|
+
this.appUrl = options.appUrl;
|
|
2729
|
+
this.signInResolver = options.signInResolver;
|
|
2730
|
+
this.authHandler = options.authHandler;
|
|
2731
|
+
this.tokenIssuer = options.tokenIssuer;
|
|
2732
|
+
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
2470
2733
|
this.logger = options.logger;
|
|
2471
|
-
this.
|
|
2472
|
-
|
|
2473
|
-
}
|
|
2474
|
-
async issueToken(params) {
|
|
2475
|
-
const key = await this.getKey();
|
|
2476
|
-
const iss = this.issuer;
|
|
2477
|
-
const sub = params.claims.sub;
|
|
2478
|
-
const ent = params.claims.ent;
|
|
2479
|
-
const aud = "backstage";
|
|
2480
|
-
const iat = Math.floor(Date.now() / MS_IN_S);
|
|
2481
|
-
const exp = iat + this.keyDurationSeconds;
|
|
2482
|
-
this.logger.info(`Issuing token for ${sub}, with entities ${ent != null ? ent : []}`);
|
|
2483
|
-
return jose.JWS.sign({ iss, sub, aud, iat, exp, ent }, key, {
|
|
2484
|
-
alg: key.alg,
|
|
2485
|
-
kid: key.kid
|
|
2734
|
+
this.strategy = new passportSaml.Strategy({ ...options }, (fullProfile, done) => {
|
|
2735
|
+
done(void 0, { fullProfile });
|
|
2486
2736
|
});
|
|
2487
2737
|
}
|
|
2488
|
-
async
|
|
2489
|
-
const {
|
|
2490
|
-
|
|
2491
|
-
const expiredKeys = [];
|
|
2492
|
-
for (const key of keys) {
|
|
2493
|
-
const expireAt = luxon.DateTime.fromJSDate(key.createdAt).plus({
|
|
2494
|
-
seconds: 3 * this.keyDurationSeconds
|
|
2495
|
-
});
|
|
2496
|
-
if (expireAt < luxon.DateTime.local()) {
|
|
2497
|
-
expiredKeys.push(key);
|
|
2498
|
-
} else {
|
|
2499
|
-
validKeys.push(key);
|
|
2500
|
-
}
|
|
2501
|
-
}
|
|
2502
|
-
if (expiredKeys.length > 0) {
|
|
2503
|
-
const kids = expiredKeys.map(({ key }) => key.kid);
|
|
2504
|
-
this.logger.info(`Removing expired signing keys, '${kids.join("', '")}'`);
|
|
2505
|
-
this.keyStore.removeKeys(kids).catch((error) => {
|
|
2506
|
-
this.logger.error(`Failed to remove expired keys, ${error}`);
|
|
2507
|
-
});
|
|
2508
|
-
}
|
|
2509
|
-
return { keys: validKeys.map(({ key }) => key) };
|
|
2738
|
+
async start(req, res) {
|
|
2739
|
+
const { url } = await executeRedirectStrategy(req, this.strategy, {});
|
|
2740
|
+
res.redirect(url);
|
|
2510
2741
|
}
|
|
2511
|
-
async
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2742
|
+
async frameHandler(req, res) {
|
|
2743
|
+
try {
|
|
2744
|
+
const context = {
|
|
2745
|
+
logger: this.logger,
|
|
2746
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
2747
|
+
tokenIssuer: this.tokenIssuer
|
|
2748
|
+
};
|
|
2749
|
+
const { result } = await executeFrameHandlerStrategy(req, this.strategy);
|
|
2750
|
+
const { profile } = await this.authHandler(result, context);
|
|
2751
|
+
const response = {
|
|
2752
|
+
profile,
|
|
2753
|
+
providerInfo: {}
|
|
2754
|
+
};
|
|
2755
|
+
if (this.signInResolver) {
|
|
2756
|
+
const signInResponse = await this.signInResolver({
|
|
2757
|
+
result,
|
|
2758
|
+
profile
|
|
2759
|
+
}, context);
|
|
2760
|
+
response.backstageIdentity = prepareBackstageIdentityResponse(signInResponse);
|
|
2515
2761
|
}
|
|
2516
|
-
this.
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
this.keyExpiry = luxon.DateTime.utc().plus({
|
|
2520
|
-
seconds: this.keyDurationSeconds
|
|
2521
|
-
}).toJSDate();
|
|
2522
|
-
const promise = (async () => {
|
|
2523
|
-
const key = await jose.JWK.generate("EC", "P-256", {
|
|
2524
|
-
use: "sig",
|
|
2525
|
-
kid: uuid.v4(),
|
|
2526
|
-
alg: "ES256"
|
|
2762
|
+
return postMessageResponse(res, this.appUrl, {
|
|
2763
|
+
type: "authorization_response",
|
|
2764
|
+
response
|
|
2527
2765
|
});
|
|
2528
|
-
this.logger.info(`Created new signing key ${key.kid}`);
|
|
2529
|
-
await this.keyStore.addKey(key.toJWK(false));
|
|
2530
|
-
return key;
|
|
2531
|
-
})();
|
|
2532
|
-
this.privateKeyPromise = promise;
|
|
2533
|
-
try {
|
|
2534
|
-
await promise;
|
|
2535
2766
|
} catch (error) {
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2767
|
+
const { name, message } = errors.isError(error) ? error : new Error("Encountered invalid error");
|
|
2768
|
+
return postMessageResponse(res, this.appUrl, {
|
|
2769
|
+
type: "authorization_response",
|
|
2770
|
+
error: { name, message }
|
|
2771
|
+
});
|
|
2539
2772
|
}
|
|
2540
|
-
return promise;
|
|
2541
2773
|
}
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
const migrationsDir = backendCommon.resolvePackagePath("@backstage/plugin-auth-backend", "migrations");
|
|
2545
|
-
const TABLE = "signing_keys";
|
|
2546
|
-
const parseDate = (date) => {
|
|
2547
|
-
const parsedDate = typeof date === "string" ? luxon.DateTime.fromSQL(date, { zone: "UTC" }) : luxon.DateTime.fromJSDate(date);
|
|
2548
|
-
if (!parsedDate.isValid) {
|
|
2549
|
-
throw new Error(`Failed to parse date, reason: ${parsedDate.invalidReason}, explanation: ${parsedDate.invalidExplanation}`);
|
|
2774
|
+
async logout(_req, res) {
|
|
2775
|
+
res.end();
|
|
2550
2776
|
}
|
|
2551
|
-
|
|
2777
|
+
}
|
|
2778
|
+
const samlDefaultSignInResolver = async (info, ctx) => {
|
|
2779
|
+
const id = info.result.fullProfile.nameID;
|
|
2780
|
+
const token = await ctx.tokenIssuer.issueToken({
|
|
2781
|
+
claims: { sub: id }
|
|
2782
|
+
});
|
|
2783
|
+
return { id, token };
|
|
2552
2784
|
};
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2785
|
+
const createSamlProvider = (options) => {
|
|
2786
|
+
return ({
|
|
2787
|
+
providerId,
|
|
2788
|
+
globalConfig,
|
|
2789
|
+
config,
|
|
2790
|
+
tokenIssuer,
|
|
2791
|
+
catalogApi,
|
|
2792
|
+
logger
|
|
2793
|
+
}) => {
|
|
2794
|
+
var _a, _b;
|
|
2795
|
+
const catalogIdentityClient = new CatalogIdentityClient({
|
|
2796
|
+
catalogApi,
|
|
2797
|
+
tokenIssuer
|
|
2558
2798
|
});
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
async addKey(key) {
|
|
2565
|
-
await this.database(TABLE).insert({
|
|
2566
|
-
kid: key.kid,
|
|
2567
|
-
key: JSON.stringify(key)
|
|
2799
|
+
const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ fullProfile }) => ({
|
|
2800
|
+
profile: {
|
|
2801
|
+
email: fullProfile.email,
|
|
2802
|
+
displayName: fullProfile.displayName
|
|
2803
|
+
}
|
|
2568
2804
|
});
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
}
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2805
|
+
const signInResolverFn = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : samlDefaultSignInResolver;
|
|
2806
|
+
const signInResolver = (info) => signInResolverFn(info, {
|
|
2807
|
+
catalogIdentityClient,
|
|
2808
|
+
tokenIssuer,
|
|
2809
|
+
logger
|
|
2810
|
+
});
|
|
2811
|
+
return new SamlAuthProvider({
|
|
2812
|
+
callbackUrl: `${globalConfig.baseUrl}/${providerId}/handler/frame`,
|
|
2813
|
+
entryPoint: config.getString("entryPoint"),
|
|
2814
|
+
logoutUrl: config.getOptionalString("logoutUrl"),
|
|
2815
|
+
audience: config.getOptionalString("audience"),
|
|
2816
|
+
issuer: config.getString("issuer"),
|
|
2817
|
+
cert: config.getString("cert"),
|
|
2818
|
+
privateKey: config.getOptionalString("privateKey"),
|
|
2819
|
+
authnContext: config.getOptionalStringArray("authnContext"),
|
|
2820
|
+
identifierFormat: config.getOptionalString("identifierFormat"),
|
|
2821
|
+
decryptionPvk: config.getOptionalString("decryptionPvk"),
|
|
2822
|
+
signatureAlgorithm: config.getOptionalString("signatureAlgorithm"),
|
|
2823
|
+
digestAlgorithm: config.getOptionalString("digestAlgorithm"),
|
|
2824
|
+
acceptedClockSkewMs: config.getOptionalNumber("acceptedClockSkewMs"),
|
|
2825
|
+
tokenIssuer,
|
|
2826
|
+
appUrl: globalConfig.appUrl,
|
|
2827
|
+
authHandler,
|
|
2828
|
+
signInResolver,
|
|
2829
|
+
logger,
|
|
2830
|
+
catalogIdentityClient
|
|
2592
2831
|
});
|
|
2832
|
+
};
|
|
2833
|
+
};
|
|
2834
|
+
|
|
2835
|
+
const IAP_JWT_HEADER = "x-goog-iap-jwt-assertion";
|
|
2836
|
+
|
|
2837
|
+
function createTokenValidator(audience, mockClient) {
|
|
2838
|
+
const client = mockClient != null ? mockClient : new googleAuthLibrary.OAuth2Client();
|
|
2839
|
+
return async function tokenValidator(token) {
|
|
2840
|
+
const response = await client.getIapPublicKeys();
|
|
2841
|
+
const ticket = await client.verifySignedJwtWithCertsAsync(token, response.pubkeys, audience, ["https://cloud.google.com/iap"]);
|
|
2842
|
+
const payload = ticket.getPayload();
|
|
2843
|
+
if (!payload) {
|
|
2844
|
+
throw new TypeError("Token had no payload");
|
|
2845
|
+
}
|
|
2846
|
+
return payload;
|
|
2847
|
+
};
|
|
2848
|
+
}
|
|
2849
|
+
async function parseRequestToken(jwtToken, tokenValidator) {
|
|
2850
|
+
if (typeof jwtToken !== "string" || !jwtToken) {
|
|
2851
|
+
throw new errors.AuthenticationError(`Missing Google IAP header: ${IAP_JWT_HEADER}`);
|
|
2593
2852
|
}
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2853
|
+
let payload;
|
|
2854
|
+
try {
|
|
2855
|
+
payload = await tokenValidator(jwtToken);
|
|
2856
|
+
} catch (e) {
|
|
2857
|
+
throw new errors.AuthenticationError(`Google IAP token verification failed, ${e}`);
|
|
2598
2858
|
}
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
items: Array.from(this.keys).map(([, { createdAt, key: keyStr }]) => ({
|
|
2602
|
-
createdAt,
|
|
2603
|
-
key: JSON.parse(keyStr)
|
|
2604
|
-
}))
|
|
2605
|
-
};
|
|
2859
|
+
if (!payload.sub || !payload.email) {
|
|
2860
|
+
throw new errors.AuthenticationError("Google IAP token payload is missing sub and/or email claim");
|
|
2606
2861
|
}
|
|
2862
|
+
return {
|
|
2863
|
+
iapToken: {
|
|
2864
|
+
...payload,
|
|
2865
|
+
sub: payload.sub,
|
|
2866
|
+
email: payload.email
|
|
2867
|
+
}
|
|
2868
|
+
};
|
|
2607
2869
|
}
|
|
2870
|
+
const defaultAuthHandler = async ({
|
|
2871
|
+
iapToken
|
|
2872
|
+
}) => ({ profile: { email: iapToken.email } });
|
|
2608
2873
|
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
this.
|
|
2614
|
-
this.
|
|
2615
|
-
this.
|
|
2616
|
-
|
|
2617
|
-
static async create(settings) {
|
|
2618
|
-
const { path, timeout, ...firestoreSettings } = settings != null ? settings : {};
|
|
2619
|
-
const database = new firestore.Firestore(firestoreSettings);
|
|
2620
|
-
return new FirestoreKeyStore(database, path != null ? path : DEFAULT_DOCUMENT_PATH, timeout != null ? timeout : DEFAULT_TIMEOUT_MS);
|
|
2874
|
+
class GcpIapProvider {
|
|
2875
|
+
constructor(options) {
|
|
2876
|
+
this.authHandler = options.authHandler;
|
|
2877
|
+
this.signInResolver = options.signInResolver;
|
|
2878
|
+
this.tokenValidator = options.tokenValidator;
|
|
2879
|
+
this.tokenIssuer = options.tokenIssuer;
|
|
2880
|
+
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
2881
|
+
this.logger = options.logger;
|
|
2621
2882
|
}
|
|
2622
|
-
|
|
2623
|
-
try {
|
|
2624
|
-
await keyStore.verify();
|
|
2625
|
-
} catch (error) {
|
|
2626
|
-
if (process.env.NODE_ENV !== "development") {
|
|
2627
|
-
throw new Error(`Failed to connect to database: ${error.message}`);
|
|
2628
|
-
}
|
|
2629
|
-
logger == null ? void 0 : logger.warn(`Failed to connect to database: ${error.message}`);
|
|
2630
|
-
}
|
|
2883
|
+
async start() {
|
|
2631
2884
|
}
|
|
2632
|
-
async
|
|
2633
|
-
await this.withTimeout(this.database.collection(this.path).doc(key.kid).set({
|
|
2634
|
-
kid: key.kid,
|
|
2635
|
-
key: JSON.stringify(key)
|
|
2636
|
-
}));
|
|
2885
|
+
async frameHandler() {
|
|
2637
2886
|
}
|
|
2638
|
-
async
|
|
2639
|
-
const
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
}))
|
|
2887
|
+
async refresh(req, res) {
|
|
2888
|
+
const result = await parseRequestToken(req.header(IAP_JWT_HEADER), this.tokenValidator);
|
|
2889
|
+
const context = {
|
|
2890
|
+
logger: this.logger,
|
|
2891
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
2892
|
+
tokenIssuer: this.tokenIssuer
|
|
2645
2893
|
};
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
reject(new Error(`Operation timed out after ${this.timeout}ms`));
|
|
2655
|
-
}, this.timeout));
|
|
2656
|
-
return Promise.race([operation, timer]);
|
|
2657
|
-
}
|
|
2658
|
-
async verify() {
|
|
2659
|
-
await this.withTimeout(this.database.collection(this.path).limit(1).get());
|
|
2894
|
+
const { profile } = await this.authHandler(result, context);
|
|
2895
|
+
const backstageIdentity = await this.signInResolver({ profile, result }, context);
|
|
2896
|
+
const response = {
|
|
2897
|
+
providerInfo: { iapToken: result.iapToken },
|
|
2898
|
+
profile,
|
|
2899
|
+
backstageIdentity: prepareBackstageIdentityResponse(backstageIdentity)
|
|
2900
|
+
};
|
|
2901
|
+
res.json(response);
|
|
2660
2902
|
}
|
|
2661
2903
|
}
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
static async fromConfig(config, options) {
|
|
2904
|
+
function createGcpIapProvider(options) {
|
|
2905
|
+
return ({ config, tokenIssuer, catalogApi, logger }) => {
|
|
2665
2906
|
var _a;
|
|
2666
|
-
const
|
|
2667
|
-
const
|
|
2668
|
-
const
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
const keyStore = await FirestoreKeyStore.create(lodash.pickBy({
|
|
2684
|
-
projectId: settings == null ? void 0 : settings.getOptionalString("projectId"),
|
|
2685
|
-
keyFilename: settings == null ? void 0 : settings.getOptionalString("keyFilename"),
|
|
2686
|
-
host: settings == null ? void 0 : settings.getOptionalString("host"),
|
|
2687
|
-
port: settings == null ? void 0 : settings.getOptionalNumber("port"),
|
|
2688
|
-
ssl: settings == null ? void 0 : settings.getOptionalBoolean("ssl"),
|
|
2689
|
-
path: settings == null ? void 0 : settings.getOptionalString("path"),
|
|
2690
|
-
timeout: settings == null ? void 0 : settings.getOptionalNumber("timeout")
|
|
2691
|
-
}, (value) => value !== void 0));
|
|
2692
|
-
await FirestoreKeyStore.verifyConnection(keyStore, logger);
|
|
2693
|
-
return keyStore;
|
|
2694
|
-
}
|
|
2695
|
-
throw new Error(`Unknown KeyStore provider: ${provider}`);
|
|
2696
|
-
}
|
|
2907
|
+
const audience = config.getString("audience");
|
|
2908
|
+
const authHandler = (_a = options.authHandler) != null ? _a : defaultAuthHandler;
|
|
2909
|
+
const signInResolver = options.signIn.resolver;
|
|
2910
|
+
const tokenValidator = createTokenValidator(audience);
|
|
2911
|
+
const catalogIdentityClient = new CatalogIdentityClient({
|
|
2912
|
+
catalogApi,
|
|
2913
|
+
tokenIssuer
|
|
2914
|
+
});
|
|
2915
|
+
return new GcpIapProvider({
|
|
2916
|
+
authHandler,
|
|
2917
|
+
signInResolver,
|
|
2918
|
+
tokenValidator,
|
|
2919
|
+
tokenIssuer,
|
|
2920
|
+
catalogIdentityClient,
|
|
2921
|
+
logger
|
|
2922
|
+
});
|
|
2923
|
+
};
|
|
2697
2924
|
}
|
|
2698
2925
|
|
|
2926
|
+
const factories = {
|
|
2927
|
+
google: createGoogleProvider(),
|
|
2928
|
+
github: createGithubProvider(),
|
|
2929
|
+
gitlab: createGitlabProvider(),
|
|
2930
|
+
saml: createSamlProvider(),
|
|
2931
|
+
okta: createOktaProvider(),
|
|
2932
|
+
auth0: createAuth0Provider(),
|
|
2933
|
+
microsoft: createMicrosoftProvider(),
|
|
2934
|
+
oauth2: createOAuth2Provider(),
|
|
2935
|
+
oidc: createOidcProvider(),
|
|
2936
|
+
onelogin: createOneLoginProvider(),
|
|
2937
|
+
awsalb: createAwsAlbProvider(),
|
|
2938
|
+
bitbucket: createBitbucketProvider(),
|
|
2939
|
+
atlassian: createAtlassianProvider()
|
|
2940
|
+
};
|
|
2941
|
+
|
|
2699
2942
|
async function createRouter(options) {
|
|
2700
2943
|
const { logger, config, discovery, database, providerFactories } = options;
|
|
2701
2944
|
const router = Router__default["default"]();
|
|
@@ -2713,7 +2956,13 @@ async function createRouter(options) {
|
|
|
2713
2956
|
const secret = config.getOptionalString("auth.session.secret");
|
|
2714
2957
|
if (secret) {
|
|
2715
2958
|
router.use(cookieParser__default["default"](secret));
|
|
2716
|
-
|
|
2959
|
+
const enforceCookieSSL = authUrl.startsWith("https");
|
|
2960
|
+
router.use(session__default["default"]({
|
|
2961
|
+
secret,
|
|
2962
|
+
saveUninitialized: false,
|
|
2963
|
+
resave: false,
|
|
2964
|
+
cookie: { secure: enforceCookieSSL ? "auto" : false }
|
|
2965
|
+
}));
|
|
2717
2966
|
router.use(passport__default["default"].initialize());
|
|
2718
2967
|
router.use(passport__default["default"].session());
|
|
2719
2968
|
} else {
|
|
@@ -2799,15 +3048,19 @@ exports.OAuthEnvironmentHandler = OAuthEnvironmentHandler;
|
|
|
2799
3048
|
exports.bitbucketUserIdSignInResolver = bitbucketUserIdSignInResolver;
|
|
2800
3049
|
exports.bitbucketUsernameSignInResolver = bitbucketUsernameSignInResolver;
|
|
2801
3050
|
exports.createAtlassianProvider = createAtlassianProvider;
|
|
3051
|
+
exports.createAuth0Provider = createAuth0Provider;
|
|
2802
3052
|
exports.createAwsAlbProvider = createAwsAlbProvider;
|
|
2803
3053
|
exports.createBitbucketProvider = createBitbucketProvider;
|
|
3054
|
+
exports.createGcpIapProvider = createGcpIapProvider;
|
|
2804
3055
|
exports.createGithubProvider = createGithubProvider;
|
|
2805
3056
|
exports.createGitlabProvider = createGitlabProvider;
|
|
2806
3057
|
exports.createGoogleProvider = createGoogleProvider;
|
|
2807
3058
|
exports.createMicrosoftProvider = createMicrosoftProvider;
|
|
2808
3059
|
exports.createOAuth2Provider = createOAuth2Provider;
|
|
3060
|
+
exports.createOauth2ProxyProvider = createOauth2ProxyProvider;
|
|
2809
3061
|
exports.createOidcProvider = createOidcProvider;
|
|
2810
3062
|
exports.createOktaProvider = createOktaProvider;
|
|
3063
|
+
exports.createOneLoginProvider = createOneLoginProvider;
|
|
2811
3064
|
exports.createOriginFilter = createOriginFilter;
|
|
2812
3065
|
exports.createRouter = createRouter;
|
|
2813
3066
|
exports.createSamlProvider = createSamlProvider;
|