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