@backstage/plugin-auth-backend 0.4.3 → 0.4.7
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 +56 -0
- package/config.d.ts +29 -0
- package/dist/index.cjs.js +450 -91
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +180 -108
- package/package.json +11 -10
package/dist/index.cjs.js
CHANGED
|
@@ -19,16 +19,18 @@ var got = require('got');
|
|
|
19
19
|
var OAuth2Strategy = require('passport-oauth2');
|
|
20
20
|
var passportOktaOauth = require('passport-okta-oauth');
|
|
21
21
|
var passportBitbucketOauth2 = require('passport-bitbucket-oauth2');
|
|
22
|
-
var openidClient = require('openid-client');
|
|
23
|
-
var passportSaml = require('passport-saml');
|
|
24
|
-
var passportOneloginOauth = require('passport-onelogin-oauth');
|
|
25
22
|
var fetch = require('cross-fetch');
|
|
26
23
|
var NodeCache = require('node-cache');
|
|
27
24
|
var jose = require('jose');
|
|
25
|
+
var openidClient = require('openid-client');
|
|
26
|
+
var passportSaml = require('passport-saml');
|
|
27
|
+
var passportOneloginOauth = require('passport-onelogin-oauth');
|
|
28
28
|
var catalogClient = require('@backstage/catalog-client');
|
|
29
29
|
var uuid = require('uuid');
|
|
30
30
|
var luxon = require('luxon');
|
|
31
31
|
var backendCommon = require('@backstage/backend-common');
|
|
32
|
+
var firestore = require('@google-cloud/firestore');
|
|
33
|
+
var lodash = require('lodash');
|
|
32
34
|
var session = require('express-session');
|
|
33
35
|
var passport = require('passport');
|
|
34
36
|
var minimatch = require('minimatch');
|
|
@@ -417,12 +419,10 @@ class OAuthAdapter {
|
|
|
417
419
|
response
|
|
418
420
|
});
|
|
419
421
|
} catch (error) {
|
|
422
|
+
const {name, message} = errors.isError(error) ? error : new Error("Encountered invalid error");
|
|
420
423
|
return postMessageResponse(res, appOrigin, {
|
|
421
424
|
type: "authorization_response",
|
|
422
|
-
error: {
|
|
423
|
-
name: error.name,
|
|
424
|
-
message: error.message
|
|
425
|
-
}
|
|
425
|
+
error: {name, message}
|
|
426
426
|
});
|
|
427
427
|
}
|
|
428
428
|
}
|
|
@@ -458,7 +458,7 @@ class OAuthAdapter {
|
|
|
458
458
|
}
|
|
459
459
|
res.status(200).json(response);
|
|
460
460
|
} catch (error) {
|
|
461
|
-
res.status(401).send(
|
|
461
|
+
res.status(401).send(String(error));
|
|
462
462
|
}
|
|
463
463
|
}
|
|
464
464
|
async populateIdentity(identity) {
|
|
@@ -551,6 +551,7 @@ class GithubAuthProvider {
|
|
|
551
551
|
constructor(options) {
|
|
552
552
|
this.signInResolver = options.signInResolver;
|
|
553
553
|
this.authHandler = options.authHandler;
|
|
554
|
+
this.stateEncoder = options.stateEncoder;
|
|
554
555
|
this.tokenIssuer = options.tokenIssuer;
|
|
555
556
|
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
556
557
|
this.logger = options.logger;
|
|
@@ -568,7 +569,7 @@ class GithubAuthProvider {
|
|
|
568
569
|
async start(req) {
|
|
569
570
|
return await executeRedirectStrategy(req, this._strategy, {
|
|
570
571
|
scope: req.scope,
|
|
571
|
-
state:
|
|
572
|
+
state: (await this.stateEncoder(req)).encodedState
|
|
572
573
|
});
|
|
573
574
|
}
|
|
574
575
|
async handler(req) {
|
|
@@ -579,13 +580,17 @@ class GithubAuthProvider {
|
|
|
579
580
|
};
|
|
580
581
|
}
|
|
581
582
|
async refresh(req) {
|
|
582
|
-
const {
|
|
583
|
+
const {
|
|
584
|
+
accessToken,
|
|
585
|
+
refreshToken: newRefreshToken,
|
|
586
|
+
params
|
|
587
|
+
} = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
|
|
583
588
|
const fullProfile = await executeFetchUserProfileStrategy(this._strategy, accessToken);
|
|
584
589
|
return this.handleResult({
|
|
585
590
|
fullProfile,
|
|
586
591
|
params,
|
|
587
592
|
accessToken,
|
|
588
|
-
refreshToken:
|
|
593
|
+
refreshToken: newRefreshToken
|
|
589
594
|
});
|
|
590
595
|
}
|
|
591
596
|
async handleResult(result) {
|
|
@@ -594,6 +599,7 @@ class GithubAuthProvider {
|
|
|
594
599
|
const response = {
|
|
595
600
|
providerInfo: {
|
|
596
601
|
accessToken: result.accessToken,
|
|
602
|
+
refreshToken: result.refreshToken,
|
|
597
603
|
scope: result.params.scope,
|
|
598
604
|
expiresInSeconds: expiresInStr === void 0 ? void 0 : Number(expiresInStr)
|
|
599
605
|
},
|
|
@@ -629,7 +635,7 @@ const createGithubProvider = (options) => {
|
|
|
629
635
|
catalogApi,
|
|
630
636
|
logger
|
|
631
637
|
}) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
|
|
632
|
-
var _a, _b;
|
|
638
|
+
var _a, _b, _c;
|
|
633
639
|
const clientId = envConfig.getString("clientId");
|
|
634
640
|
const clientSecret = envConfig.getString("clientSecret");
|
|
635
641
|
const enterpriseInstanceUrl = envConfig.getOptionalString("enterpriseInstanceUrl");
|
|
@@ -651,6 +657,9 @@ const createGithubProvider = (options) => {
|
|
|
651
657
|
tokenIssuer,
|
|
652
658
|
logger
|
|
653
659
|
});
|
|
660
|
+
const stateEncoder = (_c = options == null ? void 0 : options.stateEncoder) != null ? _c : async (req) => {
|
|
661
|
+
return {encodedState: encodeState(req.state)};
|
|
662
|
+
};
|
|
654
663
|
const provider = new GithubAuthProvider({
|
|
655
664
|
clientId,
|
|
656
665
|
clientSecret,
|
|
@@ -662,6 +671,7 @@ const createGithubProvider = (options) => {
|
|
|
662
671
|
authHandler,
|
|
663
672
|
tokenIssuer,
|
|
664
673
|
catalogIdentityClient,
|
|
674
|
+
stateEncoder,
|
|
665
675
|
logger
|
|
666
676
|
});
|
|
667
677
|
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
@@ -1379,6 +1389,9 @@ const createOktaProvider = (_options) => {
|
|
|
1379
1389
|
const clientSecret = envConfig.getString("clientSecret");
|
|
1380
1390
|
const audience = envConfig.getString("audience");
|
|
1381
1391
|
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
1392
|
+
if (!audience.startsWith("https://")) {
|
|
1393
|
+
throw new Error("URL for 'audience' must start with 'https://'.");
|
|
1394
|
+
}
|
|
1382
1395
|
const catalogIdentityClient = new CatalogIdentityClient({
|
|
1383
1396
|
catalogApi,
|
|
1384
1397
|
tokenIssuer
|
|
@@ -1550,6 +1563,305 @@ const createBitbucketProvider = (options) => {
|
|
|
1550
1563
|
});
|
|
1551
1564
|
};
|
|
1552
1565
|
|
|
1566
|
+
const defaultScopes = ["offline_access", "read:me"];
|
|
1567
|
+
class AtlassianStrategy extends OAuth2Strategy__default['default'] {
|
|
1568
|
+
constructor(options, verify) {
|
|
1569
|
+
if (!options.scope) {
|
|
1570
|
+
throw new TypeError("Atlassian requires a scope option");
|
|
1571
|
+
}
|
|
1572
|
+
const scopes = options.scope.split(" ");
|
|
1573
|
+
const optionsWithURLs = {
|
|
1574
|
+
...options,
|
|
1575
|
+
authorizationURL: `https://auth.atlassian.com/authorize`,
|
|
1576
|
+
tokenURL: `https://auth.atlassian.com/oauth/token`,
|
|
1577
|
+
scope: Array.from(new Set([...defaultScopes, ...scopes]))
|
|
1578
|
+
};
|
|
1579
|
+
super(optionsWithURLs, verify);
|
|
1580
|
+
this.profileURL = "https://api.atlassian.com/me";
|
|
1581
|
+
this.name = "atlassian";
|
|
1582
|
+
this._oauth2.useAuthorizationHeaderforGET(true);
|
|
1583
|
+
}
|
|
1584
|
+
authorizationParams() {
|
|
1585
|
+
return {
|
|
1586
|
+
audience: "api.atlassian.com",
|
|
1587
|
+
prompt: "consent"
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
userProfile(accessToken, done) {
|
|
1591
|
+
this._oauth2.get(this.profileURL, accessToken, (err, body) => {
|
|
1592
|
+
if (err) {
|
|
1593
|
+
return done(new OAuth2Strategy.InternalOAuthError("Failed to fetch user profile", err.statusCode));
|
|
1594
|
+
}
|
|
1595
|
+
if (!body) {
|
|
1596
|
+
return done(new Error("Failed to fetch user profile, body cannot be empty"));
|
|
1597
|
+
}
|
|
1598
|
+
try {
|
|
1599
|
+
const json = typeof body !== "string" ? body.toString() : body;
|
|
1600
|
+
const profile = AtlassianStrategy.parse(json);
|
|
1601
|
+
return done(null, profile);
|
|
1602
|
+
} catch (e) {
|
|
1603
|
+
return done(new Error("Failed to parse user profile"));
|
|
1604
|
+
}
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
static parse(json) {
|
|
1608
|
+
const resp = JSON.parse(json);
|
|
1609
|
+
return {
|
|
1610
|
+
id: resp.account_id,
|
|
1611
|
+
provider: "atlassian",
|
|
1612
|
+
username: resp.nickname,
|
|
1613
|
+
displayName: resp.name,
|
|
1614
|
+
emails: [{value: resp.email}],
|
|
1615
|
+
photos: [{value: resp.picture}]
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
const atlassianDefaultAuthHandler = async ({
|
|
1621
|
+
fullProfile,
|
|
1622
|
+
params
|
|
1623
|
+
}) => ({
|
|
1624
|
+
profile: makeProfileInfo(fullProfile, params.id_token)
|
|
1625
|
+
});
|
|
1626
|
+
class AtlassianAuthProvider {
|
|
1627
|
+
constructor(options) {
|
|
1628
|
+
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
1629
|
+
this.logger = options.logger;
|
|
1630
|
+
this.tokenIssuer = options.tokenIssuer;
|
|
1631
|
+
this.authHandler = options.authHandler;
|
|
1632
|
+
this.signInResolver = options.signInResolver;
|
|
1633
|
+
this._strategy = new AtlassianStrategy({
|
|
1634
|
+
clientID: options.clientId,
|
|
1635
|
+
clientSecret: options.clientSecret,
|
|
1636
|
+
callbackURL: options.callbackUrl,
|
|
1637
|
+
scope: options.scopes
|
|
1638
|
+
}, (accessToken, refreshToken, params, fullProfile, done) => {
|
|
1639
|
+
done(void 0, {
|
|
1640
|
+
fullProfile,
|
|
1641
|
+
accessToken,
|
|
1642
|
+
refreshToken,
|
|
1643
|
+
params
|
|
1644
|
+
});
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
async start(req) {
|
|
1648
|
+
return await executeRedirectStrategy(req, this._strategy, {
|
|
1649
|
+
state: encodeState(req.state)
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
async handler(req) {
|
|
1653
|
+
var _a;
|
|
1654
|
+
const {result} = await executeFrameHandlerStrategy(req, this._strategy);
|
|
1655
|
+
return {
|
|
1656
|
+
response: await this.handleResult(result),
|
|
1657
|
+
refreshToken: (_a = result.refreshToken) != null ? _a : ""
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
async handleResult(result) {
|
|
1661
|
+
const {profile} = await this.authHandler(result);
|
|
1662
|
+
const response = {
|
|
1663
|
+
providerInfo: {
|
|
1664
|
+
idToken: result.params.id_token,
|
|
1665
|
+
accessToken: result.accessToken,
|
|
1666
|
+
refreshToken: result.refreshToken,
|
|
1667
|
+
scope: result.params.scope,
|
|
1668
|
+
expiresInSeconds: result.params.expires_in
|
|
1669
|
+
},
|
|
1670
|
+
profile
|
|
1671
|
+
};
|
|
1672
|
+
if (this.signInResolver) {
|
|
1673
|
+
response.backstageIdentity = await this.signInResolver({
|
|
1674
|
+
result,
|
|
1675
|
+
profile
|
|
1676
|
+
}, {
|
|
1677
|
+
tokenIssuer: this.tokenIssuer,
|
|
1678
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
1679
|
+
logger: this.logger
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
return response;
|
|
1683
|
+
}
|
|
1684
|
+
async refresh(req) {
|
|
1685
|
+
const {
|
|
1686
|
+
accessToken,
|
|
1687
|
+
params,
|
|
1688
|
+
refreshToken: newRefreshToken
|
|
1689
|
+
} = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
|
|
1690
|
+
const fullProfile = await executeFetchUserProfileStrategy(this._strategy, accessToken);
|
|
1691
|
+
return this.handleResult({
|
|
1692
|
+
fullProfile,
|
|
1693
|
+
params,
|
|
1694
|
+
accessToken,
|
|
1695
|
+
refreshToken: newRefreshToken
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
const createAtlassianProvider = (options) => {
|
|
1700
|
+
return ({
|
|
1701
|
+
providerId,
|
|
1702
|
+
globalConfig,
|
|
1703
|
+
config,
|
|
1704
|
+
tokenIssuer,
|
|
1705
|
+
catalogApi,
|
|
1706
|
+
logger
|
|
1707
|
+
}) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
|
|
1708
|
+
var _a, _b;
|
|
1709
|
+
const clientId = envConfig.getString("clientId");
|
|
1710
|
+
const clientSecret = envConfig.getString("clientSecret");
|
|
1711
|
+
const scopes = envConfig.getString("scopes");
|
|
1712
|
+
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
|
|
1713
|
+
const catalogIdentityClient = new CatalogIdentityClient({
|
|
1714
|
+
catalogApi,
|
|
1715
|
+
tokenIssuer
|
|
1716
|
+
});
|
|
1717
|
+
const authHandler = (_a = options == null ? void 0 : options.authHandler) != null ? _a : atlassianDefaultAuthHandler;
|
|
1718
|
+
const provider = new AtlassianAuthProvider({
|
|
1719
|
+
clientId,
|
|
1720
|
+
clientSecret,
|
|
1721
|
+
scopes,
|
|
1722
|
+
callbackUrl,
|
|
1723
|
+
authHandler,
|
|
1724
|
+
signInResolver: (_b = options == null ? void 0 : options.signIn) == null ? void 0 : _b.resolver,
|
|
1725
|
+
catalogIdentityClient,
|
|
1726
|
+
logger,
|
|
1727
|
+
tokenIssuer
|
|
1728
|
+
});
|
|
1729
|
+
return OAuthAdapter.fromConfig(globalConfig, provider, {
|
|
1730
|
+
disableRefresh: true,
|
|
1731
|
+
providerId,
|
|
1732
|
+
tokenIssuer
|
|
1733
|
+
});
|
|
1734
|
+
});
|
|
1735
|
+
};
|
|
1736
|
+
|
|
1737
|
+
const ALB_JWT_HEADER = "x-amzn-oidc-data";
|
|
1738
|
+
const ALB_ACCESSTOKEN_HEADER = "x-amzn-oidc-accesstoken";
|
|
1739
|
+
const getJWTHeaders = (input) => {
|
|
1740
|
+
const encoded = input.split(".")[0];
|
|
1741
|
+
return JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
|
|
1742
|
+
};
|
|
1743
|
+
class AwsAlbAuthProvider {
|
|
1744
|
+
constructor(options) {
|
|
1745
|
+
this.region = options.region;
|
|
1746
|
+
this.issuer = options.issuer;
|
|
1747
|
+
this.authHandler = options.authHandler;
|
|
1748
|
+
this.signInResolver = options.signInResolver;
|
|
1749
|
+
this.tokenIssuer = options.tokenIssuer;
|
|
1750
|
+
this.catalogIdentityClient = options.catalogIdentityClient;
|
|
1751
|
+
this.logger = options.logger;
|
|
1752
|
+
this.keyCache = new NodeCache__default['default']({stdTTL: 3600});
|
|
1753
|
+
}
|
|
1754
|
+
frameHandler() {
|
|
1755
|
+
return Promise.resolve(void 0);
|
|
1756
|
+
}
|
|
1757
|
+
async refresh(req, res) {
|
|
1758
|
+
try {
|
|
1759
|
+
const result = await this.getResult(req);
|
|
1760
|
+
const response = await this.handleResult(result);
|
|
1761
|
+
res.json(response);
|
|
1762
|
+
} catch (e) {
|
|
1763
|
+
this.logger.error("Exception occurred during AWS ALB token refresh", e);
|
|
1764
|
+
res.status(401);
|
|
1765
|
+
res.end();
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
start() {
|
|
1769
|
+
return Promise.resolve(void 0);
|
|
1770
|
+
}
|
|
1771
|
+
async getResult(req) {
|
|
1772
|
+
const jwt = req.header(ALB_JWT_HEADER);
|
|
1773
|
+
const accessToken = req.header(ALB_ACCESSTOKEN_HEADER);
|
|
1774
|
+
if (jwt === void 0) {
|
|
1775
|
+
throw new errors.AuthenticationError(`Missing ALB OIDC header: ${ALB_JWT_HEADER}`);
|
|
1776
|
+
}
|
|
1777
|
+
if (accessToken === void 0) {
|
|
1778
|
+
throw new errors.AuthenticationError(`Missing ALB OIDC header: ${ALB_ACCESSTOKEN_HEADER}`);
|
|
1779
|
+
}
|
|
1780
|
+
try {
|
|
1781
|
+
const headers = getJWTHeaders(jwt);
|
|
1782
|
+
const key = await this.getKey(headers.kid);
|
|
1783
|
+
const claims = jose.JWT.verify(jwt, key);
|
|
1784
|
+
if (this.issuer && claims.iss !== this.issuer) {
|
|
1785
|
+
throw new errors.AuthenticationError("Issuer mismatch on JWT token");
|
|
1786
|
+
}
|
|
1787
|
+
const fullProfile = {
|
|
1788
|
+
provider: "unknown",
|
|
1789
|
+
id: claims.sub,
|
|
1790
|
+
displayName: claims.name,
|
|
1791
|
+
username: claims.email.split("@")[0].toLowerCase(),
|
|
1792
|
+
name: {
|
|
1793
|
+
familyName: claims.family_name,
|
|
1794
|
+
givenName: claims.given_name
|
|
1795
|
+
},
|
|
1796
|
+
emails: [{value: claims.email.toLowerCase()}],
|
|
1797
|
+
photos: [{value: claims.picture}]
|
|
1798
|
+
};
|
|
1799
|
+
return {
|
|
1800
|
+
fullProfile,
|
|
1801
|
+
expiresInSeconds: claims.exp,
|
|
1802
|
+
accessToken
|
|
1803
|
+
};
|
|
1804
|
+
} catch (e) {
|
|
1805
|
+
throw new Error(`Exception occurred during JWT processing: ${e}`);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
async handleResult(result) {
|
|
1809
|
+
const {profile} = await this.authHandler(result);
|
|
1810
|
+
const backstageIdentity = await this.signInResolver({
|
|
1811
|
+
result,
|
|
1812
|
+
profile
|
|
1813
|
+
}, {
|
|
1814
|
+
tokenIssuer: this.tokenIssuer,
|
|
1815
|
+
catalogIdentityClient: this.catalogIdentityClient,
|
|
1816
|
+
logger: this.logger
|
|
1817
|
+
});
|
|
1818
|
+
return {
|
|
1819
|
+
providerInfo: {
|
|
1820
|
+
accessToken: result.accessToken,
|
|
1821
|
+
expiresInSeconds: result.expiresInSeconds
|
|
1822
|
+
},
|
|
1823
|
+
backstageIdentity,
|
|
1824
|
+
profile
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
async getKey(keyId) {
|
|
1828
|
+
const optionalCacheKey = this.keyCache.get(keyId);
|
|
1829
|
+
if (optionalCacheKey) {
|
|
1830
|
+
return crypto__namespace.createPublicKey(optionalCacheKey);
|
|
1831
|
+
}
|
|
1832
|
+
const keyText = await fetch__default['default'](`https://public-keys.auth.elb.${this.region}.amazonaws.com/${keyId}`).then((response) => response.text());
|
|
1833
|
+
const keyValue = crypto__namespace.createPublicKey(keyText);
|
|
1834
|
+
this.keyCache.set(keyId, keyValue.export({format: "pem", type: "spki"}));
|
|
1835
|
+
return keyValue;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
const createAwsAlbProvider = (options) => {
|
|
1839
|
+
return ({config, tokenIssuer, catalogApi, logger}) => {
|
|
1840
|
+
const region = config.getString("region");
|
|
1841
|
+
const issuer = config.getOptionalString("iss");
|
|
1842
|
+
if ((options == null ? void 0 : options.signIn.resolver) === void 0) {
|
|
1843
|
+
throw new Error("SignInResolver is required to use this authentication provider");
|
|
1844
|
+
}
|
|
1845
|
+
const catalogIdentityClient = new CatalogIdentityClient({
|
|
1846
|
+
catalogApi,
|
|
1847
|
+
tokenIssuer
|
|
1848
|
+
});
|
|
1849
|
+
const authHandler = (options == null ? void 0 : options.authHandler) ? options.authHandler : async ({fullProfile}) => ({
|
|
1850
|
+
profile: makeProfileInfo(fullProfile)
|
|
1851
|
+
});
|
|
1852
|
+
const signInResolver = options == null ? void 0 : options.signIn.resolver;
|
|
1853
|
+
return new AwsAlbAuthProvider({
|
|
1854
|
+
region,
|
|
1855
|
+
issuer,
|
|
1856
|
+
signInResolver,
|
|
1857
|
+
authHandler,
|
|
1858
|
+
tokenIssuer,
|
|
1859
|
+
catalogIdentityClient,
|
|
1860
|
+
logger
|
|
1861
|
+
});
|
|
1862
|
+
};
|
|
1863
|
+
};
|
|
1864
|
+
|
|
1553
1865
|
class OidcAuthProvider {
|
|
1554
1866
|
constructor(options) {
|
|
1555
1867
|
this.implementation = this.setupStrategy(options);
|
|
@@ -1702,12 +2014,10 @@ class SamlAuthProvider {
|
|
|
1702
2014
|
}
|
|
1703
2015
|
});
|
|
1704
2016
|
} catch (error) {
|
|
2017
|
+
const {name, message} = errors.isError(error) ? error : new Error("Encountered invalid error");
|
|
1705
2018
|
return postMessageResponse(res, this.appUrl, {
|
|
1706
2019
|
type: "authorization_response",
|
|
1707
|
-
error: {
|
|
1708
|
-
name: error.name,
|
|
1709
|
-
message: error.message
|
|
1710
|
-
}
|
|
2020
|
+
error: {name, message}
|
|
1711
2021
|
});
|
|
1712
2022
|
}
|
|
1713
2023
|
}
|
|
@@ -1724,9 +2034,12 @@ const createSamlProvider = (_options) => {
|
|
|
1724
2034
|
callbackUrl: `${globalConfig.baseUrl}/${providerId}/handler/frame`,
|
|
1725
2035
|
entryPoint: config.getString("entryPoint"),
|
|
1726
2036
|
logoutUrl: config.getOptionalString("logoutUrl"),
|
|
2037
|
+
audience: config.getOptionalString("audience"),
|
|
1727
2038
|
issuer: config.getString("issuer"),
|
|
1728
2039
|
cert: config.getString("cert"),
|
|
1729
2040
|
privateCert: config.getOptionalString("privateKey"),
|
|
2041
|
+
authnContext: config.getOptionalStringArray("authnContext"),
|
|
2042
|
+
identifierFormat: config.getOptionalString("identifierFormat"),
|
|
1730
2043
|
decryptionPvk: config.getOptionalString("decryptionPvk"),
|
|
1731
2044
|
signatureAlgorithm: config.getOptionalString("signatureAlgorithm"),
|
|
1732
2045
|
digestAlgorithm: config.getOptionalString("digestAlgorithm"),
|
|
@@ -1923,77 +2236,6 @@ const createOneLoginProvider = (_options) => {
|
|
|
1923
2236
|
});
|
|
1924
2237
|
};
|
|
1925
2238
|
|
|
1926
|
-
const ALB_JWT_HEADER = "x-amzn-oidc-data";
|
|
1927
|
-
const getJWTHeaders = (input) => {
|
|
1928
|
-
const encoded = input.split(".")[0];
|
|
1929
|
-
return JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
|
|
1930
|
-
};
|
|
1931
|
-
class AwsAlbAuthProvider {
|
|
1932
|
-
constructor(logger, catalogClient, options) {
|
|
1933
|
-
this.logger = logger;
|
|
1934
|
-
this.catalogClient = catalogClient;
|
|
1935
|
-
this.options = options;
|
|
1936
|
-
this.keyCache = new NodeCache__default['default']({stdTTL: 3600});
|
|
1937
|
-
}
|
|
1938
|
-
frameHandler() {
|
|
1939
|
-
return Promise.resolve(void 0);
|
|
1940
|
-
}
|
|
1941
|
-
async refresh(req, res) {
|
|
1942
|
-
const jwt = req.header(ALB_JWT_HEADER);
|
|
1943
|
-
if (jwt !== void 0) {
|
|
1944
|
-
try {
|
|
1945
|
-
const headers = getJWTHeaders(jwt);
|
|
1946
|
-
const key = await this.getKey(headers.kid);
|
|
1947
|
-
const payload = jose.JWT.verify(jwt, key);
|
|
1948
|
-
if (this.options.issuer && headers.iss !== this.options.issuer) {
|
|
1949
|
-
throw new Error("issuer mismatch on JWT");
|
|
1950
|
-
}
|
|
1951
|
-
const resolvedEntity = await this.options.identityResolutionCallback(payload, this.catalogClient);
|
|
1952
|
-
res.json(resolvedEntity);
|
|
1953
|
-
} catch (e) {
|
|
1954
|
-
this.logger.error("exception occurred during JWT processing", e);
|
|
1955
|
-
res.status(401);
|
|
1956
|
-
res.end();
|
|
1957
|
-
}
|
|
1958
|
-
} else {
|
|
1959
|
-
res.status(401);
|
|
1960
|
-
res.end();
|
|
1961
|
-
}
|
|
1962
|
-
}
|
|
1963
|
-
start() {
|
|
1964
|
-
return Promise.resolve(void 0);
|
|
1965
|
-
}
|
|
1966
|
-
async getKey(keyId) {
|
|
1967
|
-
const optionalCacheKey = this.keyCache.get(keyId);
|
|
1968
|
-
if (optionalCacheKey) {
|
|
1969
|
-
return crypto__namespace.createPublicKey(optionalCacheKey);
|
|
1970
|
-
}
|
|
1971
|
-
const keyText = await fetch__default['default'](`https://public-keys.auth.elb.${this.options.region}.amazonaws.com/${keyId}`).then((response) => response.text());
|
|
1972
|
-
const keyValue = crypto__namespace.createPublicKey(keyText);
|
|
1973
|
-
this.keyCache.set(keyId, keyValue.export({format: "pem", type: "spki"}));
|
|
1974
|
-
return keyValue;
|
|
1975
|
-
}
|
|
1976
|
-
}
|
|
1977
|
-
const createAwsAlbProvider = (_options) => {
|
|
1978
|
-
return ({
|
|
1979
|
-
logger,
|
|
1980
|
-
catalogApi,
|
|
1981
|
-
config,
|
|
1982
|
-
identityResolver
|
|
1983
|
-
}) => {
|
|
1984
|
-
const region = config.getString("region");
|
|
1985
|
-
const issuer = config.getOptionalString("iss");
|
|
1986
|
-
if (identityResolver !== void 0) {
|
|
1987
|
-
return new AwsAlbAuthProvider(logger, catalogApi, {
|
|
1988
|
-
region,
|
|
1989
|
-
issuer,
|
|
1990
|
-
identityResolutionCallback: identityResolver
|
|
1991
|
-
});
|
|
1992
|
-
}
|
|
1993
|
-
throw new Error("Identity resolver is required to use this authentication provider");
|
|
1994
|
-
};
|
|
1995
|
-
};
|
|
1996
|
-
|
|
1997
2239
|
const factories = {
|
|
1998
2240
|
google: createGoogleProvider(),
|
|
1999
2241
|
github: createGithubProvider(),
|
|
@@ -2006,7 +2248,8 @@ const factories = {
|
|
|
2006
2248
|
oidc: createOidcProvider(),
|
|
2007
2249
|
onelogin: createOneLoginProvider(),
|
|
2008
2250
|
awsalb: createAwsAlbProvider(),
|
|
2009
|
-
bitbucket: createBitbucketProvider()
|
|
2251
|
+
bitbucket: createBitbucketProvider(),
|
|
2252
|
+
atlassian: createAtlassianProvider()
|
|
2010
2253
|
};
|
|
2011
2254
|
|
|
2012
2255
|
function createOidcRouter(options) {
|
|
@@ -2225,6 +2468,121 @@ class DatabaseKeyStore {
|
|
|
2225
2468
|
}
|
|
2226
2469
|
}
|
|
2227
2470
|
|
|
2471
|
+
class MemoryKeyStore {
|
|
2472
|
+
constructor() {
|
|
2473
|
+
this.keys = new Map();
|
|
2474
|
+
}
|
|
2475
|
+
async addKey(key) {
|
|
2476
|
+
this.keys.set(key.kid, {
|
|
2477
|
+
createdAt: luxon.DateTime.utc().toJSDate(),
|
|
2478
|
+
key: JSON.stringify(key)
|
|
2479
|
+
});
|
|
2480
|
+
}
|
|
2481
|
+
async removeKeys(kids) {
|
|
2482
|
+
for (const kid of kids) {
|
|
2483
|
+
this.keys.delete(kid);
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
async listKeys() {
|
|
2487
|
+
return {
|
|
2488
|
+
items: Array.from(this.keys).map(([, {createdAt, key: keyStr}]) => ({
|
|
2489
|
+
createdAt,
|
|
2490
|
+
key: JSON.parse(keyStr)
|
|
2491
|
+
}))
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
2497
|
+
const DEFAULT_DOCUMENT_PATH = "sessions";
|
|
2498
|
+
class FirestoreKeyStore {
|
|
2499
|
+
constructor(database, path, timeout) {
|
|
2500
|
+
this.database = database;
|
|
2501
|
+
this.path = path;
|
|
2502
|
+
this.timeout = timeout;
|
|
2503
|
+
}
|
|
2504
|
+
static async create(settings) {
|
|
2505
|
+
const {path, timeout, ...firestoreSettings} = settings != null ? settings : {};
|
|
2506
|
+
const database = new firestore.Firestore(firestoreSettings);
|
|
2507
|
+
return new FirestoreKeyStore(database, path != null ? path : DEFAULT_DOCUMENT_PATH, timeout != null ? timeout : DEFAULT_TIMEOUT_MS);
|
|
2508
|
+
}
|
|
2509
|
+
static async verifyConnection(keyStore, logger) {
|
|
2510
|
+
try {
|
|
2511
|
+
await keyStore.verify();
|
|
2512
|
+
} catch (error) {
|
|
2513
|
+
if (process.env.NODE_ENV !== "development") {
|
|
2514
|
+
throw new Error(`Failed to connect to database: ${error.message}`);
|
|
2515
|
+
}
|
|
2516
|
+
logger == null ? void 0 : logger.warn(`Failed to connect to database: ${error.message}`);
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
async addKey(key) {
|
|
2520
|
+
await this.withTimeout(this.database.collection(this.path).doc(key.kid).set({
|
|
2521
|
+
kid: key.kid,
|
|
2522
|
+
key: JSON.stringify(key)
|
|
2523
|
+
}));
|
|
2524
|
+
}
|
|
2525
|
+
async listKeys() {
|
|
2526
|
+
const keys = await this.withTimeout(this.database.collection(this.path).get());
|
|
2527
|
+
return {
|
|
2528
|
+
items: keys.docs.map((key) => ({
|
|
2529
|
+
key: key.data(),
|
|
2530
|
+
createdAt: key.createTime.toDate()
|
|
2531
|
+
}))
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2534
|
+
async removeKeys(kids) {
|
|
2535
|
+
for (const kid of kids) {
|
|
2536
|
+
await this.withTimeout(this.database.collection(this.path).doc(kid).delete());
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
async withTimeout(operation) {
|
|
2540
|
+
const timer = new Promise((_, reject) => setTimeout(() => {
|
|
2541
|
+
reject(new Error(`Operation timed out after ${this.timeout}ms`));
|
|
2542
|
+
}, this.timeout));
|
|
2543
|
+
return Promise.race([operation, timer]);
|
|
2544
|
+
}
|
|
2545
|
+
async verify() {
|
|
2546
|
+
await this.withTimeout(this.database.collection(this.path).limit(1).get());
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
class KeyStores {
|
|
2551
|
+
static async fromConfig(config, options) {
|
|
2552
|
+
var _a;
|
|
2553
|
+
const {logger, database} = options != null ? options : {};
|
|
2554
|
+
const ks = config.getOptionalConfig("auth.keyStore");
|
|
2555
|
+
const provider = (_a = ks == null ? void 0 : ks.getOptionalString("provider")) != null ? _a : "database";
|
|
2556
|
+
logger == null ? void 0 : logger.info(`Configuring "${provider}" as KeyStore provider`);
|
|
2557
|
+
if (provider === "database") {
|
|
2558
|
+
if (!database) {
|
|
2559
|
+
throw new Error("This KeyStore provider requires a database");
|
|
2560
|
+
}
|
|
2561
|
+
return await DatabaseKeyStore.create({
|
|
2562
|
+
database: await database.getClient()
|
|
2563
|
+
});
|
|
2564
|
+
}
|
|
2565
|
+
if (provider === "memory") {
|
|
2566
|
+
return new MemoryKeyStore();
|
|
2567
|
+
}
|
|
2568
|
+
if (provider === "firestore") {
|
|
2569
|
+
const settings = ks == null ? void 0 : ks.getConfig(provider);
|
|
2570
|
+
const keyStore = await FirestoreKeyStore.create(lodash.pickBy({
|
|
2571
|
+
projectId: settings == null ? void 0 : settings.getOptionalString("projectId"),
|
|
2572
|
+
keyFilename: settings == null ? void 0 : settings.getOptionalString("keyFilename"),
|
|
2573
|
+
host: settings == null ? void 0 : settings.getOptionalString("host"),
|
|
2574
|
+
port: settings == null ? void 0 : settings.getOptionalNumber("port"),
|
|
2575
|
+
ssl: settings == null ? void 0 : settings.getOptionalBoolean("ssl"),
|
|
2576
|
+
path: settings == null ? void 0 : settings.getOptionalString("path"),
|
|
2577
|
+
timeout: settings == null ? void 0 : settings.getOptionalNumber("timeout")
|
|
2578
|
+
}, (value) => value !== void 0));
|
|
2579
|
+
await FirestoreKeyStore.verifyConnection(keyStore, logger);
|
|
2580
|
+
return keyStore;
|
|
2581
|
+
}
|
|
2582
|
+
throw new Error(`Unknown KeyStore provider: ${provider}`);
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2228
2586
|
async function createRouter({
|
|
2229
2587
|
logger,
|
|
2230
2588
|
config,
|
|
@@ -2235,10 +2593,8 @@ async function createRouter({
|
|
|
2235
2593
|
const router = Router__default['default']();
|
|
2236
2594
|
const appUrl = config.getString("app.baseUrl");
|
|
2237
2595
|
const authUrl = await discovery.getExternalBaseUrl("auth");
|
|
2596
|
+
const keyStore = await KeyStores.fromConfig(config, {logger, database});
|
|
2238
2597
|
const keyDurationSeconds = 3600;
|
|
2239
|
-
const keyStore = await DatabaseKeyStore.create({
|
|
2240
|
-
database: await database.getClient()
|
|
2241
|
-
});
|
|
2242
2598
|
const tokenIssuer = new TokenFactory({
|
|
2243
2599
|
issuer: authUrl,
|
|
2244
2600
|
keyStore,
|
|
@@ -2289,6 +2645,7 @@ async function createRouter({
|
|
|
2289
2645
|
}
|
|
2290
2646
|
router.use(`/${providerId}`, r);
|
|
2291
2647
|
} catch (e) {
|
|
2648
|
+
errors.assertError(e);
|
|
2292
2649
|
if (process.env.NODE_ENV !== "development") {
|
|
2293
2650
|
throw new Error(`Failed to initialize ${providerId} auth provider, ${e.message}`);
|
|
2294
2651
|
}
|
|
@@ -2332,6 +2689,8 @@ exports.OAuthAdapter = OAuthAdapter;
|
|
|
2332
2689
|
exports.OAuthEnvironmentHandler = OAuthEnvironmentHandler;
|
|
2333
2690
|
exports.bitbucketUserIdSignInResolver = bitbucketUserIdSignInResolver;
|
|
2334
2691
|
exports.bitbucketUsernameSignInResolver = bitbucketUsernameSignInResolver;
|
|
2692
|
+
exports.createAtlassianProvider = createAtlassianProvider;
|
|
2693
|
+
exports.createAwsAlbProvider = createAwsAlbProvider;
|
|
2335
2694
|
exports.createBitbucketProvider = createBitbucketProvider;
|
|
2336
2695
|
exports.createGithubProvider = createGithubProvider;
|
|
2337
2696
|
exports.createGitlabProvider = createGitlabProvider;
|