@backstage/plugin-auth-backend 0.4.2 → 0.4.6
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 +61 -0
- package/config.d.ts +28 -0
- package/dist/index.cjs.js +592 -91
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +223 -108
- package/package.json +12 -10
package/dist/index.cjs.js
CHANGED
|
@@ -18,16 +18,18 @@ var passportMicrosoft = require('passport-microsoft');
|
|
|
18
18
|
var got = require('got');
|
|
19
19
|
var OAuth2Strategy = require('passport-oauth2');
|
|
20
20
|
var passportOktaOauth = require('passport-okta-oauth');
|
|
21
|
-
var
|
|
22
|
-
var passportSaml = require('passport-saml');
|
|
23
|
-
var passportOneloginOauth = require('passport-onelogin-oauth');
|
|
21
|
+
var passportBitbucketOauth2 = require('passport-bitbucket-oauth2');
|
|
24
22
|
var fetch = require('cross-fetch');
|
|
25
23
|
var NodeCache = require('node-cache');
|
|
26
24
|
var jose = require('jose');
|
|
25
|
+
var openidClient = require('openid-client');
|
|
26
|
+
var passportSaml = require('passport-saml');
|
|
27
|
+
var passportOneloginOauth = require('passport-onelogin-oauth');
|
|
27
28
|
var catalogClient = require('@backstage/catalog-client');
|
|
28
29
|
var uuid = require('uuid');
|
|
29
30
|
var luxon = require('luxon');
|
|
30
31
|
var backendCommon = require('@backstage/backend-common');
|
|
32
|
+
var firestore = require('@google-cloud/firestore');
|
|
31
33
|
var session = require('express-session');
|
|
32
34
|
var passport = require('passport');
|
|
33
35
|
var minimatch = require('minimatch');
|
|
@@ -416,12 +418,10 @@ class OAuthAdapter {
|
|
|
416
418
|
response
|
|
417
419
|
});
|
|
418
420
|
} catch (error) {
|
|
421
|
+
const {name, message} = errors.isError(error) ? error : new Error("Encountered invalid error");
|
|
419
422
|
return postMessageResponse(res, appOrigin, {
|
|
420
423
|
type: "authorization_response",
|
|
421
|
-
error: {
|
|
422
|
-
name: error.name,
|
|
423
|
-
message: error.message
|
|
424
|
-
}
|
|
424
|
+
error: {name, message}
|
|
425
425
|
});
|
|
426
426
|
}
|
|
427
427
|
}
|
|
@@ -457,7 +457,7 @@ class OAuthAdapter {
|
|
|
457
457
|
}
|
|
458
458
|
res.status(200).json(response);
|
|
459
459
|
} catch (error) {
|
|
460
|
-
res.status(401).send(
|
|
460
|
+
res.status(401).send(String(error));
|
|
461
461
|
}
|
|
462
462
|
}
|
|
463
463
|
async populateIdentity(identity) {
|
|
@@ -550,6 +550,7 @@ class GithubAuthProvider {
|
|
|
550
550
|
constructor(options) {
|
|
551
551
|
this.signInResolver = options.signInResolver;
|
|
552
552
|
this.authHandler = options.authHandler;
|
|
553
|
+
this.stateEncoder = options.stateEncoder;
|
|
553
554
|
this.tokenIssuer = options.tokenIssuer;
|
|
554
555
|
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
555
556
|
this.logger = options.logger;
|
|
@@ -567,7 +568,7 @@ class GithubAuthProvider {
|
|
|
567
568
|
async start(req) {
|
|
568
569
|
return await executeRedirectStrategy(req, this._strategy, {
|
|
569
570
|
scope: req.scope,
|
|
570
|
-
state:
|
|
571
|
+
state: (await this.stateEncoder(req)).encodedState
|
|
571
572
|
});
|
|
572
573
|
}
|
|
573
574
|
async handler(req) {
|
|
@@ -578,13 +579,17 @@ class GithubAuthProvider {
|
|
|
578
579
|
};
|
|
579
580
|
}
|
|
580
581
|
async refresh(req) {
|
|
581
|
-
const {
|
|
582
|
+
const {
|
|
583
|
+
accessToken,
|
|
584
|
+
refreshToken: newRefreshToken,
|
|
585
|
+
params
|
|
586
|
+
} = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
|
|
582
587
|
const fullProfile = await executeFetchUserProfileStrategy(this._strategy, accessToken);
|
|
583
588
|
return this.handleResult({
|
|
584
589
|
fullProfile,
|
|
585
590
|
params,
|
|
586
591
|
accessToken,
|
|
587
|
-
refreshToken:
|
|
592
|
+
refreshToken: newRefreshToken
|
|
588
593
|
});
|
|
589
594
|
}
|
|
590
595
|
async handleResult(result) {
|
|
@@ -593,6 +598,7 @@ class GithubAuthProvider {
|
|
|
593
598
|
const response = {
|
|
594
599
|
providerInfo: {
|
|
595
600
|
accessToken: result.accessToken,
|
|
601
|
+
refreshToken: result.refreshToken,
|
|
596
602
|
scope: result.params.scope,
|
|
597
603
|
expiresInSeconds: expiresInStr === void 0 ? void 0 : Number(expiresInStr)
|
|
598
604
|
},
|
|
@@ -628,7 +634,7 @@ const createGithubProvider = (options) => {
|
|
|
628
634
|
catalogApi,
|
|
629
635
|
logger
|
|
630
636
|
}) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
|
|
631
|
-
var _a, _b;
|
|
637
|
+
var _a, _b, _c;
|
|
632
638
|
const clientId = envConfig.getString("clientId");
|
|
633
639
|
const clientSecret = envConfig.getString("clientSecret");
|
|
634
640
|
const enterpriseInstanceUrl = envConfig.getOptionalString("enterpriseInstanceUrl");
|
|
@@ -650,6 +656,9 @@ const createGithubProvider = (options) => {
|
|
|
650
656
|
tokenIssuer,
|
|
651
657
|
logger
|
|
652
658
|
});
|
|
659
|
+
const stateEncoder = (_c = options == null ? void 0 : options.stateEncoder) != null ? _c : async (req) => {
|
|
660
|
+
return {encodedState: encodeState(req.state)};
|
|
661
|
+
};
|
|
653
662
|
const provider = new GithubAuthProvider({
|
|
654
663
|
clientId,
|
|
655
664
|
clientSecret,
|
|
@@ -661,6 +670,7 @@ const createGithubProvider = (options) => {
|
|
|
661
670
|
authHandler,
|
|
662
671
|
tokenIssuer,
|
|
663
672
|
catalogIdentityClient,
|
|
673
|
+
stateEncoder,
|
|
664
674
|
logger
|
|
665
675
|
});
|
|
666
676
|
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
@@ -1378,6 +1388,9 @@ const createOktaProvider = (_options) => {
|
|
|
1378
1388
|
const clientSecret = envConfig.getString("clientSecret");
|
|
1379
1389
|
const audience = envConfig.getString("audience");
|
|
1380
1390
|
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
1391
|
+
if (!audience.startsWith("https://")) {
|
|
1392
|
+
throw new Error("URL for 'audience' must start with 'https://'.");
|
|
1393
|
+
}
|
|
1381
1394
|
const catalogIdentityClient = new CatalogIdentityClient({
|
|
1382
1395
|
catalogApi,
|
|
1383
1396
|
tokenIssuer
|
|
@@ -1410,6 +1423,444 @@ const createOktaProvider = (_options) => {
|
|
|
1410
1423
|
});
|
|
1411
1424
|
};
|
|
1412
1425
|
|
|
1426
|
+
class BitbucketAuthProvider {
|
|
1427
|
+
constructor(options) {
|
|
1428
|
+
this.signInResolver = options.signInResolver;
|
|
1429
|
+
this.authHandler = options.authHandler;
|
|
1430
|
+
this.tokenIssuer = options.tokenIssuer;
|
|
1431
|
+
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
1432
|
+
this.logger = options.logger;
|
|
1433
|
+
this._strategy = new passportBitbucketOauth2.Strategy({
|
|
1434
|
+
clientID: options.clientId,
|
|
1435
|
+
clientSecret: options.clientSecret,
|
|
1436
|
+
callbackURL: options.callbackUrl,
|
|
1437
|
+
passReqToCallback: false
|
|
1438
|
+
}, (accessToken, refreshToken, params, fullProfile, done) => {
|
|
1439
|
+
done(void 0, {
|
|
1440
|
+
fullProfile,
|
|
1441
|
+
params,
|
|
1442
|
+
accessToken,
|
|
1443
|
+
refreshToken
|
|
1444
|
+
}, {
|
|
1445
|
+
refreshToken
|
|
1446
|
+
});
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
async start(req) {
|
|
1450
|
+
return await executeRedirectStrategy(req, this._strategy, {
|
|
1451
|
+
accessType: "offline",
|
|
1452
|
+
prompt: "consent",
|
|
1453
|
+
scope: req.scope,
|
|
1454
|
+
state: encodeState(req.state)
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
async handler(req) {
|
|
1458
|
+
const {result, privateInfo} = await executeFrameHandlerStrategy(req, this._strategy);
|
|
1459
|
+
return {
|
|
1460
|
+
response: await this.handleResult(result),
|
|
1461
|
+
refreshToken: privateInfo.refreshToken
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
async refresh(req) {
|
|
1465
|
+
const {accessToken, params} = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
|
|
1466
|
+
const fullProfile = await executeFetchUserProfileStrategy(this._strategy, accessToken);
|
|
1467
|
+
return this.handleResult({
|
|
1468
|
+
fullProfile,
|
|
1469
|
+
params,
|
|
1470
|
+
accessToken,
|
|
1471
|
+
refreshToken: req.refreshToken
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
async handleResult(result) {
|
|
1475
|
+
result.fullProfile.avatarUrl = result.fullProfile._json.links.avatar.href;
|
|
1476
|
+
const {profile} = await this.authHandler(result);
|
|
1477
|
+
const response = {
|
|
1478
|
+
providerInfo: {
|
|
1479
|
+
idToken: result.params.id_token,
|
|
1480
|
+
accessToken: result.accessToken,
|
|
1481
|
+
scope: result.params.scope,
|
|
1482
|
+
expiresInSeconds: result.params.expires_in
|
|
1483
|
+
},
|
|
1484
|
+
profile
|
|
1485
|
+
};
|
|
1486
|
+
if (this.signInResolver) {
|
|
1487
|
+
response.backstageIdentity = await this.signInResolver({
|
|
1488
|
+
result,
|
|
1489
|
+
profile
|
|
1490
|
+
}, {
|
|
1491
|
+
tokenIssuer: this.tokenIssuer,
|
|
1492
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
1493
|
+
logger: this.logger
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
return response;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
const bitbucketUsernameSignInResolver = async (info, ctx) => {
|
|
1500
|
+
const {result} = info;
|
|
1501
|
+
if (!result.fullProfile.username) {
|
|
1502
|
+
throw new Error("Bitbucket profile contained no Username");
|
|
1503
|
+
}
|
|
1504
|
+
const entity = await ctx.catalogIdentityClient.findUser({
|
|
1505
|
+
annotations: {
|
|
1506
|
+
"bitbucket.org/username": result.fullProfile.username
|
|
1507
|
+
}
|
|
1508
|
+
});
|
|
1509
|
+
const claims = getEntityClaims(entity);
|
|
1510
|
+
const token = await ctx.tokenIssuer.issueToken({claims});
|
|
1511
|
+
return {id: entity.metadata.name, entity, token};
|
|
1512
|
+
};
|
|
1513
|
+
const bitbucketUserIdSignInResolver = async (info, ctx) => {
|
|
1514
|
+
const {result} = info;
|
|
1515
|
+
if (!result.fullProfile.id) {
|
|
1516
|
+
throw new Error("Bitbucket profile contained no User ID");
|
|
1517
|
+
}
|
|
1518
|
+
const entity = await ctx.catalogIdentityClient.findUser({
|
|
1519
|
+
annotations: {
|
|
1520
|
+
"bitbucket.org/user-id": result.fullProfile.id
|
|
1521
|
+
}
|
|
1522
|
+
});
|
|
1523
|
+
const claims = getEntityClaims(entity);
|
|
1524
|
+
const token = await ctx.tokenIssuer.issueToken({claims});
|
|
1525
|
+
return {id: entity.metadata.name, entity, token};
|
|
1526
|
+
};
|
|
1527
|
+
const createBitbucketProvider = (options) => {
|
|
1528
|
+
return ({
|
|
1529
|
+
providerId,
|
|
1530
|
+
globalConfig,
|
|
1531
|
+
config,
|
|
1532
|
+
tokenIssuer,
|
|
1533
|
+
catalogApi,
|
|
1534
|
+
logger
|
|
1535
|
+
}) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
|
|
1536
|
+
var _a;
|
|
1537
|
+
const clientId = envConfig.getString("clientId");
|
|
1538
|
+
const clientSecret = envConfig.getString("clientSecret");
|
|
1539
|
+
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
1540
|
+
const catalogIdentityClient = new CatalogIdentityClient({
|
|
1541
|
+
catalogApi,
|
|
1542
|
+
tokenIssuer
|
|
1543
|
+
});
|
|
1544
|
+
const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({fullProfile, params}) => ({
|
|
1545
|
+
profile: makeProfileInfo(fullProfile, params.id_token)
|
|
1546
|
+
});
|
|
1547
|
+
const provider = new BitbucketAuthProvider({
|
|
1548
|
+
clientId,
|
|
1549
|
+
clientSecret,
|
|
1550
|
+
callbackUrl,
|
|
1551
|
+
signInResolver: (_a = options == null ? void 0 : options.signIn) == null ? void 0 : _a.resolver,
|
|
1552
|
+
authHandler,
|
|
1553
|
+
tokenIssuer,
|
|
1554
|
+
catalogIdentityClient,
|
|
1555
|
+
logger
|
|
1556
|
+
});
|
|
1557
|
+
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
1558
|
+
disableRefresh: false,
|
|
1559
|
+
providerId,
|
|
1560
|
+
tokenIssuer
|
|
1561
|
+
});
|
|
1562
|
+
});
|
|
1563
|
+
};
|
|
1564
|
+
|
|
1565
|
+
const defaultScopes = ["offline_access", "read:me"];
|
|
1566
|
+
class AtlassianStrategy extends OAuth2Strategy__default['default'] {
|
|
1567
|
+
constructor(options, verify) {
|
|
1568
|
+
if (!options.scope) {
|
|
1569
|
+
throw new TypeError("Atlassian requires a scope option");
|
|
1570
|
+
}
|
|
1571
|
+
const scopes = options.scope.split(" ");
|
|
1572
|
+
const optionsWithURLs = {
|
|
1573
|
+
...options,
|
|
1574
|
+
authorizationURL: `https://auth.atlassian.com/authorize`,
|
|
1575
|
+
tokenURL: `https://auth.atlassian.com/oauth/token`,
|
|
1576
|
+
scope: Array.from(new Set([...defaultScopes, ...scopes]))
|
|
1577
|
+
};
|
|
1578
|
+
super(optionsWithURLs, verify);
|
|
1579
|
+
this.profileURL = "https://api.atlassian.com/me";
|
|
1580
|
+
this.name = "atlassian";
|
|
1581
|
+
this._oauth2.useAuthorizationHeaderforGET(true);
|
|
1582
|
+
}
|
|
1583
|
+
authorizationParams() {
|
|
1584
|
+
return {
|
|
1585
|
+
audience: "api.atlassian.com",
|
|
1586
|
+
prompt: "consent"
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
userProfile(accessToken, done) {
|
|
1590
|
+
this._oauth2.get(this.profileURL, accessToken, (err, body) => {
|
|
1591
|
+
if (err) {
|
|
1592
|
+
return done(new OAuth2Strategy.InternalOAuthError("Failed to fetch user profile", err.statusCode));
|
|
1593
|
+
}
|
|
1594
|
+
if (!body) {
|
|
1595
|
+
return done(new Error("Failed to fetch user profile, body cannot be empty"));
|
|
1596
|
+
}
|
|
1597
|
+
try {
|
|
1598
|
+
const json = typeof body !== "string" ? body.toString() : body;
|
|
1599
|
+
const profile = AtlassianStrategy.parse(json);
|
|
1600
|
+
return done(null, profile);
|
|
1601
|
+
} catch (e) {
|
|
1602
|
+
return done(new Error("Failed to parse user profile"));
|
|
1603
|
+
}
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
static parse(json) {
|
|
1607
|
+
const resp = JSON.parse(json);
|
|
1608
|
+
return {
|
|
1609
|
+
id: resp.account_id,
|
|
1610
|
+
provider: "atlassian",
|
|
1611
|
+
username: resp.nickname,
|
|
1612
|
+
displayName: resp.name,
|
|
1613
|
+
emails: [{value: resp.email}],
|
|
1614
|
+
photos: [{value: resp.picture}]
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
const atlassianDefaultAuthHandler = async ({
|
|
1620
|
+
fullProfile,
|
|
1621
|
+
params
|
|
1622
|
+
}) => ({
|
|
1623
|
+
profile: makeProfileInfo(fullProfile, params.id_token)
|
|
1624
|
+
});
|
|
1625
|
+
class AtlassianAuthProvider {
|
|
1626
|
+
constructor(options) {
|
|
1627
|
+
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
1628
|
+
this.logger = options.logger;
|
|
1629
|
+
this.tokenIssuer = options.tokenIssuer;
|
|
1630
|
+
this.authHandler = options.authHandler;
|
|
1631
|
+
this.signInResolver = options.signInResolver;
|
|
1632
|
+
this._strategy = new AtlassianStrategy({
|
|
1633
|
+
clientID: options.clientId,
|
|
1634
|
+
clientSecret: options.clientSecret,
|
|
1635
|
+
callbackURL: options.callbackUrl,
|
|
1636
|
+
scope: options.scopes
|
|
1637
|
+
}, (accessToken, refreshToken, params, fullProfile, done) => {
|
|
1638
|
+
done(void 0, {
|
|
1639
|
+
fullProfile,
|
|
1640
|
+
accessToken,
|
|
1641
|
+
refreshToken,
|
|
1642
|
+
params
|
|
1643
|
+
});
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
async start(req) {
|
|
1647
|
+
return await executeRedirectStrategy(req, this._strategy, {
|
|
1648
|
+
state: encodeState(req.state)
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
async handler(req) {
|
|
1652
|
+
var _a;
|
|
1653
|
+
const {result} = await executeFrameHandlerStrategy(req, this._strategy);
|
|
1654
|
+
return {
|
|
1655
|
+
response: await this.handleResult(result),
|
|
1656
|
+
refreshToken: (_a = result.refreshToken) != null ? _a : ""
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
async handleResult(result) {
|
|
1660
|
+
const {profile} = await this.authHandler(result);
|
|
1661
|
+
const response = {
|
|
1662
|
+
providerInfo: {
|
|
1663
|
+
idToken: result.params.id_token,
|
|
1664
|
+
accessToken: result.accessToken,
|
|
1665
|
+
refreshToken: result.refreshToken,
|
|
1666
|
+
scope: result.params.scope,
|
|
1667
|
+
expiresInSeconds: result.params.expires_in
|
|
1668
|
+
},
|
|
1669
|
+
profile
|
|
1670
|
+
};
|
|
1671
|
+
if (this.signInResolver) {
|
|
1672
|
+
response.backstageIdentity = await this.signInResolver({
|
|
1673
|
+
result,
|
|
1674
|
+
profile
|
|
1675
|
+
}, {
|
|
1676
|
+
tokenIssuer: this.tokenIssuer,
|
|
1677
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
1678
|
+
logger: this.logger
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
return response;
|
|
1682
|
+
}
|
|
1683
|
+
async refresh(req) {
|
|
1684
|
+
const {
|
|
1685
|
+
accessToken,
|
|
1686
|
+
params,
|
|
1687
|
+
refreshToken: newRefreshToken
|
|
1688
|
+
} = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
|
|
1689
|
+
const fullProfile = await executeFetchUserProfileStrategy(this._strategy, accessToken);
|
|
1690
|
+
return this.handleResult({
|
|
1691
|
+
fullProfile,
|
|
1692
|
+
params,
|
|
1693
|
+
accessToken,
|
|
1694
|
+
refreshToken: newRefreshToken
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
const createAtlassianProvider = (options) => {
|
|
1699
|
+
return ({
|
|
1700
|
+
providerId,
|
|
1701
|
+
globalConfig,
|
|
1702
|
+
config,
|
|
1703
|
+
tokenIssuer,
|
|
1704
|
+
catalogApi,
|
|
1705
|
+
logger
|
|
1706
|
+
}) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
|
|
1707
|
+
var _a, _b;
|
|
1708
|
+
const clientId = envConfig.getString("clientId");
|
|
1709
|
+
const clientSecret = envConfig.getString("clientSecret");
|
|
1710
|
+
const scopes = envConfig.getString("scopes");
|
|
1711
|
+
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
1712
|
+
const catalogIdentityClient = new CatalogIdentityClient({
|
|
1713
|
+
catalogApi,
|
|
1714
|
+
tokenIssuer
|
|
1715
|
+
});
|
|
1716
|
+
const authHandler = (_a = options == null ? void 0 : options.authHandler) != null ? _a : atlassianDefaultAuthHandler;
|
|
1717
|
+
const provider = new AtlassianAuthProvider({
|
|
1718
|
+
clientId,
|
|
1719
|
+
clientSecret,
|
|
1720
|
+
scopes,
|
|
1721
|
+
callbackUrl,
|
|
1722
|
+
authHandler,
|
|
1723
|
+
signInResolver: (_b = options == null ? void 0 : options.signIn) == null ? void 0 : _b.resolver,
|
|
1724
|
+
catalogIdentityClient,
|
|
1725
|
+
logger,
|
|
1726
|
+
tokenIssuer
|
|
1727
|
+
});
|
|
1728
|
+
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
1729
|
+
disableRefresh: true,
|
|
1730
|
+
providerId,
|
|
1731
|
+
tokenIssuer
|
|
1732
|
+
});
|
|
1733
|
+
});
|
|
1734
|
+
};
|
|
1735
|
+
|
|
1736
|
+
const ALB_JWT_HEADER = "x-amzn-oidc-data";
|
|
1737
|
+
const ALB_ACCESSTOKEN_HEADER = "x-amzn-oidc-accesstoken";
|
|
1738
|
+
const getJWTHeaders = (input) => {
|
|
1739
|
+
const encoded = input.split(".")[0];
|
|
1740
|
+
return JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
|
|
1741
|
+
};
|
|
1742
|
+
class AwsAlbAuthProvider {
|
|
1743
|
+
constructor(options) {
|
|
1744
|
+
this.region = options.region;
|
|
1745
|
+
this.issuer = options.issuer;
|
|
1746
|
+
this.authHandler = options.authHandler;
|
|
1747
|
+
this.signInResolver = options.signInResolver;
|
|
1748
|
+
this.tokenIssuer = options.tokenIssuer;
|
|
1749
|
+
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
1750
|
+
this.logger = options.logger;
|
|
1751
|
+
this.keyCache = new NodeCache__default['default']({stdTTL: 3600});
|
|
1752
|
+
}
|
|
1753
|
+
frameHandler() {
|
|
1754
|
+
return Promise.resolve(void 0);
|
|
1755
|
+
}
|
|
1756
|
+
async refresh(req, res) {
|
|
1757
|
+
try {
|
|
1758
|
+
const result = await this.getResult(req);
|
|
1759
|
+
const response = await this.handleResult(result);
|
|
1760
|
+
res.json(response);
|
|
1761
|
+
} catch (e) {
|
|
1762
|
+
this.logger.error("Exception occurred during AWS ALB token refresh", e);
|
|
1763
|
+
res.status(401);
|
|
1764
|
+
res.end();
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
start() {
|
|
1768
|
+
return Promise.resolve(void 0);
|
|
1769
|
+
}
|
|
1770
|
+
async getResult(req) {
|
|
1771
|
+
const jwt = req.header(ALB_JWT_HEADER);
|
|
1772
|
+
const accessToken = req.header(ALB_ACCESSTOKEN_HEADER);
|
|
1773
|
+
if (jwt === void 0) {
|
|
1774
|
+
throw new errors.AuthenticationError(`Missing ALB OIDC header: ${ALB_JWT_HEADER}`);
|
|
1775
|
+
}
|
|
1776
|
+
if (accessToken === void 0) {
|
|
1777
|
+
throw new errors.AuthenticationError(`Missing ALB OIDC header: ${ALB_ACCESSTOKEN_HEADER}`);
|
|
1778
|
+
}
|
|
1779
|
+
try {
|
|
1780
|
+
const headers = getJWTHeaders(jwt);
|
|
1781
|
+
const key = await this.getKey(headers.kid);
|
|
1782
|
+
const claims = jose.JWT.verify(jwt, key);
|
|
1783
|
+
if (this.issuer && claims.iss !== this.issuer) {
|
|
1784
|
+
throw new errors.AuthenticationError("Issuer mismatch on JWT token");
|
|
1785
|
+
}
|
|
1786
|
+
const fullProfile = {
|
|
1787
|
+
provider: "unknown",
|
|
1788
|
+
id: claims.sub,
|
|
1789
|
+
displayName: claims.name,
|
|
1790
|
+
username: claims.email.split("@")[0].toLowerCase(),
|
|
1791
|
+
name: {
|
|
1792
|
+
familyName: claims.family_name,
|
|
1793
|
+
givenName: claims.given_name
|
|
1794
|
+
},
|
|
1795
|
+
emails: [{value: claims.email.toLowerCase()}],
|
|
1796
|
+
photos: [{value: claims.picture}]
|
|
1797
|
+
};
|
|
1798
|
+
return {
|
|
1799
|
+
fullProfile,
|
|
1800
|
+
expiresInSeconds: claims.exp,
|
|
1801
|
+
accessToken
|
|
1802
|
+
};
|
|
1803
|
+
} catch (e) {
|
|
1804
|
+
throw new Error(`Exception occurred during JWT processing: ${e}`);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
async handleResult(result) {
|
|
1808
|
+
const {profile} = await this.authHandler(result);
|
|
1809
|
+
const backstageIdentity = await this.signInResolver({
|
|
1810
|
+
result,
|
|
1811
|
+
profile
|
|
1812
|
+
}, {
|
|
1813
|
+
tokenIssuer: this.tokenIssuer,
|
|
1814
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
1815
|
+
logger: this.logger
|
|
1816
|
+
});
|
|
1817
|
+
return {
|
|
1818
|
+
providerInfo: {
|
|
1819
|
+
accessToken: result.accessToken,
|
|
1820
|
+
expiresInSeconds: result.expiresInSeconds
|
|
1821
|
+
},
|
|
1822
|
+
backstageIdentity,
|
|
1823
|
+
profile
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
async getKey(keyId) {
|
|
1827
|
+
const optionalCacheKey = this.keyCache.get(keyId);
|
|
1828
|
+
if (optionalCacheKey) {
|
|
1829
|
+
return crypto__namespace.createPublicKey(optionalCacheKey);
|
|
1830
|
+
}
|
|
1831
|
+
const keyText = await fetch__default['default'](`https://public-keys.auth.elb.${this.region}.amazonaws.com/${keyId}`).then((response) => response.text());
|
|
1832
|
+
const keyValue = crypto__namespace.createPublicKey(keyText);
|
|
1833
|
+
this.keyCache.set(keyId, keyValue.export({format: "pem", type: "spki"}));
|
|
1834
|
+
return keyValue;
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
const createAwsAlbProvider = (options) => {
|
|
1838
|
+
return ({config, tokenIssuer, catalogApi, logger}) => {
|
|
1839
|
+
const region = config.getString("region");
|
|
1840
|
+
const issuer = config.getOptionalString("iss");
|
|
1841
|
+
if ((options == null ? void 0 : options.signIn.resolver) === void 0) {
|
|
1842
|
+
throw new Error("SignInResolver is required to use this authentication provider");
|
|
1843
|
+
}
|
|
1844
|
+
const catalogIdentityClient = new CatalogIdentityClient({
|
|
1845
|
+
catalogApi,
|
|
1846
|
+
tokenIssuer
|
|
1847
|
+
});
|
|
1848
|
+
const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({fullProfile}) => ({
|
|
1849
|
+
profile: makeProfileInfo(fullProfile)
|
|
1850
|
+
});
|
|
1851
|
+
const signInResolver = options == null ? void 0 : options.signIn.resolver;
|
|
1852
|
+
return new AwsAlbAuthProvider({
|
|
1853
|
+
region,
|
|
1854
|
+
issuer,
|
|
1855
|
+
signInResolver,
|
|
1856
|
+
authHandler,
|
|
1857
|
+
tokenIssuer,
|
|
1858
|
+
catalogIdentityClient,
|
|
1859
|
+
logger
|
|
1860
|
+
});
|
|
1861
|
+
};
|
|
1862
|
+
};
|
|
1863
|
+
|
|
1413
1864
|
class OidcAuthProvider {
|
|
1414
1865
|
constructor(options) {
|
|
1415
1866
|
this.implementation = this.setupStrategy(options);
|
|
@@ -1562,12 +2013,10 @@ class SamlAuthProvider {
|
|
|
1562
2013
|
}
|
|
1563
2014
|
});
|
|
1564
2015
|
} catch (error) {
|
|
2016
|
+
const {name, message} = errors.isError(error) ? error : new Error("Encountered invalid error");
|
|
1565
2017
|
return postMessageResponse(res, this.appUrl, {
|
|
1566
2018
|
type: "authorization_response",
|
|
1567
|
-
error: {
|
|
1568
|
-
name: error.name,
|
|
1569
|
-
message: error.message
|
|
1570
|
-
}
|
|
2019
|
+
error: {name, message}
|
|
1571
2020
|
});
|
|
1572
2021
|
}
|
|
1573
2022
|
}
|
|
@@ -1587,6 +2036,8 @@ const createSamlProvider = (_options) => {
|
|
|
1587
2036
|
issuer: config.getString("issuer"),
|
|
1588
2037
|
cert: config.getString("cert"),
|
|
1589
2038
|
privateCert: config.getOptionalString("privateKey"),
|
|
2039
|
+
authnContext: config.getOptionalStringArray("authnContext"),
|
|
2040
|
+
identifierFormat: config.getOptionalString("identifierFormat"),
|
|
1590
2041
|
decryptionPvk: config.getOptionalString("decryptionPvk"),
|
|
1591
2042
|
signatureAlgorithm: config.getOptionalString("signatureAlgorithm"),
|
|
1592
2043
|
digestAlgorithm: config.getOptionalString("digestAlgorithm"),
|
|
@@ -1783,77 +2234,6 @@ const createOneLoginProvider = (_options) => {
|
|
|
1783
2234
|
});
|
|
1784
2235
|
};
|
|
1785
2236
|
|
|
1786
|
-
const ALB_JWT_HEADER = "x-amzn-oidc-data";
|
|
1787
|
-
const getJWTHeaders = (input) => {
|
|
1788
|
-
const encoded = input.split(".")[0];
|
|
1789
|
-
return JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
|
|
1790
|
-
};
|
|
1791
|
-
class AwsAlbAuthProvider {
|
|
1792
|
-
constructor(logger, catalogClient, options) {
|
|
1793
|
-
this.logger = logger;
|
|
1794
|
-
this.catalogClient = catalogClient;
|
|
1795
|
-
this.options = options;
|
|
1796
|
-
this.keyCache = new NodeCache__default['default']({stdTTL: 3600});
|
|
1797
|
-
}
|
|
1798
|
-
frameHandler() {
|
|
1799
|
-
return Promise.resolve(void 0);
|
|
1800
|
-
}
|
|
1801
|
-
async refresh(req, res) {
|
|
1802
|
-
const jwt = req.header(ALB_JWT_HEADER);
|
|
1803
|
-
if (jwt !== void 0) {
|
|
1804
|
-
try {
|
|
1805
|
-
const headers = getJWTHeaders(jwt);
|
|
1806
|
-
const key = await this.getKey(headers.kid);
|
|
1807
|
-
const payload = jose.JWT.verify(jwt, key);
|
|
1808
|
-
if (this.options.issuer && headers.iss !== this.options.issuer) {
|
|
1809
|
-
throw new Error("issuer mismatch on JWT");
|
|
1810
|
-
}
|
|
1811
|
-
const resolvedEntity = await this.options.identityResolutionCallback(payload, this.catalogClient);
|
|
1812
|
-
res.json(resolvedEntity);
|
|
1813
|
-
} catch (e) {
|
|
1814
|
-
this.logger.error("exception occurred during JWT processing", e);
|
|
1815
|
-
res.status(401);
|
|
1816
|
-
res.end();
|
|
1817
|
-
}
|
|
1818
|
-
} else {
|
|
1819
|
-
res.status(401);
|
|
1820
|
-
res.end();
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
start() {
|
|
1824
|
-
return Promise.resolve(void 0);
|
|
1825
|
-
}
|
|
1826
|
-
async getKey(keyId) {
|
|
1827
|
-
const optionalCacheKey = this.keyCache.get(keyId);
|
|
1828
|
-
if (optionalCacheKey) {
|
|
1829
|
-
return crypto__namespace.createPublicKey(optionalCacheKey);
|
|
1830
|
-
}
|
|
1831
|
-
const keyText = await fetch__default['default'](`https://public-keys.auth.elb.${this.options.region}.amazonaws.com/${keyId}`).then((response) => response.text());
|
|
1832
|
-
const keyValue = crypto__namespace.createPublicKey(keyText);
|
|
1833
|
-
this.keyCache.set(keyId, keyValue.export({format: "pem", type: "spki"}));
|
|
1834
|
-
return keyValue;
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
|
-
const createAwsAlbProvider = (_options) => {
|
|
1838
|
-
return ({
|
|
1839
|
-
logger,
|
|
1840
|
-
catalogApi,
|
|
1841
|
-
config,
|
|
1842
|
-
identityResolver
|
|
1843
|
-
}) => {
|
|
1844
|
-
const region = config.getString("region");
|
|
1845
|
-
const issuer = config.getOptionalString("iss");
|
|
1846
|
-
if (identityResolver !== void 0) {
|
|
1847
|
-
return new AwsAlbAuthProvider(logger, catalogApi, {
|
|
1848
|
-
region,
|
|
1849
|
-
issuer,
|
|
1850
|
-
identityResolutionCallback: identityResolver
|
|
1851
|
-
});
|
|
1852
|
-
}
|
|
1853
|
-
throw new Error("Identity resolver is required to use this authentication provider");
|
|
1854
|
-
};
|
|
1855
|
-
};
|
|
1856
|
-
|
|
1857
2237
|
const factories = {
|
|
1858
2238
|
google: createGoogleProvider(),
|
|
1859
2239
|
github: createGithubProvider(),
|
|
@@ -1865,7 +2245,9 @@ const factories = {
|
|
|
1865
2245
|
oauth2: createOAuth2Provider(),
|
|
1866
2246
|
oidc: createOidcProvider(),
|
|
1867
2247
|
onelogin: createOneLoginProvider(),
|
|
1868
|
-
awsalb: createAwsAlbProvider()
|
|
2248
|
+
awsalb: createAwsAlbProvider(),
|
|
2249
|
+
bitbucket: createBitbucketProvider(),
|
|
2250
|
+
atlassian: createAtlassianProvider()
|
|
1869
2251
|
};
|
|
1870
2252
|
|
|
1871
2253
|
function createOidcRouter(options) {
|
|
@@ -2084,6 +2466,121 @@ class DatabaseKeyStore {
|
|
|
2084
2466
|
}
|
|
2085
2467
|
}
|
|
2086
2468
|
|
|
2469
|
+
class MemoryKeyStore {
|
|
2470
|
+
constructor() {
|
|
2471
|
+
this.keys = new Map();
|
|
2472
|
+
}
|
|
2473
|
+
async addKey(key) {
|
|
2474
|
+
this.keys.set(key.kid, {
|
|
2475
|
+
createdAt: luxon.DateTime.utc().toJSDate(),
|
|
2476
|
+
key: JSON.stringify(key)
|
|
2477
|
+
});
|
|
2478
|
+
}
|
|
2479
|
+
async removeKeys(kids) {
|
|
2480
|
+
for (const kid of kids) {
|
|
2481
|
+
this.keys.delete(kid);
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
async listKeys() {
|
|
2485
|
+
return {
|
|
2486
|
+
items: Array.from(this.keys).map(([, {createdAt, key: keyStr}]) => ({
|
|
2487
|
+
createdAt,
|
|
2488
|
+
key: JSON.parse(keyStr)
|
|
2489
|
+
}))
|
|
2490
|
+
};
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
2495
|
+
const DEFAULT_DOCUMENT_PATH = "sessions";
|
|
2496
|
+
class FirestoreKeyStore {
|
|
2497
|
+
constructor(database, path, timeout) {
|
|
2498
|
+
this.database = database;
|
|
2499
|
+
this.path = path;
|
|
2500
|
+
this.timeout = timeout;
|
|
2501
|
+
}
|
|
2502
|
+
static async create(settings) {
|
|
2503
|
+
const {path, timeout, ...firestoreSettings} = settings != null ? settings : {};
|
|
2504
|
+
const database = new firestore.Firestore(firestoreSettings);
|
|
2505
|
+
return new FirestoreKeyStore(database, path != null ? path : DEFAULT_DOCUMENT_PATH, timeout != null ? timeout : DEFAULT_TIMEOUT_MS);
|
|
2506
|
+
}
|
|
2507
|
+
static async verifyConnection(keyStore, logger) {
|
|
2508
|
+
try {
|
|
2509
|
+
await keyStore.verify();
|
|
2510
|
+
} catch (error) {
|
|
2511
|
+
if (process.env.NODE_ENV !== "development") {
|
|
2512
|
+
throw new Error(`Failed to connect to database: ${error.message}`);
|
|
2513
|
+
}
|
|
2514
|
+
logger == null ? void 0 : logger.warn(`Failed to connect to database: ${error.message}`);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
async addKey(key) {
|
|
2518
|
+
await this.withTimeout(this.database.collection(this.path).doc(key.kid).set({
|
|
2519
|
+
kid: key.kid,
|
|
2520
|
+
key: JSON.stringify(key)
|
|
2521
|
+
}));
|
|
2522
|
+
}
|
|
2523
|
+
async listKeys() {
|
|
2524
|
+
const keys = await this.withTimeout(this.database.collection(this.path).get());
|
|
2525
|
+
return {
|
|
2526
|
+
items: keys.docs.map((key) => ({
|
|
2527
|
+
key: key.data(),
|
|
2528
|
+
createdAt: key.createTime.toDate()
|
|
2529
|
+
}))
|
|
2530
|
+
};
|
|
2531
|
+
}
|
|
2532
|
+
async removeKeys(kids) {
|
|
2533
|
+
for (const kid of kids) {
|
|
2534
|
+
await this.withTimeout(this.database.collection(this.path).doc(kid).delete());
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
async withTimeout(operation) {
|
|
2538
|
+
const timer = new Promise((_, reject) => setTimeout(() => {
|
|
2539
|
+
reject(new Error(`Operation timed out after ${this.timeout}ms`));
|
|
2540
|
+
}, this.timeout));
|
|
2541
|
+
return Promise.race([operation, timer]);
|
|
2542
|
+
}
|
|
2543
|
+
async verify() {
|
|
2544
|
+
await this.withTimeout(this.database.collection(this.path).limit(1).get());
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
class KeyStores {
|
|
2549
|
+
static async fromConfig(config, options) {
|
|
2550
|
+
var _a;
|
|
2551
|
+
const {logger, database} = options != null ? options : {};
|
|
2552
|
+
const ks = config.getOptionalConfig("auth.keyStore");
|
|
2553
|
+
const provider = (_a = ks == null ? void 0 : ks.getOptionalString("provider")) != null ? _a : "database";
|
|
2554
|
+
logger == null ? void 0 : logger.info(`Configuring "${provider}" as KeyStore provider`);
|
|
2555
|
+
if (provider === "database") {
|
|
2556
|
+
if (!database) {
|
|
2557
|
+
throw new Error("This KeyStore provider requires a database");
|
|
2558
|
+
}
|
|
2559
|
+
return await DatabaseKeyStore.create({
|
|
2560
|
+
database: await database.getClient()
|
|
2561
|
+
});
|
|
2562
|
+
}
|
|
2563
|
+
if (provider === "memory") {
|
|
2564
|
+
return new MemoryKeyStore();
|
|
2565
|
+
}
|
|
2566
|
+
if (provider === "firestore") {
|
|
2567
|
+
const settings = ks == null ? void 0 : ks.getConfig(provider);
|
|
2568
|
+
const keyStore = await FirestoreKeyStore.create({
|
|
2569
|
+
projectId: settings == null ? void 0 : settings.getOptionalString("projectId"),
|
|
2570
|
+
keyFilename: settings == null ? void 0 : settings.getOptionalString("keyFilename"),
|
|
2571
|
+
host: settings == null ? void 0 : settings.getOptionalString("host"),
|
|
2572
|
+
port: settings == null ? void 0 : settings.getOptionalNumber("port"),
|
|
2573
|
+
ssl: settings == null ? void 0 : settings.getOptionalBoolean("ssl"),
|
|
2574
|
+
path: settings == null ? void 0 : settings.getOptionalString("path"),
|
|
2575
|
+
timeout: settings == null ? void 0 : settings.getOptionalNumber("timeout")
|
|
2576
|
+
});
|
|
2577
|
+
await FirestoreKeyStore.verifyConnection(keyStore, logger);
|
|
2578
|
+
return keyStore;
|
|
2579
|
+
}
|
|
2580
|
+
throw new Error(`Unknown KeyStore provider: ${provider}`);
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2087
2584
|
async function createRouter({
|
|
2088
2585
|
logger,
|
|
2089
2586
|
config,
|
|
@@ -2094,10 +2591,8 @@ async function createRouter({
|
|
|
2094
2591
|
const router = Router__default['default']();
|
|
2095
2592
|
const appUrl = config.getString("app.baseUrl");
|
|
2096
2593
|
const authUrl = await discovery.getExternalBaseUrl("auth");
|
|
2594
|
+
const keyStore = await KeyStores.fromConfig(config, {logger, database});
|
|
2097
2595
|
const keyDurationSeconds = 3600;
|
|
2098
|
-
const keyStore = await DatabaseKeyStore.create({
|
|
2099
|
-
database: await database.getClient()
|
|
2100
|
-
});
|
|
2101
2596
|
const tokenIssuer = new TokenFactory({
|
|
2102
2597
|
issuer: authUrl,
|
|
2103
2598
|
keyStore,
|
|
@@ -2148,6 +2643,7 @@ async function createRouter({
|
|
|
2148
2643
|
}
|
|
2149
2644
|
router.use(`/${providerId}`, r);
|
|
2150
2645
|
} catch (e) {
|
|
2646
|
+
errors.assertError(e);
|
|
2151
2647
|
if (process.env.NODE_ENV !== "development") {
|
|
2152
2648
|
throw new Error(`Failed to initialize ${providerId} auth provider, ${e.message}`);
|
|
2153
2649
|
}
|
|
@@ -2189,6 +2685,11 @@ function createOriginFilter(config) {
|
|
|
2189
2685
|
exports.IdentityClient = IdentityClient;
|
|
2190
2686
|
exports.OAuthAdapter = OAuthAdapter;
|
|
2191
2687
|
exports.OAuthEnvironmentHandler = OAuthEnvironmentHandler;
|
|
2688
|
+
exports.bitbucketUserIdSignInResolver = bitbucketUserIdSignInResolver;
|
|
2689
|
+
exports.bitbucketUsernameSignInResolver = bitbucketUsernameSignInResolver;
|
|
2690
|
+
exports.createAtlassianProvider = createAtlassianProvider;
|
|
2691
|
+
exports.createAwsAlbProvider = createAwsAlbProvider;
|
|
2692
|
+
exports.createBitbucketProvider = createBitbucketProvider;
|
|
2192
2693
|
exports.createGithubProvider = createGithubProvider;
|
|
2193
2694
|
exports.createGitlabProvider = createGitlabProvider;
|
|
2194
2695
|
exports.createGoogleProvider = createGoogleProvider;
|