@muze-nl/metro-oidc 0.9.1 → 0.9.3
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/dist/browser.js +364 -114
- package/dist/browser.js.map +3 -3
- package/dist/browser.min.js +3 -3
- package/dist/browser.min.js.map +3 -3
- package/docs/reference.md +23 -5
- package/package.json +1 -1
- package/src/oidc.discovery.mjs +88 -5
- package/src/oidc.mockserver.mjs +7 -2
- package/src/oidcmw.mjs +204 -39
package/dist/browser.js
CHANGED
|
@@ -107,35 +107,11 @@
|
|
|
107
107
|
if (!(typeof options === "object") || options instanceof String) {
|
|
108
108
|
throw metroError("metro.client.fetch: Invalid options parameter " + metroURL + "client/fetch-invalid-options/", options);
|
|
109
109
|
}
|
|
110
|
-
const metrofetch = async function browserFetch(req2) {
|
|
111
|
-
if (req2[Symbol.metroProxy]) {
|
|
112
|
-
req2 = req2[Symbol.metroSource];
|
|
113
|
-
}
|
|
114
|
-
const res = await fetch(req2);
|
|
115
|
-
return response(res);
|
|
116
|
-
};
|
|
117
|
-
let middlewares = [metrofetch].concat(this.clientOptions?.middlewares?.slice() || []);
|
|
118
110
|
options = Object.assign({}, this.clientOptions, options);
|
|
119
111
|
const traceContext = createTraceContext(req, options);
|
|
120
112
|
const middlewareContext = createMiddlewareContext(this, options, traceContext);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
next = /* @__PURE__ */ (function(next2, middleware2) {
|
|
124
|
-
return async function(req2) {
|
|
125
|
-
let res;
|
|
126
|
-
let tracers = traceContext.tracers;
|
|
127
|
-
callTracers(tracers, "request", req2, middleware2, traceContext);
|
|
128
|
-
try {
|
|
129
|
-
res = await middleware2(req2, next2, middlewareContext);
|
|
130
|
-
} catch (error2) {
|
|
131
|
-
callTracers(tracers, "error", error2, req2, middleware2, traceContext);
|
|
132
|
-
throw error2;
|
|
133
|
-
}
|
|
134
|
-
callTracers(tracers, "response", res, middleware2, traceContext);
|
|
135
|
-
return res;
|
|
136
|
-
};
|
|
137
|
-
})(next, middleware);
|
|
138
|
-
}
|
|
113
|
+
const middlewares = this.clientOptions?.middlewares || [];
|
|
114
|
+
const next = createMiddlewareChain(middlewares, traceContext, middlewareContext);
|
|
139
115
|
return next(req);
|
|
140
116
|
}
|
|
141
117
|
with(...options) {
|
|
@@ -145,6 +121,35 @@
|
|
|
145
121
|
return this.clientOptions.url;
|
|
146
122
|
}
|
|
147
123
|
};
|
|
124
|
+
async function browserFetch(req) {
|
|
125
|
+
if (req[Symbol.metroProxy]) {
|
|
126
|
+
req = req[Symbol.metroSource];
|
|
127
|
+
}
|
|
128
|
+
const res = await fetch(req);
|
|
129
|
+
return response(res);
|
|
130
|
+
}
|
|
131
|
+
function createMiddlewareChain(middlewares, traceContext, middlewareContext) {
|
|
132
|
+
let next;
|
|
133
|
+
for (const middleware of [browserFetch, ...middlewares]) {
|
|
134
|
+
next = traceMiddleware(middleware, next, traceContext, middlewareContext);
|
|
135
|
+
}
|
|
136
|
+
return next;
|
|
137
|
+
}
|
|
138
|
+
function traceMiddleware(middleware, next, traceContext, middlewareContext) {
|
|
139
|
+
const tracers = traceContext.tracers;
|
|
140
|
+
return async function(req) {
|
|
141
|
+
callTracers(tracers, "request", req, middleware, traceContext);
|
|
142
|
+
let res;
|
|
143
|
+
try {
|
|
144
|
+
res = await middleware(req, next, middlewareContext);
|
|
145
|
+
} catch (error2) {
|
|
146
|
+
callTracers(tracers, "error", error2, req, middleware, traceContext);
|
|
147
|
+
throw error2;
|
|
148
|
+
}
|
|
149
|
+
callTracers(tracers, "response", res, middleware, traceContext);
|
|
150
|
+
return res;
|
|
151
|
+
};
|
|
152
|
+
}
|
|
148
153
|
var traceContextId = 0;
|
|
149
154
|
var TRACE_OPTION_KEYS = ["trace", "tracer", "tracers"];
|
|
150
155
|
function fetchOptionsFrom(...options) {
|
|
@@ -258,7 +263,7 @@
|
|
|
258
263
|
if (!tracer) {
|
|
259
264
|
return fn();
|
|
260
265
|
}
|
|
261
|
-
return tracer
|
|
266
|
+
return tracedSpan(tracer, name, fn, data, context);
|
|
262
267
|
},
|
|
263
268
|
link(key) {
|
|
264
269
|
let traceId = null;
|
|
@@ -282,6 +287,21 @@
|
|
|
282
287
|
}
|
|
283
288
|
}
|
|
284
289
|
}
|
|
290
|
+
async function tracedSpan(tracer, name, fn, data, context) {
|
|
291
|
+
let work = null;
|
|
292
|
+
const run = () => {
|
|
293
|
+
if (!work) {
|
|
294
|
+
work = (async () => fn())();
|
|
295
|
+
}
|
|
296
|
+
return work;
|
|
297
|
+
};
|
|
298
|
+
try {
|
|
299
|
+
await tracer.span(name, run, data, context);
|
|
300
|
+
} catch (error2) {
|
|
301
|
+
metroConsole.error("metro: tracer.span() failed", error2);
|
|
302
|
+
}
|
|
303
|
+
return run();
|
|
304
|
+
}
|
|
285
305
|
function callTracer(tracer, method, args) {
|
|
286
306
|
const reportFailure = (error2) => {
|
|
287
307
|
metroConsole.error("metro: tracer." + method + "() failed", error2);
|
|
@@ -1191,6 +1211,7 @@
|
|
|
1191
1211
|
}
|
|
1192
1212
|
|
|
1193
1213
|
// ../metro-oauth2/src/oauth2.mjs
|
|
1214
|
+
var pendingTokenRequests = /* @__PURE__ */ new Map();
|
|
1194
1215
|
var SUPPORTED_TOKEN_TYPES = /* @__PURE__ */ new Map([
|
|
1195
1216
|
["bearer", "Bearer"],
|
|
1196
1217
|
["dpop", "DPoP"]
|
|
@@ -1223,6 +1244,7 @@
|
|
|
1223
1244
|
const oauth2 = Object.assign({}, defaultOptions.oauth2_configuration, options?.oauth2_configuration);
|
|
1224
1245
|
options = Object.assign({}, defaultOptions, options);
|
|
1225
1246
|
options.oauth2_configuration = oauth2;
|
|
1247
|
+
const tokenStoreKey = options.tokens ?? "site:" + options.site;
|
|
1226
1248
|
const store = tokenStore(options.site);
|
|
1227
1249
|
if (!options.tokens) {
|
|
1228
1250
|
options.tokens = store.tokens;
|
|
@@ -1264,15 +1286,15 @@
|
|
|
1264
1286
|
const refreshToken = options.tokens.get("refresh_token");
|
|
1265
1287
|
const tokenIsExpired = isExpired(accessToken);
|
|
1266
1288
|
if (!accessToken || tokenIsExpired && !refreshToken) {
|
|
1267
|
-
const token = await fetchAccessToken
|
|
1289
|
+
const token = await requestToken(fetchAccessToken);
|
|
1268
1290
|
if (!token) {
|
|
1269
|
-
|
|
1291
|
+
throw authorizationNotCompleted(req);
|
|
1270
1292
|
}
|
|
1271
1293
|
return oauth2authorized(req, next);
|
|
1272
1294
|
} else if (tokenIsExpired && refreshToken) {
|
|
1273
|
-
const token = await refreshAccessToken
|
|
1295
|
+
const token = await requestToken(refreshAccessToken);
|
|
1274
1296
|
if (!token) {
|
|
1275
|
-
|
|
1297
|
+
throw authorizationNotCompleted(req);
|
|
1276
1298
|
}
|
|
1277
1299
|
return oauth2authorized(req, next);
|
|
1278
1300
|
} else {
|
|
@@ -1285,14 +1307,30 @@
|
|
|
1285
1307
|
if (!shouldAuthorizeResponse(res) || retryState.handledRejectedToken) {
|
|
1286
1308
|
return res;
|
|
1287
1309
|
}
|
|
1310
|
+
if (options.tokens.get("access_token")?.value !== accessToken.value) {
|
|
1311
|
+
return oauth2authorized(req, next, { handledRejectedToken: true });
|
|
1312
|
+
}
|
|
1288
1313
|
options.tokens.delete("access_token");
|
|
1289
|
-
|
|
1314
|
+
let tokenRequest = fetchAccessToken;
|
|
1315
|
+
if (refreshToken) {
|
|
1316
|
+
tokenRequest = refreshAccessToken;
|
|
1317
|
+
}
|
|
1318
|
+
const token = await requestToken(tokenRequest);
|
|
1290
1319
|
if (!token) {
|
|
1291
|
-
|
|
1320
|
+
throw authorizationNotCompleted(req);
|
|
1292
1321
|
}
|
|
1293
1322
|
return oauth2authorized(req, next, { handledRejectedToken: true });
|
|
1294
1323
|
}
|
|
1295
1324
|
}
|
|
1325
|
+
function requestToken(tokenRequest) {
|
|
1326
|
+
if (!pendingTokenRequests.has(tokenStoreKey)) {
|
|
1327
|
+
const pending = tokenRequest().finally(() => {
|
|
1328
|
+
pendingTokenRequests.delete(tokenStoreKey);
|
|
1329
|
+
});
|
|
1330
|
+
pendingTokenRequests.set(tokenStoreKey, pending);
|
|
1331
|
+
}
|
|
1332
|
+
return pendingTokenRequests.get(tokenStoreKey);
|
|
1333
|
+
}
|
|
1296
1334
|
function getTokensFromLocation() {
|
|
1297
1335
|
if (typeof window !== "undefined" && window?.location) {
|
|
1298
1336
|
let url2 = url(window.location);
|
|
@@ -1308,6 +1346,7 @@
|
|
|
1308
1346
|
history.pushState({}, "", url2.href);
|
|
1309
1347
|
}
|
|
1310
1348
|
if (params) {
|
|
1349
|
+
validateIssuer(params.get("iss"));
|
|
1311
1350
|
if (params.has("error")) {
|
|
1312
1351
|
throw metroError("oauth2mw: authorization failed: " + params.get("error") + (params.get("error_description") ? " (" + params.get("error_description") + ")" : ""));
|
|
1313
1352
|
}
|
|
@@ -1389,6 +1428,9 @@
|
|
|
1389
1428
|
if (oauth2.prompt) {
|
|
1390
1429
|
search.prompt = oauth2.prompt;
|
|
1391
1430
|
}
|
|
1431
|
+
if (oauth2.login_hint) {
|
|
1432
|
+
search.login_hint = oauth2.login_hint;
|
|
1433
|
+
}
|
|
1392
1434
|
if (oauth2.nonce) {
|
|
1393
1435
|
search.nonce = oauth2.nonce;
|
|
1394
1436
|
}
|
|
@@ -1453,6 +1495,7 @@
|
|
|
1453
1495
|
function storeAuthorizationResult(authorization) {
|
|
1454
1496
|
let code = authorization;
|
|
1455
1497
|
if (authorization && typeof authorization === "object") {
|
|
1498
|
+
validateIssuer(authorization.iss);
|
|
1456
1499
|
if (authorization.error) {
|
|
1457
1500
|
throw metroError("oauth2mw: authorization failed: " + authorization.error);
|
|
1458
1501
|
}
|
|
@@ -1464,6 +1507,20 @@
|
|
|
1464
1507
|
}
|
|
1465
1508
|
options.tokens.set("authorization_code", code);
|
|
1466
1509
|
}
|
|
1510
|
+
function validateIssuer(iss) {
|
|
1511
|
+
if (!oauth2.issuer) {
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
if (!iss) {
|
|
1515
|
+
if (oauth2.authorization_response_iss_parameter_supported) {
|
|
1516
|
+
throw metroError("oauth2mw: authorization response is missing iss, expected " + oauth2.issuer);
|
|
1517
|
+
}
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
if (iss !== oauth2.issuer) {
|
|
1521
|
+
throw metroError("oauth2mw: authorization response is from issuer " + iss + ", expected " + oauth2.issuer);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1467
1524
|
function validateState(state) {
|
|
1468
1525
|
let storedState = options.state.get();
|
|
1469
1526
|
if (!state || state !== storedState) {
|
|
@@ -1471,7 +1528,7 @@
|
|
|
1471
1528
|
}
|
|
1472
1529
|
}
|
|
1473
1530
|
function storeTokenResponse(data) {
|
|
1474
|
-
const token = validateTokenResponse(data);
|
|
1531
|
+
const token = validateTokenResponse(data, oauth2.token_type);
|
|
1475
1532
|
options.tokens.set("access_token", token);
|
|
1476
1533
|
if (data.refresh_token) {
|
|
1477
1534
|
options.tokens.set("refresh_token", { value: data.refresh_token });
|
|
@@ -1482,10 +1539,10 @@
|
|
|
1482
1539
|
if (!res) {
|
|
1483
1540
|
return false;
|
|
1484
1541
|
}
|
|
1542
|
+
const challenge = parseBearerChallenge(res.headers?.get("WWW-Authenticate"));
|
|
1485
1543
|
if (res.status === 400) {
|
|
1486
|
-
return
|
|
1544
|
+
return challenge?.error === "invalid_token";
|
|
1487
1545
|
}
|
|
1488
|
-
const challenge = parseBearerChallenge(res.headers?.get("WWW-Authenticate"));
|
|
1489
1546
|
if (challenge?.error === "insufficient_scope") {
|
|
1490
1547
|
return false;
|
|
1491
1548
|
}
|
|
@@ -1503,7 +1560,7 @@
|
|
|
1503
1560
|
}
|
|
1504
1561
|
return token;
|
|
1505
1562
|
}
|
|
1506
|
-
function validateTokenResponse(data) {
|
|
1563
|
+
function validateTokenResponse(data, requiredTokenType) {
|
|
1507
1564
|
if (!data || typeof data !== "object") {
|
|
1508
1565
|
throw metroError("OAuth2mw: token endpoint did not return a JSON object");
|
|
1509
1566
|
}
|
|
@@ -1514,6 +1571,9 @@
|
|
|
1514
1571
|
throw metroError("OAuth2mw: token response did not include token_type");
|
|
1515
1572
|
}
|
|
1516
1573
|
const tokenType = normalizeTokenType(data.token_type);
|
|
1574
|
+
if (requiredTokenType && tokenType !== normalizeTokenType(requiredTokenType)) {
|
|
1575
|
+
throw metroError("OAuth2mw: token endpoint returned a " + tokenType + " token, but " + requiredTokenType + " is required");
|
|
1576
|
+
}
|
|
1517
1577
|
return {
|
|
1518
1578
|
value: data.access_token,
|
|
1519
1579
|
expires: data.expires_in === void 0 ? null : getExpires(data.expires_in),
|
|
@@ -1603,6 +1663,12 @@
|
|
|
1603
1663
|
}
|
|
1604
1664
|
return randomState;
|
|
1605
1665
|
}
|
|
1666
|
+
function authorizationNotCompleted(req) {
|
|
1667
|
+
const error2 = new Error("oauth2mw: authorization was not completed for " + req.url + "; no access token was obtained");
|
|
1668
|
+
error2.code = "authorization_not_completed";
|
|
1669
|
+
error2.request = req;
|
|
1670
|
+
return error2;
|
|
1671
|
+
}
|
|
1606
1672
|
function isRedirected() {
|
|
1607
1673
|
let url2 = new URL(document.location.href);
|
|
1608
1674
|
if (!url2.searchParams.has("code")) {
|
|
@@ -1902,54 +1968,90 @@
|
|
|
1902
1968
|
}
|
|
1903
1969
|
|
|
1904
1970
|
// ../metro-oauth2/src/oauth2.dpop.mjs
|
|
1971
|
+
var serverNonces = /* @__PURE__ */ new Map();
|
|
1905
1972
|
function dpopmw(options) {
|
|
1906
1973
|
assert(options, {
|
|
1907
1974
|
site: Required(validURL),
|
|
1908
|
-
authorization_endpoint: Required(validURL),
|
|
1909
1975
|
token_endpoint: Required(validURL),
|
|
1910
1976
|
dpop_signing_alg_values_supported: Optional([])
|
|
1911
1977
|
// this property is unfortunately rarely supported
|
|
1912
1978
|
});
|
|
1913
1979
|
return async (req, next) => {
|
|
1914
|
-
const
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
await
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
} else if (req.headers.has("Authorization")) {
|
|
1937
|
-
const nonce = localStorage.getItem(url2.host + ":nonce") || void 0;
|
|
1938
|
-
const accessToken = req.headers.get("Authorization").split(" ")[1];
|
|
1939
|
-
const dpopHeader = await generateProof(keyInfo.keyPair, req.url, req.method, nonce, accessToken);
|
|
1940
|
-
req = req.with({
|
|
1980
|
+
const keyPair = await keyPairFor(options.site);
|
|
1981
|
+
const origin = url(req.url).origin;
|
|
1982
|
+
let res = await next(await withProof(req, keyPair));
|
|
1983
|
+
rememberNonce(origin, res);
|
|
1984
|
+
if (needsProof(req) && await asksForNonce(res)) {
|
|
1985
|
+
res = await next(await withProof(req, keyPair));
|
|
1986
|
+
rememberNonce(origin, res);
|
|
1987
|
+
}
|
|
1988
|
+
return res;
|
|
1989
|
+
};
|
|
1990
|
+
function needsProof(req) {
|
|
1991
|
+
return req.url.startsWith(options.token_endpoint) || isDPoPAuthorization(req.headers.get("Authorization"));
|
|
1992
|
+
}
|
|
1993
|
+
async function withProof(req, keyPair) {
|
|
1994
|
+
if (!needsProof(req)) {
|
|
1995
|
+
return req;
|
|
1996
|
+
}
|
|
1997
|
+
const nonce = serverNonces.get(url(req.url).origin);
|
|
1998
|
+
const htu = targetURI(req.url);
|
|
1999
|
+
if (req.url.startsWith(options.token_endpoint)) {
|
|
2000
|
+
const proof2 = await generateProof(keyPair, htu, req.method, nonce);
|
|
2001
|
+
return req.with({
|
|
1941
2002
|
headers: {
|
|
1942
|
-
"
|
|
1943
|
-
"DPoP": dpopHeader
|
|
2003
|
+
"DPoP": proof2
|
|
1944
2004
|
}
|
|
1945
2005
|
});
|
|
1946
2006
|
}
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
2007
|
+
const accessToken = req.headers.get("Authorization").split(" ")[1];
|
|
2008
|
+
const proof = await generateProof(keyPair, htu, req.method, nonce, accessToken);
|
|
2009
|
+
return req.with({
|
|
2010
|
+
headers: {
|
|
2011
|
+
"Authorization": "DPoP " + accessToken,
|
|
2012
|
+
"DPoP": proof
|
|
2013
|
+
}
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
async function keyPairFor(site) {
|
|
2018
|
+
const keys = await keysStore();
|
|
2019
|
+
let keyInfo = await keys.get(site);
|
|
2020
|
+
if (!keyInfo) {
|
|
2021
|
+
const keyPair = await generateKeyPair("ES256");
|
|
2022
|
+
keyInfo = { domain: site, keyPair };
|
|
2023
|
+
await keys.set(keyInfo);
|
|
2024
|
+
}
|
|
2025
|
+
return keyInfo.keyPair;
|
|
2026
|
+
}
|
|
2027
|
+
function targetURI(url2) {
|
|
2028
|
+
const target = new URL(url2);
|
|
2029
|
+
target.search = "";
|
|
2030
|
+
target.hash = "";
|
|
2031
|
+
return target.href;
|
|
2032
|
+
}
|
|
2033
|
+
function rememberNonce(origin, res) {
|
|
2034
|
+
const nonce = res.headers.get("DPoP-Nonce");
|
|
2035
|
+
if (nonce) {
|
|
2036
|
+
serverNonces.set(origin, nonce);
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
async function asksForNonce(res) {
|
|
2040
|
+
if (!res.headers.get("DPoP-Nonce")) {
|
|
2041
|
+
return false;
|
|
2042
|
+
}
|
|
2043
|
+
if (res.status == 401) {
|
|
2044
|
+
const challenge = parseBearerChallenge(res.headers.get("WWW-Authenticate"));
|
|
2045
|
+
return challenge?.error == "use_dpop_nonce";
|
|
2046
|
+
}
|
|
2047
|
+
if (res.status == 400) {
|
|
2048
|
+
const body = await res.clone().json().catch(() => null);
|
|
2049
|
+
return body?.error == "use_dpop_nonce";
|
|
2050
|
+
}
|
|
2051
|
+
return false;
|
|
2052
|
+
}
|
|
2053
|
+
function isDPoPAuthorization(value) {
|
|
2054
|
+
return /^DPoP\s/i.test(value || "");
|
|
1953
2055
|
}
|
|
1954
2056
|
|
|
1955
2057
|
// ../metro-middleware/src/json.mjs
|
|
@@ -2084,9 +2186,12 @@
|
|
|
2084
2186
|
};
|
|
2085
2187
|
options = Object.assign({}, defaultOptions, options);
|
|
2086
2188
|
options.client = options.client.with(throwermw()).with(jsonmw());
|
|
2087
|
-
|
|
2189
|
+
requireSecureURL("issuer", options.issuer);
|
|
2088
2190
|
function MustUseHTTPS(url2) {
|
|
2089
|
-
|
|
2191
|
+
if (isSecureURL(url2)) {
|
|
2192
|
+
return false;
|
|
2193
|
+
}
|
|
2194
|
+
return error("url must use https", url2);
|
|
2090
2195
|
}
|
|
2091
2196
|
const openid_provider_metadata = {
|
|
2092
2197
|
issuer: Required(allOf(options.issuer, MustUseHTTPS)),
|
|
@@ -2134,9 +2239,57 @@
|
|
|
2134
2239
|
);
|
|
2135
2240
|
const openid_config = response2.data;
|
|
2136
2241
|
assert(openid_config, openid_provider_metadata);
|
|
2137
|
-
|
|
2242
|
+
checkProviderMetadata(openid_config, options.issuer);
|
|
2138
2243
|
return openid_config;
|
|
2139
2244
|
}
|
|
2245
|
+
var REQUIRED_ENDPOINTS = ["authorization_endpoint", "token_endpoint", "jwks_uri"];
|
|
2246
|
+
function checkProviderMetadata(config, issuer) {
|
|
2247
|
+
if (!config || typeof config !== "object") {
|
|
2248
|
+
throw metroError("metro.oidc.discovery: openid-configuration for " + issuer + " is not a JSON object");
|
|
2249
|
+
}
|
|
2250
|
+
if (!sameIssuer(config.issuer, issuer)) {
|
|
2251
|
+
throw metroError("metro.oidc.discovery: openid-configuration is for issuer " + config.issuer + ", expected " + issuer);
|
|
2252
|
+
}
|
|
2253
|
+
requireSecureURL("issuer", config.issuer);
|
|
2254
|
+
for (const name of REQUIRED_ENDPOINTS) {
|
|
2255
|
+
if (!config[name]) {
|
|
2256
|
+
throw metroError("metro.oidc.discovery: openid-configuration for " + issuer + " has no " + name);
|
|
2257
|
+
}
|
|
2258
|
+
requireSecureURL(name, config[name]);
|
|
2259
|
+
}
|
|
2260
|
+
if (config.registration_endpoint) {
|
|
2261
|
+
requireSecureURL("registration_endpoint", config.registration_endpoint);
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
function sameIssuer(discovered, requested) {
|
|
2265
|
+
if (typeof discovered != "string") {
|
|
2266
|
+
return false;
|
|
2267
|
+
}
|
|
2268
|
+
return withoutTrailingSlash(discovered) === withoutTrailingSlash(String(requested));
|
|
2269
|
+
}
|
|
2270
|
+
function withoutTrailingSlash(value) {
|
|
2271
|
+
return value.replace(/\/$/, "");
|
|
2272
|
+
}
|
|
2273
|
+
function requireSecureURL(name, value) {
|
|
2274
|
+
if (!isSecureURL(value)) {
|
|
2275
|
+
throw metroError("metro.oidc.discovery: " + name + " must use https: " + value);
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
function isSecureURL(value) {
|
|
2279
|
+
let url2;
|
|
2280
|
+
try {
|
|
2281
|
+
url2 = new URL(String(value));
|
|
2282
|
+
} catch (e) {
|
|
2283
|
+
return false;
|
|
2284
|
+
}
|
|
2285
|
+
if (url2.protocol == "https:") {
|
|
2286
|
+
return true;
|
|
2287
|
+
}
|
|
2288
|
+
return url2.protocol == "http:" && isLoopback(url2.hostname);
|
|
2289
|
+
}
|
|
2290
|
+
function isLoopback(hostname) {
|
|
2291
|
+
return hostname == "localhost" || hostname.endsWith(".localhost") || hostname == "127.0.0.1" || hostname == "[::1]";
|
|
2292
|
+
}
|
|
2140
2293
|
|
|
2141
2294
|
// src/oidc.register.mjs
|
|
2142
2295
|
async function register(options) {
|
|
@@ -3510,6 +3663,21 @@
|
|
|
3510
3663
|
}
|
|
3511
3664
|
|
|
3512
3665
|
// src/oidcmw.mjs
|
|
3666
|
+
var pendingClientSetups = /* @__PURE__ */ new Map();
|
|
3667
|
+
var BROWSER_CLIENT_METADATA = {
|
|
3668
|
+
token_endpoint_auth_method: "none",
|
|
3669
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
3670
|
+
response_types: ["code"]
|
|
3671
|
+
};
|
|
3672
|
+
function sharedClientSetup(key, setup) {
|
|
3673
|
+
if (!pendingClientSetups.has(key)) {
|
|
3674
|
+
const pending = setup().finally(() => {
|
|
3675
|
+
pendingClientSetups.delete(key);
|
|
3676
|
+
});
|
|
3677
|
+
pendingClientSetups.set(key, pending);
|
|
3678
|
+
}
|
|
3679
|
+
return pendingClientSetups.get(key);
|
|
3680
|
+
}
|
|
3513
3681
|
function oidcmw(options = {}) {
|
|
3514
3682
|
const defaultOptions = {
|
|
3515
3683
|
client: client(),
|
|
@@ -3532,8 +3700,14 @@
|
|
|
3532
3700
|
oauth2: Optional({}),
|
|
3533
3701
|
openid_configuration: Optional()
|
|
3534
3702
|
});
|
|
3703
|
+
const account = accountFor(options);
|
|
3704
|
+
const expectedClaims = expectedClaimsFor(options);
|
|
3705
|
+
const site = siteFor(options.issuer, account);
|
|
3706
|
+
const clientSetupKey = options.issuer + " " + JSON.stringify(requestedClientInfo?.redirect_uris ?? null);
|
|
3707
|
+
let userStore = options.store;
|
|
3535
3708
|
if (!options.store) {
|
|
3536
3709
|
options.store = oidcStore(options.issuer);
|
|
3710
|
+
userStore = oidcStore(site);
|
|
3537
3711
|
}
|
|
3538
3712
|
if (!options.openid_configuration && options.store.has("openid_configuration")) {
|
|
3539
3713
|
options.openid_configuration = options.store.get("openid_configuration");
|
|
@@ -3544,51 +3718,70 @@
|
|
|
3544
3718
|
options.client_info = storedClientInfo;
|
|
3545
3719
|
}
|
|
3546
3720
|
}
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
try {
|
|
3551
|
-
res = await next(req);
|
|
3552
|
-
} catch (err) {
|
|
3553
|
-
res = err?.cause;
|
|
3554
|
-
if (!res || res.status != 401 && res.status != 403) {
|
|
3555
|
-
throw err;
|
|
3556
|
-
}
|
|
3557
|
-
}
|
|
3558
|
-
if (res.ok || res.status != 401 && res.status != 403) {
|
|
3559
|
-
return res;
|
|
3560
|
-
}
|
|
3721
|
+
async function prepareClient() {
|
|
3722
|
+
if (options.openid_configuration && options.client_info?.client_id) {
|
|
3723
|
+
return;
|
|
3561
3724
|
}
|
|
3562
|
-
|
|
3563
|
-
|
|
3725
|
+
const prepared = await sharedClientSetup(clientSetupKey, discoverAndRegister);
|
|
3726
|
+
options.openid_configuration = prepared.openid_configuration;
|
|
3727
|
+
options.client_info = prepared.client_info;
|
|
3728
|
+
options.store.set("openid_configuration", options.openid_configuration);
|
|
3729
|
+
options.store.set("client_info", options.client_info);
|
|
3730
|
+
}
|
|
3731
|
+
async function discoverAndRegister() {
|
|
3732
|
+
let openid_configuration = options.openid_configuration;
|
|
3733
|
+
if (!openid_configuration) {
|
|
3734
|
+
openid_configuration = await oidcDiscovery({
|
|
3564
3735
|
issuer: options.issuer,
|
|
3565
3736
|
client: options.client.with(options.issuer)
|
|
3566
3737
|
});
|
|
3567
|
-
options.store.set("openid_configuration", options.openid_configuration);
|
|
3568
3738
|
}
|
|
3569
|
-
|
|
3570
|
-
|
|
3739
|
+
let client_info = options.client_info;
|
|
3740
|
+
if (!client_info?.client_id) {
|
|
3741
|
+
if (!openid_configuration.registration_endpoint) {
|
|
3571
3742
|
throw metroError("metro.oidcmw: Error: issuer " + options.issuer + " does not support dynamic client registration, but you haven't specified a client_id");
|
|
3572
3743
|
}
|
|
3573
|
-
|
|
3574
|
-
registration_endpoint:
|
|
3744
|
+
client_info = await register({
|
|
3745
|
+
registration_endpoint: openid_configuration.registration_endpoint,
|
|
3575
3746
|
client: options.client,
|
|
3576
|
-
client_info:
|
|
3747
|
+
client_info: Object.assign({}, BROWSER_CLIENT_METADATA, client_info)
|
|
3577
3748
|
});
|
|
3578
|
-
|
|
3749
|
+
if (client_info.token_endpoint_auth_method == "none") {
|
|
3750
|
+
delete client_info.client_secret;
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
return { openid_configuration, client_info };
|
|
3754
|
+
}
|
|
3755
|
+
function rememberNonce2(authorizeCallback) {
|
|
3756
|
+
if (typeof authorizeCallback != "function") {
|
|
3757
|
+
return authorizeCallback;
|
|
3758
|
+
}
|
|
3759
|
+
return async (url2) => {
|
|
3760
|
+
userStore.set("pending_nonce", url2.searchParams.get("nonce"));
|
|
3761
|
+
return authorizeCallback(url2);
|
|
3762
|
+
};
|
|
3763
|
+
}
|
|
3764
|
+
return async (req, next) => {
|
|
3765
|
+
let res;
|
|
3766
|
+
if (!options.force_authorization) {
|
|
3767
|
+
res = await next(req);
|
|
3768
|
+
if (res.ok || res.status != 401 && res.status != 403) {
|
|
3769
|
+
return res;
|
|
3770
|
+
}
|
|
3579
3771
|
}
|
|
3580
|
-
|
|
3772
|
+
await prepareClient();
|
|
3773
|
+
const scope = options.scope || defaultScope(options);
|
|
3581
3774
|
const nonce = options.nonce || generateCodeVerifier(32);
|
|
3582
|
-
options.store.set("pending_nonce", nonce);
|
|
3583
3775
|
const oauth2Options = Object.assign(
|
|
3584
3776
|
{
|
|
3585
|
-
site
|
|
3777
|
+
site,
|
|
3586
3778
|
client: options.client,
|
|
3587
3779
|
force_authorization: true,
|
|
3588
|
-
authorize_callback: options.authorize_callback,
|
|
3780
|
+
authorize_callback: rememberNonce2(options.authorize_callback),
|
|
3589
3781
|
oauth2_configuration: {
|
|
3590
3782
|
client_id: options.client_info?.client_id,
|
|
3591
3783
|
client_secret: options.client_info?.client_secret,
|
|
3784
|
+
token_endpoint_auth_method: options.client_info?.token_endpoint_auth_method,
|
|
3592
3785
|
grant_type: "authorization_code",
|
|
3593
3786
|
response_type: "code",
|
|
3594
3787
|
response_mode: "query",
|
|
@@ -3597,38 +3790,50 @@
|
|
|
3597
3790
|
scope,
|
|
3598
3791
|
//FIXME: should only use scopes supported by server
|
|
3599
3792
|
redirect_uri: options.client_info.redirect_uris[0],
|
|
3793
|
+
login_hint: options.login_hint ?? options.webid,
|
|
3794
|
+
issuer: options.openid_configuration.issuer,
|
|
3795
|
+
authorization_response_iss_parameter_supported: options.openid_configuration.authorization_response_iss_parameter_supported,
|
|
3796
|
+
token_type: options.use_dpop ? "DPoP" : void 0,
|
|
3600
3797
|
nonce
|
|
3601
3798
|
}
|
|
3602
3799
|
}
|
|
3603
3800
|
//...
|
|
3604
3801
|
);
|
|
3605
3802
|
const storeIdToken = async (req2, next2) => {
|
|
3606
|
-
const res2 = await next2(req2);
|
|
3607
3803
|
const tokenEndpoint = url(options.openid_configuration.token_endpoint, { hash: "" }).href;
|
|
3608
3804
|
const requestUrl = url(req2.url, { hash: "" }).href;
|
|
3609
3805
|
if (requestUrl !== tokenEndpoint) {
|
|
3610
|
-
return
|
|
3806
|
+
return next2(req2);
|
|
3611
3807
|
}
|
|
3808
|
+
const isRefresh = await grantTypeOf(req2) == "refresh_token";
|
|
3809
|
+
const res2 = await next2(req2);
|
|
3612
3810
|
const contentType = res2.headers.get("content-type");
|
|
3613
|
-
if (!contentType?.startsWith("application/json")) {
|
|
3811
|
+
if (!res2.ok || !contentType?.startsWith("application/json")) {
|
|
3614
3812
|
return res2;
|
|
3615
3813
|
}
|
|
3616
|
-
let data = res2.data
|
|
3617
|
-
if (!data) {
|
|
3814
|
+
let data = res2.data;
|
|
3815
|
+
if (!data || typeof data !== "object" || data instanceof ReadableStream) {
|
|
3618
3816
|
const res22 = res2.clone();
|
|
3619
3817
|
data = await res22.json();
|
|
3620
3818
|
}
|
|
3621
3819
|
const id_token = data?.id_token;
|
|
3820
|
+
if (isRefresh && !id_token) {
|
|
3821
|
+
return res2;
|
|
3822
|
+
}
|
|
3622
3823
|
const jwks = await getJwks();
|
|
3623
3824
|
const validation = await validateIdToken(id_token, {
|
|
3624
3825
|
issuer: options.openid_configuration.issuer,
|
|
3625
3826
|
client_id: options.client_info.client_id,
|
|
3626
3827
|
jwks,
|
|
3627
3828
|
openid_configuration: options.openid_configuration,
|
|
3628
|
-
nonce:
|
|
3829
|
+
nonce: isRefresh ? void 0 : userStore.get("pending_nonce")
|
|
3629
3830
|
});
|
|
3630
|
-
|
|
3631
|
-
|
|
3831
|
+
if (isRefresh) {
|
|
3832
|
+
assertSameUserAsLogin(validation.claims, userStore.get("id_token_claims"));
|
|
3833
|
+
}
|
|
3834
|
+
assertExpectedUser(validation.claims, expectedClaims);
|
|
3835
|
+
userStore.set("id_token", id_token);
|
|
3836
|
+
userStore.set("id_token_claims", validation.claims);
|
|
3632
3837
|
return res2;
|
|
3633
3838
|
};
|
|
3634
3839
|
const getJwks = async () => {
|
|
@@ -3643,7 +3848,6 @@
|
|
|
3643
3848
|
if (options.use_dpop) {
|
|
3644
3849
|
const dpopOptions = {
|
|
3645
3850
|
site: options.issuer,
|
|
3646
|
-
authorization_endpoint: options.openid_configuration.authorization_endpoint,
|
|
3647
3851
|
token_endpoint: options.openid_configuration.token_endpoint,
|
|
3648
3852
|
dpop_signing_alg_values_supported: options.openid_configuration.dpop_signing_alg_values_supported
|
|
3649
3853
|
};
|
|
@@ -3665,6 +3869,52 @@
|
|
|
3665
3869
|
const storedRedirectUris = new Set(storedClientInfo.redirect_uris || []);
|
|
3666
3870
|
return requestedClientInfo.redirect_uris.every((uri) => storedRedirectUris.has(uri));
|
|
3667
3871
|
}
|
|
3872
|
+
function accountFor(options) {
|
|
3873
|
+
return options.webid ?? options.login_hint;
|
|
3874
|
+
}
|
|
3875
|
+
function expectedClaimsFor(options) {
|
|
3876
|
+
if (options.webid) {
|
|
3877
|
+
return Object.assign({ webid: options.webid }, options.expected_claims);
|
|
3878
|
+
}
|
|
3879
|
+
return options.expected_claims || {};
|
|
3880
|
+
}
|
|
3881
|
+
function defaultScope(options) {
|
|
3882
|
+
const supported = options.openid_configuration?.scopes_supported;
|
|
3883
|
+
const solidIssuer = Array.isArray(supported) && supported.includes("webid");
|
|
3884
|
+
if (options.webid || solidIssuer) {
|
|
3885
|
+
return "openid webid";
|
|
3886
|
+
}
|
|
3887
|
+
return "openid";
|
|
3888
|
+
}
|
|
3889
|
+
function siteFor(issuer, account) {
|
|
3890
|
+
if (!account) {
|
|
3891
|
+
return issuer;
|
|
3892
|
+
}
|
|
3893
|
+
return issuer + "|" + account;
|
|
3894
|
+
}
|
|
3895
|
+
function assertExpectedUser(claims, expectedClaims) {
|
|
3896
|
+
for (const [name, expected] of Object.entries(expectedClaims)) {
|
|
3897
|
+
const actual = claimValue(claims, name);
|
|
3898
|
+
if (actual !== expected) {
|
|
3899
|
+
throw metroError("metro.oidcmw: id_token is for a different user: expected " + name + " " + expected + ", got " + actual);
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
async function grantTypeOf(req) {
|
|
3904
|
+
const body = await req.clone().text();
|
|
3905
|
+
return new URLSearchParams(body).get("grant_type");
|
|
3906
|
+
}
|
|
3907
|
+
function assertSameUserAsLogin(claims, loginClaims) {
|
|
3908
|
+
if (loginClaims && claims.sub !== loginClaims.sub) {
|
|
3909
|
+
throw metroError("metro.oidcmw: refreshed id_token is for a different user: expected sub " + loginClaims.sub + ", got " + claims.sub);
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
function claimValue(claims, name) {
|
|
3913
|
+
if (name == "webid" && claims.webid === void 0) {
|
|
3914
|
+
return claims.sub;
|
|
3915
|
+
}
|
|
3916
|
+
return claims[name];
|
|
3917
|
+
}
|
|
3668
3918
|
function isRedirected2() {
|
|
3669
3919
|
return isRedirected();
|
|
3670
3920
|
}
|
|
@@ -3673,7 +3923,7 @@
|
|
|
3673
3923
|
if (!options.issuer) {
|
|
3674
3924
|
throw metroError("Must supply options.issuer or options.store to get the id_token");
|
|
3675
3925
|
}
|
|
3676
|
-
options.store = oidcStore(options.issuer);
|
|
3926
|
+
options.store = oidcStore(siteFor(options.issuer, accountFor(options)));
|
|
3677
3927
|
}
|
|
3678
3928
|
return options.store.get("id_token");
|
|
3679
3929
|
}
|
|
@@ -3682,7 +3932,7 @@
|
|
|
3682
3932
|
if (!options.issuer) {
|
|
3683
3933
|
throw metroError("Must supply options.issuer or options.store to get the id_token claims");
|
|
3684
3934
|
}
|
|
3685
|
-
options.store = oidcStore(options.issuer);
|
|
3935
|
+
options.store = oidcStore(siteFor(options.issuer, accountFor(options)));
|
|
3686
3936
|
}
|
|
3687
3937
|
return options.store.get("id_token_claims");
|
|
3688
3938
|
}
|