@backstage/plugin-auth-backend 0.6.1 → 0.8.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 +67 -0
- package/dist/index.cjs.js +959 -846
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +67 -32
- package/package.json +10 -10
package/dist/index.cjs.js
CHANGED
|
@@ -20,17 +20,17 @@ var passportGithub2 = require('passport-github2');
|
|
|
20
20
|
var passportGitlab2 = require('passport-gitlab2');
|
|
21
21
|
var passportGoogleOauth20 = require('passport-google-oauth20');
|
|
22
22
|
var passportMicrosoft = require('passport-microsoft');
|
|
23
|
+
var uuid = require('uuid');
|
|
24
|
+
var luxon = require('luxon');
|
|
25
|
+
var backendCommon = require('@backstage/backend-common');
|
|
26
|
+
var firestore = require('@google-cloud/firestore');
|
|
27
|
+
var lodash = require('lodash');
|
|
23
28
|
var openidClient = require('openid-client');
|
|
24
29
|
var passportOktaOauth = require('passport-okta-oauth');
|
|
25
30
|
var passportOneloginOauth = require('passport-onelogin-oauth');
|
|
26
31
|
var passportSaml = require('passport-saml');
|
|
27
32
|
var googleAuthLibrary = require('google-auth-library');
|
|
28
33
|
var catalogClient = require('@backstage/catalog-client');
|
|
29
|
-
var uuid = require('uuid');
|
|
30
|
-
var luxon = require('luxon');
|
|
31
|
-
var backendCommon = require('@backstage/backend-common');
|
|
32
|
-
var firestore = require('@google-cloud/firestore');
|
|
33
|
-
var lodash = require('lodash');
|
|
34
34
|
var session = require('express-session');
|
|
35
35
|
var passport = require('passport');
|
|
36
36
|
var minimatch = require('minimatch');
|
|
@@ -149,6 +149,16 @@ const verifyNonce = (req, providerId) => {
|
|
|
149
149
|
throw new Error("Invalid nonce");
|
|
150
150
|
}
|
|
151
151
|
};
|
|
152
|
+
const getCookieConfig = (authUrl, providerId) => {
|
|
153
|
+
const { hostname: cookieDomain, pathname, protocol } = authUrl;
|
|
154
|
+
const secure = protocol === "https:";
|
|
155
|
+
const cookiePath = pathname.endsWith(`${providerId}/handler/frame`) ? pathname.slice(0, -"/handler/frame".length) : `${pathname}/${providerId}`;
|
|
156
|
+
return {
|
|
157
|
+
cookieDomain,
|
|
158
|
+
cookiePath,
|
|
159
|
+
secure
|
|
160
|
+
};
|
|
161
|
+
};
|
|
152
162
|
|
|
153
163
|
class OAuthEnvironmentHandler {
|
|
154
164
|
constructor(handlers) {
|
|
@@ -245,6 +255,10 @@ function parseJwtPayload(token) {
|
|
|
245
255
|
}
|
|
246
256
|
function prepareBackstageIdentityResponse(result) {
|
|
247
257
|
const { sub, ent } = parseJwtPayload(result.token);
|
|
258
|
+
const userEntityRef = catalogModel.stringifyEntityRef(catalogModel.parseEntityRef(sub, {
|
|
259
|
+
defaultKind: "user",
|
|
260
|
+
defaultNamespace: catalogModel.ENTITY_DEFAULT_NAMESPACE
|
|
261
|
+
}));
|
|
248
262
|
return {
|
|
249
263
|
...{
|
|
250
264
|
idToken: result.token,
|
|
@@ -252,7 +266,7 @@ function prepareBackstageIdentityResponse(result) {
|
|
|
252
266
|
},
|
|
253
267
|
identity: {
|
|
254
268
|
type: "user",
|
|
255
|
-
userEntityRef
|
|
269
|
+
userEntityRef,
|
|
256
270
|
ownershipEntityRefs: ent != null ? ent : []
|
|
257
271
|
}
|
|
258
272
|
};
|
|
@@ -309,14 +323,14 @@ class OAuthAdapter {
|
|
|
309
323
|
};
|
|
310
324
|
}
|
|
311
325
|
static fromConfig(config, handlers, options) {
|
|
326
|
+
var _a;
|
|
312
327
|
const { origin: appOrigin } = new url.URL(config.appUrl);
|
|
313
|
-
const
|
|
314
|
-
const
|
|
315
|
-
const cookiePath = `${url$1.pathname}/${options.providerId}`;
|
|
328
|
+
const authUrl = new url.URL((_a = options.callbackUrl) != null ? _a : config.baseUrl);
|
|
329
|
+
const { cookieDomain, cookiePath, secure } = getCookieConfig(authUrl, options.providerId);
|
|
316
330
|
return new OAuthAdapter(handlers, {
|
|
317
331
|
...options,
|
|
318
332
|
appOrigin,
|
|
319
|
-
cookieDomain
|
|
333
|
+
cookieDomain,
|
|
320
334
|
cookiePath,
|
|
321
335
|
secure,
|
|
322
336
|
isOriginAllowed: config.isOriginAllowed
|
|
@@ -655,7 +669,12 @@ class AtlassianAuthProvider {
|
|
|
655
669
|
};
|
|
656
670
|
}
|
|
657
671
|
async handleResult(result) {
|
|
658
|
-
const
|
|
672
|
+
const context = {
|
|
673
|
+
logger: this.logger,
|
|
674
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
675
|
+
tokenIssuer: this.tokenIssuer
|
|
676
|
+
};
|
|
677
|
+
const { profile } = await this.authHandler(result, context);
|
|
659
678
|
const response = {
|
|
660
679
|
providerInfo: {
|
|
661
680
|
idToken: result.params.id_token,
|
|
@@ -669,11 +688,7 @@ class AtlassianAuthProvider {
|
|
|
669
688
|
response.backstageIdentity = await this.signInResolver({
|
|
670
689
|
result,
|
|
671
690
|
profile
|
|
672
|
-
},
|
|
673
|
-
tokenIssuer: this.tokenIssuer,
|
|
674
|
-
catalogIdentityClient: this.catalogIdentityClient,
|
|
675
|
-
logger: this.logger
|
|
676
|
-
});
|
|
691
|
+
}, context);
|
|
677
692
|
}
|
|
678
693
|
return response;
|
|
679
694
|
}
|
|
@@ -793,7 +808,12 @@ class Auth0AuthProvider {
|
|
|
793
808
|
};
|
|
794
809
|
}
|
|
795
810
|
async handleResult(result) {
|
|
796
|
-
const
|
|
811
|
+
const context = {
|
|
812
|
+
logger: this.logger,
|
|
813
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
814
|
+
tokenIssuer: this.tokenIssuer
|
|
815
|
+
};
|
|
816
|
+
const { profile } = await this.authHandler(result, context);
|
|
797
817
|
const response = {
|
|
798
818
|
providerInfo: {
|
|
799
819
|
idToken: result.params.id_token,
|
|
@@ -807,11 +827,7 @@ class Auth0AuthProvider {
|
|
|
807
827
|
response.backstageIdentity = await this.signInResolver({
|
|
808
828
|
result,
|
|
809
829
|
profile
|
|
810
|
-
},
|
|
811
|
-
tokenIssuer: this.tokenIssuer,
|
|
812
|
-
catalogIdentityClient: this.catalogIdentityClient,
|
|
813
|
-
logger: this.logger
|
|
814
|
-
});
|
|
830
|
+
}, context);
|
|
815
831
|
}
|
|
816
832
|
return response;
|
|
817
833
|
}
|
|
@@ -937,15 +953,16 @@ class AwsAlbAuthProvider {
|
|
|
937
953
|
}
|
|
938
954
|
}
|
|
939
955
|
async handleResult(result) {
|
|
940
|
-
const
|
|
941
|
-
const backstageIdentity = await this.signInResolver({
|
|
942
|
-
result,
|
|
943
|
-
profile
|
|
944
|
-
}, {
|
|
956
|
+
const context = {
|
|
945
957
|
tokenIssuer: this.tokenIssuer,
|
|
946
958
|
catalogIdentityClient: this.catalogIdentityClient,
|
|
947
959
|
logger: this.logger
|
|
948
|
-
}
|
|
960
|
+
};
|
|
961
|
+
const { profile } = await this.authHandler(result, context);
|
|
962
|
+
const backstageIdentity = await this.signInResolver({
|
|
963
|
+
result,
|
|
964
|
+
profile
|
|
965
|
+
}, context);
|
|
949
966
|
return {
|
|
950
967
|
providerInfo: {
|
|
951
968
|
accessToken: result.accessToken,
|
|
@@ -1045,7 +1062,12 @@ class BitbucketAuthProvider {
|
|
|
1045
1062
|
}
|
|
1046
1063
|
async handleResult(result) {
|
|
1047
1064
|
result.fullProfile.avatarUrl = result.fullProfile._json.links.avatar.href;
|
|
1048
|
-
const
|
|
1065
|
+
const context = {
|
|
1066
|
+
logger: this.logger,
|
|
1067
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
1068
|
+
tokenIssuer: this.tokenIssuer
|
|
1069
|
+
};
|
|
1070
|
+
const { profile } = await this.authHandler(result, context);
|
|
1049
1071
|
const response = {
|
|
1050
1072
|
providerInfo: {
|
|
1051
1073
|
idToken: result.params.id_token,
|
|
@@ -1059,11 +1081,7 @@ class BitbucketAuthProvider {
|
|
|
1059
1081
|
response.backstageIdentity = await this.signInResolver({
|
|
1060
1082
|
result,
|
|
1061
1083
|
profile
|
|
1062
|
-
},
|
|
1063
|
-
tokenIssuer: this.tokenIssuer,
|
|
1064
|
-
catalogIdentityClient: this.catalogIdentityClient,
|
|
1065
|
-
logger: this.logger
|
|
1066
|
-
});
|
|
1084
|
+
}, context);
|
|
1067
1085
|
}
|
|
1068
1086
|
return response;
|
|
1069
1087
|
}
|
|
@@ -1179,7 +1197,12 @@ class GithubAuthProvider {
|
|
|
1179
1197
|
};
|
|
1180
1198
|
}
|
|
1181
1199
|
async handleResult(result) {
|
|
1182
|
-
const
|
|
1200
|
+
const context = {
|
|
1201
|
+
logger: this.logger,
|
|
1202
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
1203
|
+
tokenIssuer: this.tokenIssuer
|
|
1204
|
+
};
|
|
1205
|
+
const { profile } = await this.authHandler(result, context);
|
|
1183
1206
|
const expiresInStr = result.params.expires_in;
|
|
1184
1207
|
const response = {
|
|
1185
1208
|
providerInfo: {
|
|
@@ -1193,11 +1216,7 @@ class GithubAuthProvider {
|
|
|
1193
1216
|
response.backstageIdentity = await this.signInResolver({
|
|
1194
1217
|
result,
|
|
1195
1218
|
profile
|
|
1196
|
-
},
|
|
1197
|
-
tokenIssuer: this.tokenIssuer,
|
|
1198
|
-
catalogIdentityClient: this.catalogIdentityClient,
|
|
1199
|
-
logger: this.logger
|
|
1200
|
-
});
|
|
1219
|
+
}, context);
|
|
1201
1220
|
}
|
|
1202
1221
|
return response;
|
|
1203
1222
|
}
|
|
@@ -1206,7 +1225,10 @@ const githubDefaultSignInResolver = async (info, ctx) => {
|
|
|
1206
1225
|
const { fullProfile } = info.result;
|
|
1207
1226
|
const userId = fullProfile.username || fullProfile.id;
|
|
1208
1227
|
const token = await ctx.tokenIssuer.issueToken({
|
|
1209
|
-
claims: {
|
|
1228
|
+
claims: {
|
|
1229
|
+
sub: `user:default/${userId}`,
|
|
1230
|
+
ent: [`user:default/${userId}`]
|
|
1231
|
+
}
|
|
1210
1232
|
});
|
|
1211
1233
|
return { id: userId, token };
|
|
1212
1234
|
};
|
|
@@ -1261,7 +1283,8 @@ const createGithubProvider = (options) => {
|
|
|
1261
1283
|
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
1262
1284
|
persistScopes: true,
|
|
1263
1285
|
providerId,
|
|
1264
|
-
tokenIssuer
|
|
1286
|
+
tokenIssuer,
|
|
1287
|
+
callbackUrl
|
|
1265
1288
|
});
|
|
1266
1289
|
});
|
|
1267
1290
|
};
|
|
@@ -1273,7 +1296,7 @@ const gitlabDefaultSignInResolver = async (info, ctx) => {
|
|
|
1273
1296
|
id = profile.email.split("@")[0];
|
|
1274
1297
|
}
|
|
1275
1298
|
const token = await ctx.tokenIssuer.issueToken({
|
|
1276
|
-
claims: { sub: id
|
|
1299
|
+
claims: { sub: `user:default/${id}`, ent: [`user:default/${id}`] }
|
|
1277
1300
|
});
|
|
1278
1301
|
return { id, token };
|
|
1279
1302
|
};
|
|
@@ -1327,7 +1350,12 @@ class GitlabAuthProvider {
|
|
|
1327
1350
|
};
|
|
1328
1351
|
}
|
|
1329
1352
|
async handleResult(result) {
|
|
1330
|
-
const
|
|
1353
|
+
const context = {
|
|
1354
|
+
logger: this.logger,
|
|
1355
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
1356
|
+
tokenIssuer: this.tokenIssuer
|
|
1357
|
+
};
|
|
1358
|
+
const { profile } = await this.authHandler(result, context);
|
|
1331
1359
|
const response = {
|
|
1332
1360
|
providerInfo: {
|
|
1333
1361
|
idToken: result.params.id_token,
|
|
@@ -1341,11 +1369,7 @@ class GitlabAuthProvider {
|
|
|
1341
1369
|
response.backstageIdentity = await this.signInResolver({
|
|
1342
1370
|
result,
|
|
1343
1371
|
profile
|
|
1344
|
-
},
|
|
1345
|
-
tokenIssuer: this.tokenIssuer,
|
|
1346
|
-
catalogIdentityClient: this.catalogIdentityClient,
|
|
1347
|
-
logger: this.logger
|
|
1348
|
-
});
|
|
1372
|
+
}, context);
|
|
1349
1373
|
}
|
|
1350
1374
|
return response;
|
|
1351
1375
|
}
|
|
@@ -1446,7 +1470,12 @@ class GoogleAuthProvider {
|
|
|
1446
1470
|
};
|
|
1447
1471
|
}
|
|
1448
1472
|
async handleResult(result) {
|
|
1449
|
-
const
|
|
1473
|
+
const context = {
|
|
1474
|
+
logger: this.logger,
|
|
1475
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
1476
|
+
tokenIssuer: this.tokenIssuer
|
|
1477
|
+
};
|
|
1478
|
+
const { profile } = await this.authHandler(result, context);
|
|
1450
1479
|
const response = {
|
|
1451
1480
|
providerInfo: {
|
|
1452
1481
|
idToken: result.params.id_token,
|
|
@@ -1460,11 +1489,7 @@ class GoogleAuthProvider {
|
|
|
1460
1489
|
response.backstageIdentity = await this.signInResolver({
|
|
1461
1490
|
result,
|
|
1462
1491
|
profile
|
|
1463
|
-
},
|
|
1464
|
-
tokenIssuer: this.tokenIssuer,
|
|
1465
|
-
catalogIdentityClient: this.catalogIdentityClient,
|
|
1466
|
-
logger: this.logger
|
|
1467
|
-
});
|
|
1492
|
+
}, context);
|
|
1468
1493
|
}
|
|
1469
1494
|
return response;
|
|
1470
1495
|
}
|
|
@@ -1501,7 +1526,7 @@ const googleDefaultSignInResolver = async (info, ctx) => {
|
|
|
1501
1526
|
userId = profile.email.split("@")[0];
|
|
1502
1527
|
}
|
|
1503
1528
|
const token = await ctx.tokenIssuer.issueToken({
|
|
1504
|
-
claims: { sub: userId
|
|
1529
|
+
claims: { sub: `user:default/${userId}`, ent: [`user:default/${userId}`] }
|
|
1505
1530
|
});
|
|
1506
1531
|
return { id: userId, token };
|
|
1507
1532
|
};
|
|
@@ -1595,7 +1620,12 @@ class MicrosoftAuthProvider {
|
|
|
1595
1620
|
async handleResult(result) {
|
|
1596
1621
|
const photo = await this.getUserPhoto(result.accessToken);
|
|
1597
1622
|
result.fullProfile.photos = photo ? [{ value: photo }] : void 0;
|
|
1598
|
-
const
|
|
1623
|
+
const context = {
|
|
1624
|
+
logger: this.logger,
|
|
1625
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
1626
|
+
tokenIssuer: this.tokenIssuer
|
|
1627
|
+
};
|
|
1628
|
+
const { profile } = await this.authHandler(result, context);
|
|
1599
1629
|
const response = {
|
|
1600
1630
|
providerInfo: {
|
|
1601
1631
|
idToken: result.params.id_token,
|
|
@@ -1609,11 +1639,7 @@ class MicrosoftAuthProvider {
|
|
|
1609
1639
|
response.backstageIdentity = await this.signInResolver({
|
|
1610
1640
|
result,
|
|
1611
1641
|
profile
|
|
1612
|
-
},
|
|
1613
|
-
tokenIssuer: this.tokenIssuer,
|
|
1614
|
-
catalogIdentityClient: this.catalogIdentityClient,
|
|
1615
|
-
logger: this.logger
|
|
1616
|
-
});
|
|
1642
|
+
}, context);
|
|
1617
1643
|
}
|
|
1618
1644
|
return response;
|
|
1619
1645
|
}
|
|
@@ -1654,7 +1680,10 @@ const microsoftDefaultSignInResolver = async (info, ctx) => {
|
|
|
1654
1680
|
}
|
|
1655
1681
|
const userId = profile.email.split("@")[0];
|
|
1656
1682
|
const token = await ctx.tokenIssuer.issueToken({
|
|
1657
|
-
claims: {
|
|
1683
|
+
claims: {
|
|
1684
|
+
sub: `user:default/${userId}`,
|
|
1685
|
+
ent: [`user:default/${userId}`]
|
|
1686
|
+
}
|
|
1658
1687
|
});
|
|
1659
1688
|
return { id: userId, token };
|
|
1660
1689
|
};
|
|
@@ -1765,7 +1794,12 @@ class OAuth2AuthProvider {
|
|
|
1765
1794
|
};
|
|
1766
1795
|
}
|
|
1767
1796
|
async handleResult(result) {
|
|
1768
|
-
const
|
|
1797
|
+
const context = {
|
|
1798
|
+
logger: this.logger,
|
|
1799
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
1800
|
+
tokenIssuer: this.tokenIssuer
|
|
1801
|
+
};
|
|
1802
|
+
const { profile } = await this.authHandler(result, context);
|
|
1769
1803
|
const response = {
|
|
1770
1804
|
providerInfo: {
|
|
1771
1805
|
idToken: result.params.id_token,
|
|
@@ -1779,11 +1813,7 @@ class OAuth2AuthProvider {
|
|
|
1779
1813
|
response.backstageIdentity = await this.signInResolver({
|
|
1780
1814
|
result,
|
|
1781
1815
|
profile
|
|
1782
|
-
},
|
|
1783
|
-
tokenIssuer: this.tokenIssuer,
|
|
1784
|
-
catalogIdentityClient: this.catalogIdentityClient,
|
|
1785
|
-
logger: this.logger
|
|
1786
|
-
});
|
|
1816
|
+
}, context);
|
|
1787
1817
|
}
|
|
1788
1818
|
return response;
|
|
1789
1819
|
}
|
|
@@ -1798,7 +1828,7 @@ const oAuth2DefaultSignInResolver$1 = async (info, ctx) => {
|
|
|
1798
1828
|
}
|
|
1799
1829
|
const userId = profile.email.split("@")[0];
|
|
1800
1830
|
const token = await ctx.tokenIssuer.issueToken({
|
|
1801
|
-
claims: { sub: userId
|
|
1831
|
+
claims: { sub: `user:default/${userId}`, ent: [`user:default/${userId}`] }
|
|
1802
1832
|
});
|
|
1803
1833
|
return { id: userId, token };
|
|
1804
1834
|
};
|
|
@@ -1855,110 +1885,528 @@ const createOAuth2Provider = (options) => {
|
|
|
1855
1885
|
});
|
|
1856
1886
|
};
|
|
1857
1887
|
|
|
1858
|
-
|
|
1888
|
+
function createOidcRouter(options) {
|
|
1889
|
+
const { baseUrl, tokenIssuer } = options;
|
|
1890
|
+
const router = Router__default["default"]();
|
|
1891
|
+
const config = {
|
|
1892
|
+
issuer: baseUrl,
|
|
1893
|
+
token_endpoint: `${baseUrl}/v1/token`,
|
|
1894
|
+
userinfo_endpoint: `${baseUrl}/v1/userinfo`,
|
|
1895
|
+
jwks_uri: `${baseUrl}/.well-known/jwks.json`,
|
|
1896
|
+
response_types_supported: ["id_token"],
|
|
1897
|
+
subject_types_supported: ["public"],
|
|
1898
|
+
id_token_signing_alg_values_supported: ["RS256"],
|
|
1899
|
+
scopes_supported: ["openid"],
|
|
1900
|
+
token_endpoint_auth_methods_supported: [],
|
|
1901
|
+
claims_supported: ["sub"],
|
|
1902
|
+
grant_types_supported: []
|
|
1903
|
+
};
|
|
1904
|
+
router.get("/.well-known/openid-configuration", (_req, res) => {
|
|
1905
|
+
res.json(config);
|
|
1906
|
+
});
|
|
1907
|
+
router.get("/.well-known/jwks.json", async (_req, res) => {
|
|
1908
|
+
const { keys } = await tokenIssuer.listPublicKeys();
|
|
1909
|
+
res.json({ keys });
|
|
1910
|
+
});
|
|
1911
|
+
router.get("/v1/token", (_req, res) => {
|
|
1912
|
+
res.status(501).send("Not Implemented");
|
|
1913
|
+
});
|
|
1914
|
+
router.get("/v1/userinfo", (_req, res) => {
|
|
1915
|
+
res.status(501).send("Not Implemented");
|
|
1916
|
+
});
|
|
1917
|
+
return router;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
const CLOCK_MARGIN_S = 10;
|
|
1921
|
+
class IdentityClient {
|
|
1859
1922
|
constructor(options) {
|
|
1860
|
-
this.
|
|
1861
|
-
this.
|
|
1862
|
-
this.
|
|
1863
|
-
this.
|
|
1864
|
-
this.authHandler = options.authHandler;
|
|
1865
|
-
this.tokenIssuer = options.tokenIssuer;
|
|
1866
|
-
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
1867
|
-
this.logger = options.logger;
|
|
1923
|
+
this.discovery = options.discovery;
|
|
1924
|
+
this.issuer = options.issuer;
|
|
1925
|
+
this.keyStore = new jose.JWKS.KeyStore();
|
|
1926
|
+
this.keyStoreUpdated = 0;
|
|
1868
1927
|
}
|
|
1869
|
-
async
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
state: encodeState(req.state)
|
|
1874
|
-
};
|
|
1875
|
-
const prompt = this.prompt || "none";
|
|
1876
|
-
if (prompt !== "auto") {
|
|
1877
|
-
options.prompt = prompt;
|
|
1928
|
+
async authenticate(token) {
|
|
1929
|
+
var _a;
|
|
1930
|
+
if (!token) {
|
|
1931
|
+
throw new errors.AuthenticationError("No token specified");
|
|
1878
1932
|
}
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
const
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1933
|
+
const key = await this.getKey(token);
|
|
1934
|
+
if (!key) {
|
|
1935
|
+
throw new errors.AuthenticationError("No signing key matching token found");
|
|
1936
|
+
}
|
|
1937
|
+
const decoded = jose.JWT.IdToken.verify(token, key, {
|
|
1938
|
+
algorithms: ["ES256"],
|
|
1939
|
+
audience: "backstage",
|
|
1940
|
+
issuer: this.issuer
|
|
1941
|
+
});
|
|
1942
|
+
if (!decoded.sub) {
|
|
1943
|
+
throw new errors.AuthenticationError("No user sub found in token");
|
|
1944
|
+
}
|
|
1945
|
+
const user = {
|
|
1946
|
+
id: decoded.sub,
|
|
1947
|
+
token,
|
|
1948
|
+
identity: {
|
|
1949
|
+
type: "user",
|
|
1950
|
+
userEntityRef: decoded.sub,
|
|
1951
|
+
ownershipEntityRefs: (_a = decoded.ent) != null ? _a : []
|
|
1952
|
+
}
|
|
1887
1953
|
};
|
|
1954
|
+
return user;
|
|
1888
1955
|
}
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
if (!tokenset.access_token) {
|
|
1893
|
-
throw new Error("Refresh failed");
|
|
1956
|
+
static getBearerToken(authorizationHeader) {
|
|
1957
|
+
if (typeof authorizationHeader !== "string") {
|
|
1958
|
+
return void 0;
|
|
1894
1959
|
}
|
|
1895
|
-
const
|
|
1896
|
-
return
|
|
1897
|
-
response: await this.handleResult({ tokenset, userinfo }),
|
|
1898
|
-
refreshToken: tokenset.refresh_token
|
|
1899
|
-
};
|
|
1960
|
+
const matches = authorizationHeader.match(/Bearer\s+(\S+)/i);
|
|
1961
|
+
return matches == null ? void 0 : matches[1];
|
|
1900
1962
|
}
|
|
1901
|
-
async
|
|
1902
|
-
const
|
|
1903
|
-
|
|
1904
|
-
access_type: "offline",
|
|
1905
|
-
client_id: options.clientId,
|
|
1906
|
-
client_secret: options.clientSecret,
|
|
1907
|
-
redirect_uris: [options.callbackUrl],
|
|
1908
|
-
response_types: ["code"],
|
|
1909
|
-
id_token_signed_response_alg: options.tokenSignedResponseAlg || "RS256",
|
|
1910
|
-
scope: options.scope || ""
|
|
1911
|
-
});
|
|
1912
|
-
const strategy = new openidClient.Strategy({
|
|
1913
|
-
client,
|
|
1914
|
-
passReqToCallback: false
|
|
1915
|
-
}, (tokenset, userinfo, done) => {
|
|
1916
|
-
if (typeof done !== "function") {
|
|
1917
|
-
throw new Error("OIDC IdP must provide a userinfo_endpoint in the metadata response");
|
|
1918
|
-
}
|
|
1919
|
-
done(void 0, { tokenset, userinfo }, {
|
|
1920
|
-
refreshToken: tokenset.refresh_token
|
|
1921
|
-
});
|
|
1963
|
+
async getKey(rawJwtToken) {
|
|
1964
|
+
const { header, payload } = jose.JWT.decode(rawJwtToken, {
|
|
1965
|
+
complete: true
|
|
1922
1966
|
});
|
|
1923
|
-
|
|
1924
|
-
|
|
1967
|
+
const keyStoreHasKey = !!this.keyStore.get({ kid: header.kid });
|
|
1968
|
+
const issuedAfterLastRefresh = (payload == null ? void 0 : payload.iat) && payload.iat > this.keyStoreUpdated - CLOCK_MARGIN_S;
|
|
1969
|
+
if (!keyStoreHasKey && issuedAfterLastRefresh) {
|
|
1970
|
+
await this.refreshKeyStore();
|
|
1971
|
+
}
|
|
1972
|
+
return this.keyStore.get({ kid: header.kid });
|
|
1925
1973
|
}
|
|
1926
|
-
async
|
|
1927
|
-
const
|
|
1928
|
-
const response =
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
expiresInSeconds: result.tokenset.expires_in
|
|
1934
|
-
},
|
|
1935
|
-
profile
|
|
1936
|
-
};
|
|
1937
|
-
if (this.signInResolver) {
|
|
1938
|
-
response.backstageIdentity = await this.signInResolver({
|
|
1939
|
-
result,
|
|
1940
|
-
profile
|
|
1941
|
-
}, {
|
|
1942
|
-
tokenIssuer: this.tokenIssuer,
|
|
1943
|
-
catalogIdentityClient: this.catalogIdentityClient,
|
|
1944
|
-
logger: this.logger
|
|
1945
|
-
});
|
|
1974
|
+
async listPublicKeys() {
|
|
1975
|
+
const url = `${await this.discovery.getBaseUrl("auth")}/.well-known/jwks.json`;
|
|
1976
|
+
const response = await fetch__default["default"](url);
|
|
1977
|
+
if (!response.ok) {
|
|
1978
|
+
const payload = await response.text();
|
|
1979
|
+
const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
|
|
1980
|
+
throw new Error(message);
|
|
1946
1981
|
}
|
|
1947
|
-
|
|
1982
|
+
const publicKeys = await response.json();
|
|
1983
|
+
return publicKeys;
|
|
1984
|
+
}
|
|
1985
|
+
async refreshKeyStore() {
|
|
1986
|
+
const now = Date.now() / 1e3;
|
|
1987
|
+
const publicKeys = await this.listPublicKeys();
|
|
1988
|
+
this.keyStore = jose.JWKS.asKeyStore({
|
|
1989
|
+
keys: publicKeys.keys.map((key) => key)
|
|
1990
|
+
});
|
|
1991
|
+
this.keyStoreUpdated = now;
|
|
1948
1992
|
}
|
|
1949
1993
|
}
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1994
|
+
|
|
1995
|
+
const MS_IN_S = 1e3;
|
|
1996
|
+
class TokenFactory {
|
|
1997
|
+
constructor(options) {
|
|
1998
|
+
this.issuer = options.issuer;
|
|
1999
|
+
this.logger = options.logger;
|
|
2000
|
+
this.keyStore = options.keyStore;
|
|
2001
|
+
this.keyDurationSeconds = options.keyDurationSeconds;
|
|
1954
2002
|
}
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
const
|
|
2003
|
+
async issueToken(params) {
|
|
2004
|
+
const key = await this.getKey();
|
|
2005
|
+
const iss = this.issuer;
|
|
2006
|
+
const sub = params.claims.sub;
|
|
2007
|
+
const ent = params.claims.ent;
|
|
2008
|
+
const aud = "backstage";
|
|
2009
|
+
const iat = Math.floor(Date.now() / MS_IN_S);
|
|
2010
|
+
const exp = iat + this.keyDurationSeconds;
|
|
2011
|
+
this.logger.info(`Issuing token for ${sub}, with entities ${ent != null ? ent : []}`);
|
|
2012
|
+
return jose.JWS.sign({ iss, sub, aud, iat, exp, ent }, key, {
|
|
2013
|
+
alg: key.alg,
|
|
2014
|
+
kid: key.kid
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
async listPublicKeys() {
|
|
2018
|
+
const { items: keys } = await this.keyStore.listKeys();
|
|
2019
|
+
const validKeys = [];
|
|
2020
|
+
const expiredKeys = [];
|
|
2021
|
+
for (const key of keys) {
|
|
2022
|
+
const expireAt = luxon.DateTime.fromJSDate(key.createdAt).plus({
|
|
2023
|
+
seconds: 3 * this.keyDurationSeconds
|
|
2024
|
+
});
|
|
2025
|
+
if (expireAt < luxon.DateTime.local()) {
|
|
2026
|
+
expiredKeys.push(key);
|
|
2027
|
+
} else {
|
|
2028
|
+
validKeys.push(key);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
if (expiredKeys.length > 0) {
|
|
2032
|
+
const kids = expiredKeys.map(({ key }) => key.kid);
|
|
2033
|
+
this.logger.info(`Removing expired signing keys, '${kids.join("', '")}'`);
|
|
2034
|
+
this.keyStore.removeKeys(kids).catch((error) => {
|
|
2035
|
+
this.logger.error(`Failed to remove expired keys, ${error}`);
|
|
2036
|
+
});
|
|
2037
|
+
}
|
|
2038
|
+
return { keys: validKeys.map(({ key }) => key) };
|
|
2039
|
+
}
|
|
2040
|
+
async getKey() {
|
|
2041
|
+
if (this.privateKeyPromise) {
|
|
2042
|
+
if (this.keyExpiry && luxon.DateTime.fromJSDate(this.keyExpiry) > luxon.DateTime.local()) {
|
|
2043
|
+
return this.privateKeyPromise;
|
|
2044
|
+
}
|
|
2045
|
+
this.logger.info(`Signing key has expired, generating new key`);
|
|
2046
|
+
delete this.privateKeyPromise;
|
|
2047
|
+
}
|
|
2048
|
+
this.keyExpiry = luxon.DateTime.utc().plus({
|
|
2049
|
+
seconds: this.keyDurationSeconds
|
|
2050
|
+
}).toJSDate();
|
|
2051
|
+
const promise = (async () => {
|
|
2052
|
+
const key = await jose.JWK.generate("EC", "P-256", {
|
|
2053
|
+
use: "sig",
|
|
2054
|
+
kid: uuid.v4(),
|
|
2055
|
+
alg: "ES256"
|
|
2056
|
+
});
|
|
2057
|
+
this.logger.info(`Created new signing key ${key.kid}`);
|
|
2058
|
+
await this.keyStore.addKey(key.toJWK(false));
|
|
2059
|
+
return key;
|
|
2060
|
+
})();
|
|
2061
|
+
this.privateKeyPromise = promise;
|
|
2062
|
+
try {
|
|
2063
|
+
await promise;
|
|
2064
|
+
} catch (error) {
|
|
2065
|
+
this.logger.error(`Failed to generate new signing key, ${error}`);
|
|
2066
|
+
delete this.keyExpiry;
|
|
2067
|
+
delete this.privateKeyPromise;
|
|
2068
|
+
}
|
|
2069
|
+
return promise;
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
const migrationsDir = backendCommon.resolvePackagePath("@backstage/plugin-auth-backend", "migrations");
|
|
2074
|
+
const TABLE = "signing_keys";
|
|
2075
|
+
const parseDate = (date) => {
|
|
2076
|
+
const parsedDate = typeof date === "string" ? luxon.DateTime.fromSQL(date, { zone: "UTC" }) : luxon.DateTime.fromJSDate(date);
|
|
2077
|
+
if (!parsedDate.isValid) {
|
|
2078
|
+
throw new Error(`Failed to parse date, reason: ${parsedDate.invalidReason}, explanation: ${parsedDate.invalidExplanation}`);
|
|
2079
|
+
}
|
|
2080
|
+
return parsedDate.toJSDate();
|
|
2081
|
+
};
|
|
2082
|
+
class DatabaseKeyStore {
|
|
2083
|
+
static async create(options) {
|
|
2084
|
+
const { database } = options;
|
|
2085
|
+
await database.migrate.latest({
|
|
2086
|
+
directory: migrationsDir
|
|
2087
|
+
});
|
|
2088
|
+
return new DatabaseKeyStore(options);
|
|
2089
|
+
}
|
|
2090
|
+
constructor(options) {
|
|
2091
|
+
this.database = options.database;
|
|
2092
|
+
}
|
|
2093
|
+
async addKey(key) {
|
|
2094
|
+
await this.database(TABLE).insert({
|
|
2095
|
+
kid: key.kid,
|
|
2096
|
+
key: JSON.stringify(key)
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
async listKeys() {
|
|
2100
|
+
const rows = await this.database(TABLE).select();
|
|
2101
|
+
return {
|
|
2102
|
+
items: rows.map((row) => ({
|
|
2103
|
+
key: JSON.parse(row.key),
|
|
2104
|
+
createdAt: parseDate(row.created_at)
|
|
2105
|
+
}))
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
async removeKeys(kids) {
|
|
2109
|
+
await this.database(TABLE).delete().whereIn("kid", kids);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
class MemoryKeyStore {
|
|
2114
|
+
constructor() {
|
|
2115
|
+
this.keys = /* @__PURE__ */ new Map();
|
|
2116
|
+
}
|
|
2117
|
+
async addKey(key) {
|
|
2118
|
+
this.keys.set(key.kid, {
|
|
2119
|
+
createdAt: luxon.DateTime.utc().toJSDate(),
|
|
2120
|
+
key: JSON.stringify(key)
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
2123
|
+
async removeKeys(kids) {
|
|
2124
|
+
for (const kid of kids) {
|
|
2125
|
+
this.keys.delete(kid);
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
async listKeys() {
|
|
2129
|
+
return {
|
|
2130
|
+
items: Array.from(this.keys).map(([, { createdAt, key: keyStr }]) => ({
|
|
2131
|
+
createdAt,
|
|
2132
|
+
key: JSON.parse(keyStr)
|
|
2133
|
+
}))
|
|
2134
|
+
};
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
2139
|
+
const DEFAULT_DOCUMENT_PATH = "sessions";
|
|
2140
|
+
class FirestoreKeyStore {
|
|
2141
|
+
constructor(database, path, timeout) {
|
|
2142
|
+
this.database = database;
|
|
2143
|
+
this.path = path;
|
|
2144
|
+
this.timeout = timeout;
|
|
2145
|
+
}
|
|
2146
|
+
static async create(settings) {
|
|
2147
|
+
const { path, timeout, ...firestoreSettings } = settings != null ? settings : {};
|
|
2148
|
+
const database = new firestore.Firestore(firestoreSettings);
|
|
2149
|
+
return new FirestoreKeyStore(database, path != null ? path : DEFAULT_DOCUMENT_PATH, timeout != null ? timeout : DEFAULT_TIMEOUT_MS);
|
|
2150
|
+
}
|
|
2151
|
+
static async verifyConnection(keyStore, logger) {
|
|
2152
|
+
try {
|
|
2153
|
+
await keyStore.verify();
|
|
2154
|
+
} catch (error) {
|
|
2155
|
+
if (process.env.NODE_ENV !== "development") {
|
|
2156
|
+
throw new Error(`Failed to connect to database: ${error.message}`);
|
|
2157
|
+
}
|
|
2158
|
+
logger == null ? void 0 : logger.warn(`Failed to connect to database: ${error.message}`);
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
async addKey(key) {
|
|
2162
|
+
await this.withTimeout(this.database.collection(this.path).doc(key.kid).set({
|
|
2163
|
+
kid: key.kid,
|
|
2164
|
+
key: JSON.stringify(key)
|
|
2165
|
+
}));
|
|
2166
|
+
}
|
|
2167
|
+
async listKeys() {
|
|
2168
|
+
const keys = await this.withTimeout(this.database.collection(this.path).get());
|
|
2169
|
+
return {
|
|
2170
|
+
items: keys.docs.map((key) => ({
|
|
2171
|
+
key: key.data(),
|
|
2172
|
+
createdAt: key.createTime.toDate()
|
|
2173
|
+
}))
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
async removeKeys(kids) {
|
|
2177
|
+
for (const kid of kids) {
|
|
2178
|
+
await this.withTimeout(this.database.collection(this.path).doc(kid).delete());
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
async withTimeout(operation) {
|
|
2182
|
+
const timer = new Promise((_, reject) => setTimeout(() => {
|
|
2183
|
+
reject(new Error(`Operation timed out after ${this.timeout}ms`));
|
|
2184
|
+
}, this.timeout));
|
|
2185
|
+
return Promise.race([operation, timer]);
|
|
2186
|
+
}
|
|
2187
|
+
async verify() {
|
|
2188
|
+
await this.withTimeout(this.database.collection(this.path).limit(1).get());
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
class KeyStores {
|
|
2193
|
+
static async fromConfig(config, options) {
|
|
2194
|
+
var _a;
|
|
2195
|
+
const { logger, database } = options != null ? options : {};
|
|
2196
|
+
const ks = config.getOptionalConfig("auth.keyStore");
|
|
2197
|
+
const provider = (_a = ks == null ? void 0 : ks.getOptionalString("provider")) != null ? _a : "database";
|
|
2198
|
+
logger == null ? void 0 : logger.info(`Configuring "${provider}" as KeyStore provider`);
|
|
2199
|
+
if (provider === "database") {
|
|
2200
|
+
if (!database) {
|
|
2201
|
+
throw new Error("This KeyStore provider requires a database");
|
|
2202
|
+
}
|
|
2203
|
+
return await DatabaseKeyStore.create({
|
|
2204
|
+
database: await database.getClient()
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
if (provider === "memory") {
|
|
2208
|
+
return new MemoryKeyStore();
|
|
2209
|
+
}
|
|
2210
|
+
if (provider === "firestore") {
|
|
2211
|
+
const settings = ks == null ? void 0 : ks.getConfig(provider);
|
|
2212
|
+
const keyStore = await FirestoreKeyStore.create(lodash.pickBy({
|
|
2213
|
+
projectId: settings == null ? void 0 : settings.getOptionalString("projectId"),
|
|
2214
|
+
keyFilename: settings == null ? void 0 : settings.getOptionalString("keyFilename"),
|
|
2215
|
+
host: settings == null ? void 0 : settings.getOptionalString("host"),
|
|
2216
|
+
port: settings == null ? void 0 : settings.getOptionalNumber("port"),
|
|
2217
|
+
ssl: settings == null ? void 0 : settings.getOptionalBoolean("ssl"),
|
|
2218
|
+
path: settings == null ? void 0 : settings.getOptionalString("path"),
|
|
2219
|
+
timeout: settings == null ? void 0 : settings.getOptionalNumber("timeout")
|
|
2220
|
+
}, (value) => value !== void 0));
|
|
2221
|
+
await FirestoreKeyStore.verifyConnection(keyStore, logger);
|
|
2222
|
+
return keyStore;
|
|
2223
|
+
}
|
|
2224
|
+
throw new Error(`Unknown KeyStore provider: ${provider}`);
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
const OAUTH2_PROXY_JWT_HEADER = "X-OAUTH2-PROXY-ID-TOKEN";
|
|
2229
|
+
class Oauth2ProxyAuthProvider {
|
|
2230
|
+
constructor(options) {
|
|
2231
|
+
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
2232
|
+
this.logger = options.logger;
|
|
2233
|
+
this.tokenIssuer = options.tokenIssuer;
|
|
2234
|
+
this.signInResolver = options.signInResolver;
|
|
2235
|
+
this.authHandler = options.authHandler;
|
|
2236
|
+
}
|
|
2237
|
+
frameHandler() {
|
|
2238
|
+
return Promise.resolve(void 0);
|
|
2239
|
+
}
|
|
2240
|
+
async refresh(req, res) {
|
|
2241
|
+
try {
|
|
2242
|
+
const result = this.getResult(req);
|
|
2243
|
+
const response = await this.handleResult(result);
|
|
2244
|
+
res.json(response);
|
|
2245
|
+
} catch (e) {
|
|
2246
|
+
this.logger.error(`Exception occurred during ${OAUTH2_PROXY_JWT_HEADER} refresh`, e);
|
|
2247
|
+
res.status(401);
|
|
2248
|
+
res.end();
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
start() {
|
|
2252
|
+
return Promise.resolve(void 0);
|
|
2253
|
+
}
|
|
2254
|
+
async handleResult(result) {
|
|
2255
|
+
const ctx = {
|
|
2256
|
+
logger: this.logger,
|
|
2257
|
+
tokenIssuer: this.tokenIssuer,
|
|
2258
|
+
catalogIdentityClient: this.catalogIdentityClient
|
|
2259
|
+
};
|
|
2260
|
+
const { profile } = await this.authHandler(result, ctx);
|
|
2261
|
+
const backstageSignInResult = await this.signInResolver({
|
|
2262
|
+
result,
|
|
2263
|
+
profile
|
|
2264
|
+
}, ctx);
|
|
2265
|
+
return {
|
|
2266
|
+
providerInfo: {
|
|
2267
|
+
accessToken: result.accessToken
|
|
2268
|
+
},
|
|
2269
|
+
backstageIdentity: prepareBackstageIdentityResponse(backstageSignInResult),
|
|
2270
|
+
profile
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2273
|
+
getResult(req) {
|
|
2274
|
+
const authHeader = req.header(OAUTH2_PROXY_JWT_HEADER);
|
|
2275
|
+
const jwt = IdentityClient.getBearerToken(authHeader);
|
|
2276
|
+
if (!jwt) {
|
|
2277
|
+
throw new errors.AuthenticationError(`Missing or in incorrect format - Oauth2Proxy OIDC header: ${OAUTH2_PROXY_JWT_HEADER}`);
|
|
2278
|
+
}
|
|
2279
|
+
const decodedJWT = jose.JWT.decode(jwt);
|
|
2280
|
+
return {
|
|
2281
|
+
fullProfile: decodedJWT,
|
|
2282
|
+
accessToken: jwt
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
const createOauth2ProxyProvider = (options) => ({ catalogApi, logger, tokenIssuer }) => {
|
|
2287
|
+
const signInResolver = options.signIn.resolver;
|
|
2288
|
+
const authHandler = options.authHandler;
|
|
2289
|
+
const catalogIdentityClient = new CatalogIdentityClient({
|
|
2290
|
+
catalogApi,
|
|
2291
|
+
tokenIssuer
|
|
2292
|
+
});
|
|
2293
|
+
return new Oauth2ProxyAuthProvider({
|
|
2294
|
+
logger,
|
|
2295
|
+
signInResolver,
|
|
2296
|
+
authHandler,
|
|
2297
|
+
tokenIssuer,
|
|
2298
|
+
catalogIdentityClient
|
|
2299
|
+
});
|
|
2300
|
+
};
|
|
2301
|
+
|
|
2302
|
+
class OidcAuthProvider {
|
|
2303
|
+
constructor(options) {
|
|
2304
|
+
this.implementation = this.setupStrategy(options);
|
|
2305
|
+
this.scope = options.scope;
|
|
2306
|
+
this.prompt = options.prompt;
|
|
2307
|
+
this.signInResolver = options.signInResolver;
|
|
2308
|
+
this.authHandler = options.authHandler;
|
|
2309
|
+
this.tokenIssuer = options.tokenIssuer;
|
|
2310
|
+
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
2311
|
+
this.logger = options.logger;
|
|
2312
|
+
}
|
|
2313
|
+
async start(req) {
|
|
2314
|
+
const { strategy } = await this.implementation;
|
|
2315
|
+
const options = {
|
|
2316
|
+
scope: req.scope || this.scope || "openid profile email",
|
|
2317
|
+
state: encodeState(req.state)
|
|
2318
|
+
};
|
|
2319
|
+
const prompt = this.prompt || "none";
|
|
2320
|
+
if (prompt !== "auto") {
|
|
2321
|
+
options.prompt = prompt;
|
|
2322
|
+
}
|
|
2323
|
+
return await executeRedirectStrategy(req, strategy, options);
|
|
2324
|
+
}
|
|
2325
|
+
async handler(req) {
|
|
2326
|
+
const { strategy } = await this.implementation;
|
|
2327
|
+
const { result, privateInfo } = await executeFrameHandlerStrategy(req, strategy);
|
|
2328
|
+
return {
|
|
2329
|
+
response: await this.handleResult(result),
|
|
2330
|
+
refreshToken: privateInfo.refreshToken
|
|
2331
|
+
};
|
|
2332
|
+
}
|
|
2333
|
+
async refresh(req) {
|
|
2334
|
+
const { client } = await this.implementation;
|
|
2335
|
+
const tokenset = await client.refresh(req.refreshToken);
|
|
2336
|
+
if (!tokenset.access_token) {
|
|
2337
|
+
throw new Error("Refresh failed");
|
|
2338
|
+
}
|
|
2339
|
+
const userinfo = await client.userinfo(tokenset.access_token);
|
|
2340
|
+
return {
|
|
2341
|
+
response: await this.handleResult({ tokenset, userinfo }),
|
|
2342
|
+
refreshToken: tokenset.refresh_token
|
|
2343
|
+
};
|
|
2344
|
+
}
|
|
2345
|
+
async setupStrategy(options) {
|
|
2346
|
+
const issuer = await openidClient.Issuer.discover(options.metadataUrl);
|
|
2347
|
+
const client = new issuer.Client({
|
|
2348
|
+
access_type: "offline",
|
|
2349
|
+
client_id: options.clientId,
|
|
2350
|
+
client_secret: options.clientSecret,
|
|
2351
|
+
redirect_uris: [options.callbackUrl],
|
|
2352
|
+
response_types: ["code"],
|
|
2353
|
+
id_token_signed_response_alg: options.tokenSignedResponseAlg || "RS256",
|
|
2354
|
+
scope: options.scope || ""
|
|
2355
|
+
});
|
|
2356
|
+
const strategy = new openidClient.Strategy({
|
|
2357
|
+
client,
|
|
2358
|
+
passReqToCallback: false
|
|
2359
|
+
}, (tokenset, userinfo, done) => {
|
|
2360
|
+
if (typeof done !== "function") {
|
|
2361
|
+
throw new Error("OIDC IdP must provide a userinfo_endpoint in the metadata response");
|
|
2362
|
+
}
|
|
2363
|
+
done(void 0, { tokenset, userinfo }, {
|
|
2364
|
+
refreshToken: tokenset.refresh_token
|
|
2365
|
+
});
|
|
2366
|
+
});
|
|
2367
|
+
strategy.error = console.error;
|
|
2368
|
+
return { strategy, client };
|
|
2369
|
+
}
|
|
2370
|
+
async handleResult(result) {
|
|
2371
|
+
const context = {
|
|
2372
|
+
logger: this.logger,
|
|
2373
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
2374
|
+
tokenIssuer: this.tokenIssuer
|
|
2375
|
+
};
|
|
2376
|
+
const { profile } = await this.authHandler(result, context);
|
|
2377
|
+
const response = {
|
|
2378
|
+
providerInfo: {
|
|
2379
|
+
idToken: result.tokenset.id_token,
|
|
2380
|
+
accessToken: result.tokenset.access_token,
|
|
2381
|
+
scope: result.tokenset.scope,
|
|
2382
|
+
expiresInSeconds: result.tokenset.expires_in
|
|
2383
|
+
},
|
|
2384
|
+
profile
|
|
2385
|
+
};
|
|
2386
|
+
if (this.signInResolver) {
|
|
2387
|
+
response.backstageIdentity = await this.signInResolver({
|
|
2388
|
+
result,
|
|
2389
|
+
profile
|
|
2390
|
+
}, context);
|
|
2391
|
+
}
|
|
2392
|
+
return response;
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
const oAuth2DefaultSignInResolver = async (info, ctx) => {
|
|
2396
|
+
const { profile } = info;
|
|
2397
|
+
if (!profile.email) {
|
|
2398
|
+
throw new Error("Profile contained no email");
|
|
2399
|
+
}
|
|
2400
|
+
const userId = profile.email.split("@")[0];
|
|
2401
|
+
const token = await ctx.tokenIssuer.issueToken({
|
|
2402
|
+
claims: {
|
|
2403
|
+
sub: `user:default/${userId}`,
|
|
2404
|
+
ent: [`user:default/${userId}`]
|
|
2405
|
+
}
|
|
2406
|
+
});
|
|
2407
|
+
return { id: userId, token };
|
|
2408
|
+
};
|
|
2409
|
+
const createOidcProvider = (options) => {
|
|
1962
2410
|
return ({
|
|
1963
2411
|
providerId,
|
|
1964
2412
|
globalConfig,
|
|
@@ -2076,7 +2524,12 @@ class OktaAuthProvider {
|
|
|
2076
2524
|
};
|
|
2077
2525
|
}
|
|
2078
2526
|
async handleResult(result) {
|
|
2079
|
-
const
|
|
2527
|
+
const context = {
|
|
2528
|
+
logger: this._logger,
|
|
2529
|
+
catalogIdentityClient: this._catalogIdentityClient,
|
|
2530
|
+
tokenIssuer: this._tokenIssuer
|
|
2531
|
+
};
|
|
2532
|
+
const { profile } = await this._authHandler(result, context);
|
|
2080
2533
|
const response = {
|
|
2081
2534
|
providerInfo: {
|
|
2082
2535
|
idToken: result.params.id_token,
|
|
@@ -2090,11 +2543,7 @@ class OktaAuthProvider {
|
|
|
2090
2543
|
response.backstageIdentity = await this._signInResolver({
|
|
2091
2544
|
result,
|
|
2092
2545
|
profile
|
|
2093
|
-
},
|
|
2094
|
-
tokenIssuer: this._tokenIssuer,
|
|
2095
|
-
catalogIdentityClient: this._catalogIdentityClient,
|
|
2096
|
-
logger: this._logger
|
|
2097
|
-
});
|
|
2546
|
+
}, context);
|
|
2098
2547
|
}
|
|
2099
2548
|
return response;
|
|
2100
2549
|
}
|
|
@@ -2117,245 +2566,14 @@ const oktaDefaultSignInResolver = async (info, ctx) => {
|
|
|
2117
2566
|
const { profile } = info;
|
|
2118
2567
|
if (!profile.email) {
|
|
2119
2568
|
throw new Error("Okta profile contained no email");
|
|
2120
|
-
}
|
|
2121
|
-
const userId = profile.email.split("@")[0];
|
|
2122
|
-
const token = await ctx.tokenIssuer.issueToken({
|
|
2123
|
-
claims: { sub: userId, ent: [`user:default/${userId}`] }
|
|
2124
|
-
});
|
|
2125
|
-
return { id: userId, token };
|
|
2126
|
-
};
|
|
2127
|
-
const createOktaProvider = (_options) => {
|
|
2128
|
-
return ({
|
|
2129
|
-
providerId,
|
|
2130
|
-
globalConfig,
|
|
2131
|
-
config,
|
|
2132
|
-
tokenIssuer,
|
|
2133
|
-
catalogApi,
|
|
2134
|
-
logger
|
|
2135
|
-
}) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
|
|
2136
|
-
var _a, _b;
|
|
2137
|
-
const clientId = envConfig.getString("clientId");
|
|
2138
|
-
const clientSecret = envConfig.getString("clientSecret");
|
|
2139
|
-
const audience = envConfig.getString("audience");
|
|
2140
|
-
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
2141
|
-
if (!audience.startsWith("https://")) {
|
|
2142
|
-
throw new Error("URL for 'audience' must start with 'https://'.");
|
|
2143
|
-
}
|
|
2144
|
-
const catalogIdentityClient = new CatalogIdentityClient({
|
|
2145
|
-
catalogApi,
|
|
2146
|
-
tokenIssuer
|
|
2147
|
-
});
|
|
2148
|
-
const authHandler = (_options == null ? void 0 : _options.authHandler) ? _options.authHandler : async ({ fullProfile, params }) => ({
|
|
2149
|
-
profile: makeProfileInfo(fullProfile, params.id_token)
|
|
2150
|
-
});
|
|
2151
|
-
const signInResolverFn = (_b = (_a = _options == null ? void 0 : _options.signIn) == null ? void 0 : _a.resolver) != null ? _b : oktaDefaultSignInResolver;
|
|
2152
|
-
const signInResolver = (info) => signInResolverFn(info, {
|
|
2153
|
-
catalogIdentityClient,
|
|
2154
|
-
tokenIssuer,
|
|
2155
|
-
logger
|
|
2156
|
-
});
|
|
2157
|
-
const provider = new OktaAuthProvider({
|
|
2158
|
-
audience,
|
|
2159
|
-
clientId,
|
|
2160
|
-
clientSecret,
|
|
2161
|
-
callbackUrl,
|
|
2162
|
-
authHandler,
|
|
2163
|
-
signInResolver,
|
|
2164
|
-
tokenIssuer,
|
|
2165
|
-
catalogIdentityClient,
|
|
2166
|
-
logger
|
|
2167
|
-
});
|
|
2168
|
-
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
2169
|
-
disableRefresh: false,
|
|
2170
|
-
providerId,
|
|
2171
|
-
tokenIssuer
|
|
2172
|
-
});
|
|
2173
|
-
});
|
|
2174
|
-
};
|
|
2175
|
-
|
|
2176
|
-
class OneLoginProvider {
|
|
2177
|
-
constructor(options) {
|
|
2178
|
-
this.signInResolver = options.signInResolver;
|
|
2179
|
-
this.authHandler = options.authHandler;
|
|
2180
|
-
this.tokenIssuer = options.tokenIssuer;
|
|
2181
|
-
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
2182
|
-
this.logger = options.logger;
|
|
2183
|
-
this._strategy = new passportOneloginOauth.Strategy({
|
|
2184
|
-
issuer: options.issuer,
|
|
2185
|
-
clientID: options.clientId,
|
|
2186
|
-
clientSecret: options.clientSecret,
|
|
2187
|
-
callbackURL: options.callbackUrl,
|
|
2188
|
-
passReqToCallback: false
|
|
2189
|
-
}, (accessToken, refreshToken, params, fullProfile, done) => {
|
|
2190
|
-
done(void 0, {
|
|
2191
|
-
accessToken,
|
|
2192
|
-
refreshToken,
|
|
2193
|
-
params,
|
|
2194
|
-
fullProfile
|
|
2195
|
-
}, {
|
|
2196
|
-
refreshToken
|
|
2197
|
-
});
|
|
2198
|
-
});
|
|
2199
|
-
}
|
|
2200
|
-
async start(req) {
|
|
2201
|
-
return await executeRedirectStrategy(req, this._strategy, {
|
|
2202
|
-
accessType: "offline",
|
|
2203
|
-
prompt: "consent",
|
|
2204
|
-
scope: "openid",
|
|
2205
|
-
state: encodeState(req.state)
|
|
2206
|
-
});
|
|
2207
|
-
}
|
|
2208
|
-
async handler(req) {
|
|
2209
|
-
const { result, privateInfo } = await executeFrameHandlerStrategy(req, this._strategy);
|
|
2210
|
-
return {
|
|
2211
|
-
response: await this.handleResult(result),
|
|
2212
|
-
refreshToken: privateInfo.refreshToken
|
|
2213
|
-
};
|
|
2214
|
-
}
|
|
2215
|
-
async refresh(req) {
|
|
2216
|
-
const { accessToken, refreshToken, params } = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
|
|
2217
|
-
const fullProfile = await executeFetchUserProfileStrategy(this._strategy, accessToken);
|
|
2218
|
-
return {
|
|
2219
|
-
response: await this.handleResult({
|
|
2220
|
-
fullProfile,
|
|
2221
|
-
params,
|
|
2222
|
-
accessToken
|
|
2223
|
-
}),
|
|
2224
|
-
refreshToken
|
|
2225
|
-
};
|
|
2226
|
-
}
|
|
2227
|
-
async handleResult(result) {
|
|
2228
|
-
const { profile } = await this.authHandler(result);
|
|
2229
|
-
const response = {
|
|
2230
|
-
providerInfo: {
|
|
2231
|
-
idToken: result.params.id_token,
|
|
2232
|
-
accessToken: result.accessToken,
|
|
2233
|
-
scope: result.params.scope,
|
|
2234
|
-
expiresInSeconds: result.params.expires_in
|
|
2235
|
-
},
|
|
2236
|
-
profile
|
|
2237
|
-
};
|
|
2238
|
-
if (this.signInResolver) {
|
|
2239
|
-
response.backstageIdentity = await this.signInResolver({
|
|
2240
|
-
result,
|
|
2241
|
-
profile
|
|
2242
|
-
}, {
|
|
2243
|
-
tokenIssuer: this.tokenIssuer,
|
|
2244
|
-
catalogIdentityClient: this.catalogIdentityClient,
|
|
2245
|
-
logger: this.logger
|
|
2246
|
-
});
|
|
2247
|
-
}
|
|
2248
|
-
return response;
|
|
2249
|
-
}
|
|
2250
|
-
}
|
|
2251
|
-
const defaultSignInResolver = async (info) => {
|
|
2252
|
-
const { profile } = info;
|
|
2253
|
-
if (!profile.email) {
|
|
2254
|
-
throw new Error("OIDC profile contained no email");
|
|
2255
|
-
}
|
|
2256
|
-
const id = profile.email.split("@")[0];
|
|
2257
|
-
return { id, token: "" };
|
|
2258
|
-
};
|
|
2259
|
-
const createOneLoginProvider = (options) => {
|
|
2260
|
-
return ({
|
|
2261
|
-
providerId,
|
|
2262
|
-
globalConfig,
|
|
2263
|
-
config,
|
|
2264
|
-
tokenIssuer,
|
|
2265
|
-
catalogApi,
|
|
2266
|
-
logger
|
|
2267
|
-
}) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
|
|
2268
|
-
var _a, _b;
|
|
2269
|
-
const clientId = envConfig.getString("clientId");
|
|
2270
|
-
const clientSecret = envConfig.getString("clientSecret");
|
|
2271
|
-
const issuer = envConfig.getString("issuer");
|
|
2272
|
-
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
2273
|
-
const catalogIdentityClient = new CatalogIdentityClient({
|
|
2274
|
-
catalogApi,
|
|
2275
|
-
tokenIssuer
|
|
2276
|
-
});
|
|
2277
|
-
const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ fullProfile, params }) => ({
|
|
2278
|
-
profile: makeProfileInfo(fullProfile, params.id_token)
|
|
2279
|
-
});
|
|
2280
|
-
const signInResolver = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : defaultSignInResolver;
|
|
2281
|
-
const provider = new OneLoginProvider({
|
|
2282
|
-
clientId,
|
|
2283
|
-
clientSecret,
|
|
2284
|
-
callbackUrl,
|
|
2285
|
-
issuer,
|
|
2286
|
-
authHandler,
|
|
2287
|
-
signInResolver,
|
|
2288
|
-
tokenIssuer,
|
|
2289
|
-
catalogIdentityClient,
|
|
2290
|
-
logger
|
|
2291
|
-
});
|
|
2292
|
-
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
2293
|
-
disableRefresh: false,
|
|
2294
|
-
providerId,
|
|
2295
|
-
tokenIssuer
|
|
2296
|
-
});
|
|
2297
|
-
});
|
|
2298
|
-
};
|
|
2299
|
-
|
|
2300
|
-
class SamlAuthProvider {
|
|
2301
|
-
constructor(options) {
|
|
2302
|
-
this.appUrl = options.appUrl;
|
|
2303
|
-
this.signInResolver = options.signInResolver;
|
|
2304
|
-
this.authHandler = options.authHandler;
|
|
2305
|
-
this.tokenIssuer = options.tokenIssuer;
|
|
2306
|
-
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
2307
|
-
this.logger = options.logger;
|
|
2308
|
-
this.strategy = new passportSaml.Strategy({ ...options }, (fullProfile, done) => {
|
|
2309
|
-
done(void 0, { fullProfile });
|
|
2310
|
-
});
|
|
2311
|
-
}
|
|
2312
|
-
async start(req, res) {
|
|
2313
|
-
const { url } = await executeRedirectStrategy(req, this.strategy, {});
|
|
2314
|
-
res.redirect(url);
|
|
2315
|
-
}
|
|
2316
|
-
async frameHandler(req, res) {
|
|
2317
|
-
try {
|
|
2318
|
-
const { result } = await executeFrameHandlerStrategy(req, this.strategy);
|
|
2319
|
-
const { profile } = await this.authHandler(result);
|
|
2320
|
-
const response = {
|
|
2321
|
-
profile,
|
|
2322
|
-
providerInfo: {}
|
|
2323
|
-
};
|
|
2324
|
-
if (this.signInResolver) {
|
|
2325
|
-
const signInResponse = await this.signInResolver({
|
|
2326
|
-
result,
|
|
2327
|
-
profile
|
|
2328
|
-
}, {
|
|
2329
|
-
tokenIssuer: this.tokenIssuer,
|
|
2330
|
-
catalogIdentityClient: this.catalogIdentityClient,
|
|
2331
|
-
logger: this.logger
|
|
2332
|
-
});
|
|
2333
|
-
response.backstageIdentity = prepareBackstageIdentityResponse(signInResponse);
|
|
2334
|
-
}
|
|
2335
|
-
return postMessageResponse(res, this.appUrl, {
|
|
2336
|
-
type: "authorization_response",
|
|
2337
|
-
response
|
|
2338
|
-
});
|
|
2339
|
-
} catch (error) {
|
|
2340
|
-
const { name, message } = errors.isError(error) ? error : new Error("Encountered invalid error");
|
|
2341
|
-
return postMessageResponse(res, this.appUrl, {
|
|
2342
|
-
type: "authorization_response",
|
|
2343
|
-
error: { name, message }
|
|
2344
|
-
});
|
|
2345
|
-
}
|
|
2346
|
-
}
|
|
2347
|
-
async logout(_req, res) {
|
|
2348
|
-
res.end();
|
|
2349
|
-
}
|
|
2350
|
-
}
|
|
2351
|
-
const samlDefaultSignInResolver = async (info, ctx) => {
|
|
2352
|
-
const id = info.result.fullProfile.nameID;
|
|
2569
|
+
}
|
|
2570
|
+
const userId = profile.email.split("@")[0];
|
|
2353
2571
|
const token = await ctx.tokenIssuer.issueToken({
|
|
2354
|
-
claims: { sub:
|
|
2572
|
+
claims: { sub: `user:default/${userId}`, ent: [`user:default/${userId}`] }
|
|
2355
2573
|
});
|
|
2356
|
-
return { id, token };
|
|
2574
|
+
return { id: userId, token };
|
|
2357
2575
|
};
|
|
2358
|
-
const
|
|
2576
|
+
const createOktaProvider = (_options) => {
|
|
2359
2577
|
return ({
|
|
2360
2578
|
providerId,
|
|
2361
2579
|
globalConfig,
|
|
@@ -2363,494 +2581,388 @@ const createSamlProvider = (options) => {
|
|
|
2363
2581
|
tokenIssuer,
|
|
2364
2582
|
catalogApi,
|
|
2365
2583
|
logger
|
|
2366
|
-
}) => {
|
|
2584
|
+
}) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
|
|
2367
2585
|
var _a, _b;
|
|
2586
|
+
const clientId = envConfig.getString("clientId");
|
|
2587
|
+
const clientSecret = envConfig.getString("clientSecret");
|
|
2588
|
+
const audience = envConfig.getString("audience");
|
|
2589
|
+
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
2590
|
+
if (!audience.startsWith("https://")) {
|
|
2591
|
+
throw new Error("URL for 'audience' must start with 'https://'.");
|
|
2592
|
+
}
|
|
2368
2593
|
const catalogIdentityClient = new CatalogIdentityClient({
|
|
2369
2594
|
catalogApi,
|
|
2370
2595
|
tokenIssuer
|
|
2371
2596
|
});
|
|
2372
|
-
const authHandler = (
|
|
2373
|
-
profile:
|
|
2374
|
-
email: fullProfile.email,
|
|
2375
|
-
displayName: fullProfile.displayName
|
|
2376
|
-
}
|
|
2597
|
+
const authHandler = (_options == null ? void 0 : _options.authHandler) ? _options.authHandler : async ({ fullProfile, params }) => ({
|
|
2598
|
+
profile: makeProfileInfo(fullProfile, params.id_token)
|
|
2377
2599
|
});
|
|
2378
|
-
const signInResolverFn = (_b = (_a =
|
|
2600
|
+
const signInResolverFn = (_b = (_a = _options == null ? void 0 : _options.signIn) == null ? void 0 : _a.resolver) != null ? _b : oktaDefaultSignInResolver;
|
|
2379
2601
|
const signInResolver = (info) => signInResolverFn(info, {
|
|
2380
2602
|
catalogIdentityClient,
|
|
2381
2603
|
tokenIssuer,
|
|
2382
2604
|
logger
|
|
2383
2605
|
});
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
decryptionPvk: config.getOptionalString("decryptionPvk"),
|
|
2395
|
-
signatureAlgorithm: config.getOptionalString("signatureAlgorithm"),
|
|
2396
|
-
digestAlgorithm: config.getOptionalString("digestAlgorithm"),
|
|
2397
|
-
acceptedClockSkewMs: config.getOptionalNumber("acceptedClockSkewMs"),
|
|
2398
|
-
tokenIssuer,
|
|
2399
|
-
appUrl: globalConfig.appUrl,
|
|
2400
|
-
authHandler,
|
|
2401
|
-
signInResolver,
|
|
2402
|
-
logger,
|
|
2403
|
-
catalogIdentityClient
|
|
2404
|
-
});
|
|
2405
|
-
};
|
|
2406
|
-
};
|
|
2407
|
-
|
|
2408
|
-
const IAP_JWT_HEADER = "x-goog-iap-jwt-assertion";
|
|
2409
|
-
|
|
2410
|
-
function createTokenValidator(audience, mockClient) {
|
|
2411
|
-
const client = mockClient != null ? mockClient : new googleAuthLibrary.OAuth2Client();
|
|
2412
|
-
return async function tokenValidator(token) {
|
|
2413
|
-
const response = await client.getIapPublicKeys();
|
|
2414
|
-
const ticket = await client.verifySignedJwtWithCertsAsync(token, response.pubkeys, audience, ["https://cloud.google.com/iap"]);
|
|
2415
|
-
const payload = ticket.getPayload();
|
|
2416
|
-
if (!payload) {
|
|
2417
|
-
throw new TypeError("Token had no payload");
|
|
2418
|
-
}
|
|
2419
|
-
return payload;
|
|
2420
|
-
};
|
|
2421
|
-
}
|
|
2422
|
-
async function parseRequestToken(jwtToken, tokenValidator) {
|
|
2423
|
-
if (typeof jwtToken !== "string" || !jwtToken) {
|
|
2424
|
-
throw new errors.AuthenticationError(`Missing Google IAP header: ${IAP_JWT_HEADER}`);
|
|
2425
|
-
}
|
|
2426
|
-
let payload;
|
|
2427
|
-
try {
|
|
2428
|
-
payload = await tokenValidator(jwtToken);
|
|
2429
|
-
} catch (e) {
|
|
2430
|
-
throw new errors.AuthenticationError(`Google IAP token verification failed, ${e}`);
|
|
2431
|
-
}
|
|
2432
|
-
if (!payload.sub || !payload.email) {
|
|
2433
|
-
throw new errors.AuthenticationError("Google IAP token payload is missing sub and/or email claim");
|
|
2434
|
-
}
|
|
2435
|
-
return {
|
|
2436
|
-
iapToken: {
|
|
2437
|
-
...payload,
|
|
2438
|
-
sub: payload.sub,
|
|
2439
|
-
email: payload.email
|
|
2440
|
-
}
|
|
2441
|
-
};
|
|
2442
|
-
}
|
|
2443
|
-
const defaultAuthHandler = async ({
|
|
2444
|
-
iapToken
|
|
2445
|
-
}) => ({ profile: { email: iapToken.email } });
|
|
2446
|
-
|
|
2447
|
-
class GcpIapProvider {
|
|
2448
|
-
constructor(options) {
|
|
2449
|
-
this.authHandler = options.authHandler;
|
|
2450
|
-
this.signInResolver = options.signInResolver;
|
|
2451
|
-
this.tokenValidator = options.tokenValidator;
|
|
2452
|
-
this.tokenIssuer = options.tokenIssuer;
|
|
2453
|
-
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
2454
|
-
this.logger = options.logger;
|
|
2455
|
-
}
|
|
2456
|
-
async start() {
|
|
2457
|
-
}
|
|
2458
|
-
async frameHandler() {
|
|
2459
|
-
}
|
|
2460
|
-
async refresh(req, res) {
|
|
2461
|
-
const result = await parseRequestToken(req.header(IAP_JWT_HEADER), this.tokenValidator);
|
|
2462
|
-
const { profile } = await this.authHandler(result);
|
|
2463
|
-
const backstageIdentity = await this.signInResolver({ profile, result }, {
|
|
2464
|
-
tokenIssuer: this.tokenIssuer,
|
|
2465
|
-
catalogIdentityClient: this.catalogIdentityClient,
|
|
2466
|
-
logger: this.logger
|
|
2467
|
-
});
|
|
2468
|
-
const response = {
|
|
2469
|
-
providerInfo: { iapToken: result.iapToken },
|
|
2470
|
-
profile,
|
|
2471
|
-
backstageIdentity: prepareBackstageIdentityResponse(backstageIdentity)
|
|
2472
|
-
};
|
|
2473
|
-
res.json(response);
|
|
2474
|
-
}
|
|
2475
|
-
}
|
|
2476
|
-
function createGcpIapProvider(options) {
|
|
2477
|
-
return ({ config, tokenIssuer, catalogApi, logger }) => {
|
|
2478
|
-
var _a;
|
|
2479
|
-
const audience = config.getString("audience");
|
|
2480
|
-
const authHandler = (_a = options.authHandler) != null ? _a : defaultAuthHandler;
|
|
2481
|
-
const signInResolver = options.signIn.resolver;
|
|
2482
|
-
const tokenValidator = createTokenValidator(audience);
|
|
2483
|
-
const catalogIdentityClient = new CatalogIdentityClient({
|
|
2484
|
-
catalogApi,
|
|
2485
|
-
tokenIssuer
|
|
2486
|
-
});
|
|
2487
|
-
return new GcpIapProvider({
|
|
2488
|
-
authHandler,
|
|
2489
|
-
signInResolver,
|
|
2490
|
-
tokenValidator,
|
|
2491
|
-
tokenIssuer,
|
|
2492
|
-
catalogIdentityClient,
|
|
2493
|
-
logger
|
|
2494
|
-
});
|
|
2495
|
-
};
|
|
2496
|
-
}
|
|
2497
|
-
|
|
2498
|
-
const factories = {
|
|
2499
|
-
google: createGoogleProvider(),
|
|
2500
|
-
github: createGithubProvider(),
|
|
2501
|
-
gitlab: createGitlabProvider(),
|
|
2502
|
-
saml: createSamlProvider(),
|
|
2503
|
-
okta: createOktaProvider(),
|
|
2504
|
-
auth0: createAuth0Provider(),
|
|
2505
|
-
microsoft: createMicrosoftProvider(),
|
|
2506
|
-
oauth2: createOAuth2Provider(),
|
|
2507
|
-
oidc: createOidcProvider(),
|
|
2508
|
-
onelogin: createOneLoginProvider(),
|
|
2509
|
-
awsalb: createAwsAlbProvider(),
|
|
2510
|
-
bitbucket: createBitbucketProvider(),
|
|
2511
|
-
atlassian: createAtlassianProvider()
|
|
2512
|
-
};
|
|
2513
|
-
|
|
2514
|
-
function createOidcRouter(options) {
|
|
2515
|
-
const { baseUrl, tokenIssuer } = options;
|
|
2516
|
-
const router = Router__default["default"]();
|
|
2517
|
-
const config = {
|
|
2518
|
-
issuer: baseUrl,
|
|
2519
|
-
token_endpoint: `${baseUrl}/v1/token`,
|
|
2520
|
-
userinfo_endpoint: `${baseUrl}/v1/userinfo`,
|
|
2521
|
-
jwks_uri: `${baseUrl}/.well-known/jwks.json`,
|
|
2522
|
-
response_types_supported: ["id_token"],
|
|
2523
|
-
subject_types_supported: ["public"],
|
|
2524
|
-
id_token_signing_alg_values_supported: ["RS256"],
|
|
2525
|
-
scopes_supported: ["openid"],
|
|
2526
|
-
token_endpoint_auth_methods_supported: [],
|
|
2527
|
-
claims_supported: ["sub"],
|
|
2528
|
-
grant_types_supported: []
|
|
2529
|
-
};
|
|
2530
|
-
router.get("/.well-known/openid-configuration", (_req, res) => {
|
|
2531
|
-
res.json(config);
|
|
2532
|
-
});
|
|
2533
|
-
router.get("/.well-known/jwks.json", async (_req, res) => {
|
|
2534
|
-
const { keys } = await tokenIssuer.listPublicKeys();
|
|
2535
|
-
res.json({ keys });
|
|
2536
|
-
});
|
|
2537
|
-
router.get("/v1/token", (_req, res) => {
|
|
2538
|
-
res.status(501).send("Not Implemented");
|
|
2539
|
-
});
|
|
2540
|
-
router.get("/v1/userinfo", (_req, res) => {
|
|
2541
|
-
res.status(501).send("Not Implemented");
|
|
2542
|
-
});
|
|
2543
|
-
return router;
|
|
2544
|
-
}
|
|
2545
|
-
|
|
2546
|
-
const CLOCK_MARGIN_S = 10;
|
|
2547
|
-
class IdentityClient {
|
|
2548
|
-
constructor(options) {
|
|
2549
|
-
this.discovery = options.discovery;
|
|
2550
|
-
this.issuer = options.issuer;
|
|
2551
|
-
this.keyStore = new jose.JWKS.KeyStore();
|
|
2552
|
-
this.keyStoreUpdated = 0;
|
|
2553
|
-
}
|
|
2554
|
-
async authenticate(token) {
|
|
2555
|
-
var _a;
|
|
2556
|
-
if (!token) {
|
|
2557
|
-
throw new errors.AuthenticationError("No token specified");
|
|
2558
|
-
}
|
|
2559
|
-
const key = await this.getKey(token);
|
|
2560
|
-
if (!key) {
|
|
2561
|
-
throw new errors.AuthenticationError("No signing key matching token found");
|
|
2562
|
-
}
|
|
2563
|
-
const decoded = jose.JWT.IdToken.verify(token, key, {
|
|
2564
|
-
algorithms: ["ES256"],
|
|
2565
|
-
audience: "backstage",
|
|
2566
|
-
issuer: this.issuer
|
|
2567
|
-
});
|
|
2568
|
-
if (!decoded.sub) {
|
|
2569
|
-
throw new errors.AuthenticationError("No user sub found in token");
|
|
2570
|
-
}
|
|
2571
|
-
const user = {
|
|
2572
|
-
id: decoded.sub,
|
|
2573
|
-
token,
|
|
2574
|
-
identity: {
|
|
2575
|
-
type: "user",
|
|
2576
|
-
userEntityRef: decoded.sub,
|
|
2577
|
-
ownershipEntityRefs: (_a = decoded.ent) != null ? _a : []
|
|
2578
|
-
}
|
|
2579
|
-
};
|
|
2580
|
-
return user;
|
|
2581
|
-
}
|
|
2582
|
-
static getBearerToken(authorizationHeader) {
|
|
2583
|
-
if (typeof authorizationHeader !== "string") {
|
|
2584
|
-
return void 0;
|
|
2585
|
-
}
|
|
2586
|
-
const matches = authorizationHeader.match(/Bearer\s+(\S+)/i);
|
|
2587
|
-
return matches == null ? void 0 : matches[1];
|
|
2588
|
-
}
|
|
2589
|
-
async getKey(rawJwtToken) {
|
|
2590
|
-
const { header, payload } = jose.JWT.decode(rawJwtToken, {
|
|
2591
|
-
complete: true
|
|
2592
|
-
});
|
|
2593
|
-
const keyStoreHasKey = !!this.keyStore.get({ kid: header.kid });
|
|
2594
|
-
const issuedAfterLastRefresh = (payload == null ? void 0 : payload.iat) && payload.iat > this.keyStoreUpdated - CLOCK_MARGIN_S;
|
|
2595
|
-
if (!keyStoreHasKey && issuedAfterLastRefresh) {
|
|
2596
|
-
await this.refreshKeyStore();
|
|
2597
|
-
}
|
|
2598
|
-
return this.keyStore.get({ kid: header.kid });
|
|
2599
|
-
}
|
|
2600
|
-
async listPublicKeys() {
|
|
2601
|
-
const url = `${await this.discovery.getBaseUrl("auth")}/.well-known/jwks.json`;
|
|
2602
|
-
const response = await fetch__default["default"](url);
|
|
2603
|
-
if (!response.ok) {
|
|
2604
|
-
const payload = await response.text();
|
|
2605
|
-
const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
|
|
2606
|
-
throw new Error(message);
|
|
2607
|
-
}
|
|
2608
|
-
const publicKeys = await response.json();
|
|
2609
|
-
return publicKeys;
|
|
2610
|
-
}
|
|
2611
|
-
async refreshKeyStore() {
|
|
2612
|
-
const now = Date.now() / 1e3;
|
|
2613
|
-
const publicKeys = await this.listPublicKeys();
|
|
2614
|
-
this.keyStore = jose.JWKS.asKeyStore({
|
|
2615
|
-
keys: publicKeys.keys.map((key) => key)
|
|
2606
|
+
const provider = new OktaAuthProvider({
|
|
2607
|
+
audience,
|
|
2608
|
+
clientId,
|
|
2609
|
+
clientSecret,
|
|
2610
|
+
callbackUrl,
|
|
2611
|
+
authHandler,
|
|
2612
|
+
signInResolver,
|
|
2613
|
+
tokenIssuer,
|
|
2614
|
+
catalogIdentityClient,
|
|
2615
|
+
logger
|
|
2616
2616
|
});
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2617
|
+
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
2618
|
+
disableRefresh: false,
|
|
2619
|
+
providerId,
|
|
2620
|
+
tokenIssuer
|
|
2621
|
+
});
|
|
2622
|
+
});
|
|
2623
|
+
};
|
|
2620
2624
|
|
|
2621
|
-
|
|
2622
|
-
class TokenFactory {
|
|
2625
|
+
class OneLoginProvider {
|
|
2623
2626
|
constructor(options) {
|
|
2624
|
-
this.
|
|
2627
|
+
this.signInResolver = options.signInResolver;
|
|
2628
|
+
this.authHandler = options.authHandler;
|
|
2629
|
+
this.tokenIssuer = options.tokenIssuer;
|
|
2630
|
+
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
2625
2631
|
this.logger = options.logger;
|
|
2626
|
-
this.
|
|
2627
|
-
|
|
2632
|
+
this._strategy = new passportOneloginOauth.Strategy({
|
|
2633
|
+
issuer: options.issuer,
|
|
2634
|
+
clientID: options.clientId,
|
|
2635
|
+
clientSecret: options.clientSecret,
|
|
2636
|
+
callbackURL: options.callbackUrl,
|
|
2637
|
+
passReqToCallback: false
|
|
2638
|
+
}, (accessToken, refreshToken, params, fullProfile, done) => {
|
|
2639
|
+
done(void 0, {
|
|
2640
|
+
accessToken,
|
|
2641
|
+
refreshToken,
|
|
2642
|
+
params,
|
|
2643
|
+
fullProfile
|
|
2644
|
+
}, {
|
|
2645
|
+
refreshToken
|
|
2646
|
+
});
|
|
2647
|
+
});
|
|
2628
2648
|
}
|
|
2629
|
-
async
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
const iat = Math.floor(Date.now() / MS_IN_S);
|
|
2636
|
-
const exp = iat + this.keyDurationSeconds;
|
|
2637
|
-
this.logger.info(`Issuing token for ${sub}, with entities ${ent != null ? ent : []}`);
|
|
2638
|
-
return jose.JWS.sign({ iss, sub, aud, iat, exp, ent }, key, {
|
|
2639
|
-
alg: key.alg,
|
|
2640
|
-
kid: key.kid
|
|
2649
|
+
async start(req) {
|
|
2650
|
+
return await executeRedirectStrategy(req, this._strategy, {
|
|
2651
|
+
accessType: "offline",
|
|
2652
|
+
prompt: "consent",
|
|
2653
|
+
scope: "openid",
|
|
2654
|
+
state: encodeState(req.state)
|
|
2641
2655
|
});
|
|
2642
2656
|
}
|
|
2643
|
-
async
|
|
2644
|
-
const {
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
seconds: 3 * this.keyDurationSeconds
|
|
2650
|
-
});
|
|
2651
|
-
if (expireAt < luxon.DateTime.local()) {
|
|
2652
|
-
expiredKeys.push(key);
|
|
2653
|
-
} else {
|
|
2654
|
-
validKeys.push(key);
|
|
2655
|
-
}
|
|
2656
|
-
}
|
|
2657
|
-
if (expiredKeys.length > 0) {
|
|
2658
|
-
const kids = expiredKeys.map(({ key }) => key.kid);
|
|
2659
|
-
this.logger.info(`Removing expired signing keys, '${kids.join("', '")}'`);
|
|
2660
|
-
this.keyStore.removeKeys(kids).catch((error) => {
|
|
2661
|
-
this.logger.error(`Failed to remove expired keys, ${error}`);
|
|
2662
|
-
});
|
|
2663
|
-
}
|
|
2664
|
-
return { keys: validKeys.map(({ key }) => key) };
|
|
2657
|
+
async handler(req) {
|
|
2658
|
+
const { result, privateInfo } = await executeFrameHandlerStrategy(req, this._strategy);
|
|
2659
|
+
return {
|
|
2660
|
+
response: await this.handleResult(result),
|
|
2661
|
+
refreshToken: privateInfo.refreshToken
|
|
2662
|
+
};
|
|
2665
2663
|
}
|
|
2666
|
-
async
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
}
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2664
|
+
async refresh(req) {
|
|
2665
|
+
const { accessToken, refreshToken, params } = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
|
|
2666
|
+
const fullProfile = await executeFetchUserProfileStrategy(this._strategy, accessToken);
|
|
2667
|
+
return {
|
|
2668
|
+
response: await this.handleResult({
|
|
2669
|
+
fullProfile,
|
|
2670
|
+
params,
|
|
2671
|
+
accessToken
|
|
2672
|
+
}),
|
|
2673
|
+
refreshToken
|
|
2674
|
+
};
|
|
2675
|
+
}
|
|
2676
|
+
async handleResult(result) {
|
|
2677
|
+
const context = {
|
|
2678
|
+
logger: this.logger,
|
|
2679
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
2680
|
+
tokenIssuer: this.tokenIssuer
|
|
2681
|
+
};
|
|
2682
|
+
const { profile } = await this.authHandler(result, context);
|
|
2683
|
+
const response = {
|
|
2684
|
+
providerInfo: {
|
|
2685
|
+
idToken: result.params.id_token,
|
|
2686
|
+
accessToken: result.accessToken,
|
|
2687
|
+
scope: result.params.scope,
|
|
2688
|
+
expiresInSeconds: result.params.expires_in
|
|
2689
|
+
},
|
|
2690
|
+
profile
|
|
2691
|
+
};
|
|
2692
|
+
if (this.signInResolver) {
|
|
2693
|
+
response.backstageIdentity = await this.signInResolver({
|
|
2694
|
+
result,
|
|
2695
|
+
profile
|
|
2696
|
+
}, context);
|
|
2694
2697
|
}
|
|
2695
|
-
return
|
|
2698
|
+
return response;
|
|
2696
2699
|
}
|
|
2697
2700
|
}
|
|
2698
|
-
|
|
2699
|
-
const
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
const parsedDate = typeof date === "string" ? luxon.DateTime.fromSQL(date, { zone: "UTC" }) : luxon.DateTime.fromJSDate(date);
|
|
2703
|
-
if (!parsedDate.isValid) {
|
|
2704
|
-
throw new Error(`Failed to parse date, reason: ${parsedDate.invalidReason}, explanation: ${parsedDate.invalidExplanation}`);
|
|
2701
|
+
const defaultSignInResolver = async (info) => {
|
|
2702
|
+
const { profile } = info;
|
|
2703
|
+
if (!profile.email) {
|
|
2704
|
+
throw new Error("OIDC profile contained no email");
|
|
2705
2705
|
}
|
|
2706
|
-
|
|
2706
|
+
const id = profile.email.split("@")[0];
|
|
2707
|
+
return { id, token: "" };
|
|
2707
2708
|
};
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2709
|
+
const createOneLoginProvider = (options) => {
|
|
2710
|
+
return ({
|
|
2711
|
+
providerId,
|
|
2712
|
+
globalConfig,
|
|
2713
|
+
config,
|
|
2714
|
+
tokenIssuer,
|
|
2715
|
+
catalogApi,
|
|
2716
|
+
logger
|
|
2717
|
+
}) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
|
|
2718
|
+
var _a, _b;
|
|
2719
|
+
const clientId = envConfig.getString("clientId");
|
|
2720
|
+
const clientSecret = envConfig.getString("clientSecret");
|
|
2721
|
+
const issuer = envConfig.getString("issuer");
|
|
2722
|
+
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
2723
|
+
const catalogIdentityClient = new CatalogIdentityClient({
|
|
2724
|
+
catalogApi,
|
|
2725
|
+
tokenIssuer
|
|
2713
2726
|
});
|
|
2714
|
-
|
|
2715
|
-
|
|
2727
|
+
const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ fullProfile, params }) => ({
|
|
2728
|
+
profile: makeProfileInfo(fullProfile, params.id_token)
|
|
2729
|
+
});
|
|
2730
|
+
const signInResolver = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : defaultSignInResolver;
|
|
2731
|
+
const provider = new OneLoginProvider({
|
|
2732
|
+
clientId,
|
|
2733
|
+
clientSecret,
|
|
2734
|
+
callbackUrl,
|
|
2735
|
+
issuer,
|
|
2736
|
+
authHandler,
|
|
2737
|
+
signInResolver,
|
|
2738
|
+
tokenIssuer,
|
|
2739
|
+
catalogIdentityClient,
|
|
2740
|
+
logger
|
|
2741
|
+
});
|
|
2742
|
+
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
2743
|
+
disableRefresh: false,
|
|
2744
|
+
providerId,
|
|
2745
|
+
tokenIssuer
|
|
2746
|
+
});
|
|
2747
|
+
});
|
|
2748
|
+
};
|
|
2749
|
+
|
|
2750
|
+
class SamlAuthProvider {
|
|
2716
2751
|
constructor(options) {
|
|
2717
|
-
this.
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2752
|
+
this.appUrl = options.appUrl;
|
|
2753
|
+
this.signInResolver = options.signInResolver;
|
|
2754
|
+
this.authHandler = options.authHandler;
|
|
2755
|
+
this.tokenIssuer = options.tokenIssuer;
|
|
2756
|
+
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
2757
|
+
this.logger = options.logger;
|
|
2758
|
+
this.strategy = new passportSaml.Strategy({ ...options }, (fullProfile, done) => {
|
|
2759
|
+
done(void 0, { fullProfile });
|
|
2723
2760
|
});
|
|
2724
2761
|
}
|
|
2725
|
-
async
|
|
2726
|
-
const
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2762
|
+
async start(req, res) {
|
|
2763
|
+
const { url } = await executeRedirectStrategy(req, this.strategy, {});
|
|
2764
|
+
res.redirect(url);
|
|
2765
|
+
}
|
|
2766
|
+
async frameHandler(req, res) {
|
|
2767
|
+
try {
|
|
2768
|
+
const context = {
|
|
2769
|
+
logger: this.logger,
|
|
2770
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
2771
|
+
tokenIssuer: this.tokenIssuer
|
|
2772
|
+
};
|
|
2773
|
+
const { result } = await executeFrameHandlerStrategy(req, this.strategy);
|
|
2774
|
+
const { profile } = await this.authHandler(result, context);
|
|
2775
|
+
const response = {
|
|
2776
|
+
profile,
|
|
2777
|
+
providerInfo: {}
|
|
2778
|
+
};
|
|
2779
|
+
if (this.signInResolver) {
|
|
2780
|
+
const signInResponse = await this.signInResolver({
|
|
2781
|
+
result,
|
|
2782
|
+
profile
|
|
2783
|
+
}, context);
|
|
2784
|
+
response.backstageIdentity = prepareBackstageIdentityResponse(signInResponse);
|
|
2785
|
+
}
|
|
2786
|
+
return postMessageResponse(res, this.appUrl, {
|
|
2787
|
+
type: "authorization_response",
|
|
2788
|
+
response
|
|
2789
|
+
});
|
|
2790
|
+
} catch (error) {
|
|
2791
|
+
const { name, message } = errors.isError(error) ? error : new Error("Encountered invalid error");
|
|
2792
|
+
return postMessageResponse(res, this.appUrl, {
|
|
2793
|
+
type: "authorization_response",
|
|
2794
|
+
error: { name, message }
|
|
2795
|
+
});
|
|
2796
|
+
}
|
|
2733
2797
|
}
|
|
2734
|
-
async
|
|
2735
|
-
|
|
2798
|
+
async logout(_req, res) {
|
|
2799
|
+
res.end();
|
|
2736
2800
|
}
|
|
2737
2801
|
}
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
}
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2802
|
+
const samlDefaultSignInResolver = async (info, ctx) => {
|
|
2803
|
+
const id = info.result.fullProfile.nameID;
|
|
2804
|
+
const token = await ctx.tokenIssuer.issueToken({
|
|
2805
|
+
claims: { sub: id }
|
|
2806
|
+
});
|
|
2807
|
+
return { id, token };
|
|
2808
|
+
};
|
|
2809
|
+
const createSamlProvider = (options) => {
|
|
2810
|
+
return ({
|
|
2811
|
+
providerId,
|
|
2812
|
+
globalConfig,
|
|
2813
|
+
config,
|
|
2814
|
+
tokenIssuer,
|
|
2815
|
+
catalogApi,
|
|
2816
|
+
logger
|
|
2817
|
+
}) => {
|
|
2818
|
+
var _a, _b;
|
|
2819
|
+
const catalogIdentityClient = new CatalogIdentityClient({
|
|
2820
|
+
catalogApi,
|
|
2821
|
+
tokenIssuer
|
|
2747
2822
|
});
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2823
|
+
const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({ fullProfile }) => ({
|
|
2824
|
+
profile: {
|
|
2825
|
+
email: fullProfile.email,
|
|
2826
|
+
displayName: fullProfile.displayName
|
|
2827
|
+
}
|
|
2828
|
+
});
|
|
2829
|
+
const signInResolverFn = (_b = (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver) != null ? _b : samlDefaultSignInResolver;
|
|
2830
|
+
const signInResolver = (info) => signInResolverFn(info, {
|
|
2831
|
+
catalogIdentityClient,
|
|
2832
|
+
tokenIssuer,
|
|
2833
|
+
logger
|
|
2834
|
+
});
|
|
2835
|
+
return new SamlAuthProvider({
|
|
2836
|
+
callbackUrl: `${globalConfig.baseUrl}/${providerId}/handler/frame`,
|
|
2837
|
+
entryPoint: config.getString("entryPoint"),
|
|
2838
|
+
logoutUrl: config.getOptionalString("logoutUrl"),
|
|
2839
|
+
audience: config.getOptionalString("audience"),
|
|
2840
|
+
issuer: config.getString("issuer"),
|
|
2841
|
+
cert: config.getString("cert"),
|
|
2842
|
+
privateKey: config.getOptionalString("privateKey"),
|
|
2843
|
+
authnContext: config.getOptionalStringArray("authnContext"),
|
|
2844
|
+
identifierFormat: config.getOptionalString("identifierFormat"),
|
|
2845
|
+
decryptionPvk: config.getOptionalString("decryptionPvk"),
|
|
2846
|
+
signatureAlgorithm: config.getOptionalString("signatureAlgorithm"),
|
|
2847
|
+
digestAlgorithm: config.getOptionalString("digestAlgorithm"),
|
|
2848
|
+
acceptedClockSkewMs: config.getOptionalNumber("acceptedClockSkewMs"),
|
|
2849
|
+
tokenIssuer,
|
|
2850
|
+
appUrl: globalConfig.appUrl,
|
|
2851
|
+
authHandler,
|
|
2852
|
+
signInResolver,
|
|
2853
|
+
logger,
|
|
2854
|
+
catalogIdentityClient
|
|
2855
|
+
});
|
|
2856
|
+
};
|
|
2857
|
+
};
|
|
2858
|
+
|
|
2859
|
+
const IAP_JWT_HEADER = "x-goog-iap-jwt-assertion";
|
|
2860
|
+
|
|
2861
|
+
function createTokenValidator(audience, mockClient) {
|
|
2862
|
+
const client = mockClient != null ? mockClient : new googleAuthLibrary.OAuth2Client();
|
|
2863
|
+
return async function tokenValidator(token) {
|
|
2864
|
+
const response = await client.getIapPublicKeys();
|
|
2865
|
+
const ticket = await client.verifySignedJwtWithCertsAsync(token, response.pubkeys, audience, ["https://cloud.google.com/iap"]);
|
|
2866
|
+
const payload = ticket.getPayload();
|
|
2867
|
+
if (!payload) {
|
|
2868
|
+
throw new TypeError("Token had no payload");
|
|
2752
2869
|
}
|
|
2870
|
+
return payload;
|
|
2871
|
+
};
|
|
2872
|
+
}
|
|
2873
|
+
async function parseRequestToken(jwtToken, tokenValidator) {
|
|
2874
|
+
if (typeof jwtToken !== "string" || !jwtToken) {
|
|
2875
|
+
throw new errors.AuthenticationError(`Missing Google IAP header: ${IAP_JWT_HEADER}`);
|
|
2753
2876
|
}
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
}))
|
|
2760
|
-
};
|
|
2877
|
+
let payload;
|
|
2878
|
+
try {
|
|
2879
|
+
payload = await tokenValidator(jwtToken);
|
|
2880
|
+
} catch (e) {
|
|
2881
|
+
throw new errors.AuthenticationError(`Google IAP token verification failed, ${e}`);
|
|
2761
2882
|
}
|
|
2883
|
+
if (!payload.sub || !payload.email) {
|
|
2884
|
+
throw new errors.AuthenticationError("Google IAP token payload is missing sub and/or email claim");
|
|
2885
|
+
}
|
|
2886
|
+
return {
|
|
2887
|
+
iapToken: {
|
|
2888
|
+
...payload,
|
|
2889
|
+
sub: payload.sub,
|
|
2890
|
+
email: payload.email
|
|
2891
|
+
}
|
|
2892
|
+
};
|
|
2762
2893
|
}
|
|
2894
|
+
const defaultAuthHandler = async ({
|
|
2895
|
+
iapToken
|
|
2896
|
+
}) => ({ profile: { email: iapToken.email } });
|
|
2763
2897
|
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
this.
|
|
2769
|
-
this.
|
|
2770
|
-
this.
|
|
2771
|
-
|
|
2772
|
-
static async create(settings) {
|
|
2773
|
-
const { path, timeout, ...firestoreSettings } = settings != null ? settings : {};
|
|
2774
|
-
const database = new firestore.Firestore(firestoreSettings);
|
|
2775
|
-
return new FirestoreKeyStore(database, path != null ? path : DEFAULT_DOCUMENT_PATH, timeout != null ? timeout : DEFAULT_TIMEOUT_MS);
|
|
2898
|
+
class GcpIapProvider {
|
|
2899
|
+
constructor(options) {
|
|
2900
|
+
this.authHandler = options.authHandler;
|
|
2901
|
+
this.signInResolver = options.signInResolver;
|
|
2902
|
+
this.tokenValidator = options.tokenValidator;
|
|
2903
|
+
this.tokenIssuer = options.tokenIssuer;
|
|
2904
|
+
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
2905
|
+
this.logger = options.logger;
|
|
2776
2906
|
}
|
|
2777
|
-
|
|
2778
|
-
try {
|
|
2779
|
-
await keyStore.verify();
|
|
2780
|
-
} catch (error) {
|
|
2781
|
-
if (process.env.NODE_ENV !== "development") {
|
|
2782
|
-
throw new Error(`Failed to connect to database: ${error.message}`);
|
|
2783
|
-
}
|
|
2784
|
-
logger == null ? void 0 : logger.warn(`Failed to connect to database: ${error.message}`);
|
|
2785
|
-
}
|
|
2907
|
+
async start() {
|
|
2786
2908
|
}
|
|
2787
|
-
async
|
|
2788
|
-
await this.withTimeout(this.database.collection(this.path).doc(key.kid).set({
|
|
2789
|
-
kid: key.kid,
|
|
2790
|
-
key: JSON.stringify(key)
|
|
2791
|
-
}));
|
|
2909
|
+
async frameHandler() {
|
|
2792
2910
|
}
|
|
2793
|
-
async
|
|
2794
|
-
const
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
}))
|
|
2911
|
+
async refresh(req, res) {
|
|
2912
|
+
const result = await parseRequestToken(req.header(IAP_JWT_HEADER), this.tokenValidator);
|
|
2913
|
+
const context = {
|
|
2914
|
+
logger: this.logger,
|
|
2915
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
2916
|
+
tokenIssuer: this.tokenIssuer
|
|
2800
2917
|
};
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
reject(new Error(`Operation timed out after ${this.timeout}ms`));
|
|
2810
|
-
}, this.timeout));
|
|
2811
|
-
return Promise.race([operation, timer]);
|
|
2812
|
-
}
|
|
2813
|
-
async verify() {
|
|
2814
|
-
await this.withTimeout(this.database.collection(this.path).limit(1).get());
|
|
2918
|
+
const { profile } = await this.authHandler(result, context);
|
|
2919
|
+
const backstageIdentity = await this.signInResolver({ profile, result }, context);
|
|
2920
|
+
const response = {
|
|
2921
|
+
providerInfo: { iapToken: result.iapToken },
|
|
2922
|
+
profile,
|
|
2923
|
+
backstageIdentity: prepareBackstageIdentityResponse(backstageIdentity)
|
|
2924
|
+
};
|
|
2925
|
+
res.json(response);
|
|
2815
2926
|
}
|
|
2816
2927
|
}
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
static async fromConfig(config, options) {
|
|
2928
|
+
function createGcpIapProvider(options) {
|
|
2929
|
+
return ({ config, tokenIssuer, catalogApi, logger }) => {
|
|
2820
2930
|
var _a;
|
|
2821
|
-
const
|
|
2822
|
-
const
|
|
2823
|
-
const
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
const keyStore = await FirestoreKeyStore.create(lodash.pickBy({
|
|
2839
|
-
projectId: settings == null ? void 0 : settings.getOptionalString("projectId"),
|
|
2840
|
-
keyFilename: settings == null ? void 0 : settings.getOptionalString("keyFilename"),
|
|
2841
|
-
host: settings == null ? void 0 : settings.getOptionalString("host"),
|
|
2842
|
-
port: settings == null ? void 0 : settings.getOptionalNumber("port"),
|
|
2843
|
-
ssl: settings == null ? void 0 : settings.getOptionalBoolean("ssl"),
|
|
2844
|
-
path: settings == null ? void 0 : settings.getOptionalString("path"),
|
|
2845
|
-
timeout: settings == null ? void 0 : settings.getOptionalNumber("timeout")
|
|
2846
|
-
}, (value) => value !== void 0));
|
|
2847
|
-
await FirestoreKeyStore.verifyConnection(keyStore, logger);
|
|
2848
|
-
return keyStore;
|
|
2849
|
-
}
|
|
2850
|
-
throw new Error(`Unknown KeyStore provider: ${provider}`);
|
|
2851
|
-
}
|
|
2931
|
+
const audience = config.getString("audience");
|
|
2932
|
+
const authHandler = (_a = options.authHandler) != null ? _a : defaultAuthHandler;
|
|
2933
|
+
const signInResolver = options.signIn.resolver;
|
|
2934
|
+
const tokenValidator = createTokenValidator(audience);
|
|
2935
|
+
const catalogIdentityClient = new CatalogIdentityClient({
|
|
2936
|
+
catalogApi,
|
|
2937
|
+
tokenIssuer
|
|
2938
|
+
});
|
|
2939
|
+
return new GcpIapProvider({
|
|
2940
|
+
authHandler,
|
|
2941
|
+
signInResolver,
|
|
2942
|
+
tokenValidator,
|
|
2943
|
+
tokenIssuer,
|
|
2944
|
+
catalogIdentityClient,
|
|
2945
|
+
logger
|
|
2946
|
+
});
|
|
2947
|
+
};
|
|
2852
2948
|
}
|
|
2853
2949
|
|
|
2950
|
+
const factories = {
|
|
2951
|
+
google: createGoogleProvider(),
|
|
2952
|
+
github: createGithubProvider(),
|
|
2953
|
+
gitlab: createGitlabProvider(),
|
|
2954
|
+
saml: createSamlProvider(),
|
|
2955
|
+
okta: createOktaProvider(),
|
|
2956
|
+
auth0: createAuth0Provider(),
|
|
2957
|
+
microsoft: createMicrosoftProvider(),
|
|
2958
|
+
oauth2: createOAuth2Provider(),
|
|
2959
|
+
oidc: createOidcProvider(),
|
|
2960
|
+
onelogin: createOneLoginProvider(),
|
|
2961
|
+
awsalb: createAwsAlbProvider(),
|
|
2962
|
+
bitbucket: createBitbucketProvider(),
|
|
2963
|
+
atlassian: createAtlassianProvider()
|
|
2964
|
+
};
|
|
2965
|
+
|
|
2854
2966
|
async function createRouter(options) {
|
|
2855
2967
|
const { logger, config, discovery, database, providerFactories } = options;
|
|
2856
2968
|
const router = Router__default["default"]();
|
|
@@ -2873,7 +2985,7 @@ async function createRouter(options) {
|
|
|
2873
2985
|
secret,
|
|
2874
2986
|
saveUninitialized: false,
|
|
2875
2987
|
resave: false,
|
|
2876
|
-
cookie: { secure: enforceCookieSSL }
|
|
2988
|
+
cookie: { secure: enforceCookieSSL ? "auto" : false }
|
|
2877
2989
|
}));
|
|
2878
2990
|
router.use(passport__default["default"].initialize());
|
|
2879
2991
|
router.use(passport__default["default"].session());
|
|
@@ -2969,6 +3081,7 @@ exports.createGitlabProvider = createGitlabProvider;
|
|
|
2969
3081
|
exports.createGoogleProvider = createGoogleProvider;
|
|
2970
3082
|
exports.createMicrosoftProvider = createMicrosoftProvider;
|
|
2971
3083
|
exports.createOAuth2Provider = createOAuth2Provider;
|
|
3084
|
+
exports.createOauth2ProxyProvider = createOauth2ProxyProvider;
|
|
2972
3085
|
exports.createOidcProvider = createOidcProvider;
|
|
2973
3086
|
exports.createOktaProvider = createOktaProvider;
|
|
2974
3087
|
exports.createOneLoginProvider = createOneLoginProvider;
|