@muze-nl/metro-oidc 0.9.0 → 0.9.2
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 +374 -111
- 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 +19 -5
- package/package.json +1 -1
- package/src/oidc.discovery.mjs +88 -5
- package/src/oidc.mockserver.mjs +7 -2
- package/src/oidcmw.mjs +199 -35
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;
|
|
@@ -278,10 +283,38 @@
|
|
|
278
283
|
function callTracers(tracers, method, ...args) {
|
|
279
284
|
for (const tracer of tracers) {
|
|
280
285
|
if (tracer && typeof tracer[method] == "function") {
|
|
281
|
-
|
|
286
|
+
callTracer(tracer, method, args);
|
|
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
|
+
}
|
|
305
|
+
function callTracer(tracer, method, args) {
|
|
306
|
+
const reportFailure = (error2) => {
|
|
307
|
+
metroConsole.error("metro: tracer." + method + "() failed", error2);
|
|
308
|
+
};
|
|
309
|
+
try {
|
|
310
|
+
const result = tracer[method].call(tracer, ...args);
|
|
311
|
+
if (typeof result?.then == "function") {
|
|
312
|
+
result.then(void 0, reportFailure);
|
|
313
|
+
}
|
|
314
|
+
} catch (error2) {
|
|
315
|
+
reportFailure(error2);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
285
318
|
function isPlainObject(value) {
|
|
286
319
|
return value && typeof value == "object" && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
287
320
|
}
|
|
@@ -1178,6 +1211,7 @@
|
|
|
1178
1211
|
}
|
|
1179
1212
|
|
|
1180
1213
|
// ../metro-oauth2/src/oauth2.mjs
|
|
1214
|
+
var pendingTokenRequests = /* @__PURE__ */ new Map();
|
|
1181
1215
|
var SUPPORTED_TOKEN_TYPES = /* @__PURE__ */ new Map([
|
|
1182
1216
|
["bearer", "Bearer"],
|
|
1183
1217
|
["dpop", "DPoP"]
|
|
@@ -1210,6 +1244,7 @@
|
|
|
1210
1244
|
const oauth2 = Object.assign({}, defaultOptions.oauth2_configuration, options?.oauth2_configuration);
|
|
1211
1245
|
options = Object.assign({}, defaultOptions, options);
|
|
1212
1246
|
options.oauth2_configuration = oauth2;
|
|
1247
|
+
const tokenStoreKey = options.tokens ?? "site:" + options.site;
|
|
1213
1248
|
const store = tokenStore(options.site);
|
|
1214
1249
|
if (!options.tokens) {
|
|
1215
1250
|
options.tokens = store.tokens;
|
|
@@ -1251,15 +1286,15 @@
|
|
|
1251
1286
|
const refreshToken = options.tokens.get("refresh_token");
|
|
1252
1287
|
const tokenIsExpired = isExpired(accessToken);
|
|
1253
1288
|
if (!accessToken || tokenIsExpired && !refreshToken) {
|
|
1254
|
-
const token = await fetchAccessToken
|
|
1289
|
+
const token = await requestToken(fetchAccessToken);
|
|
1255
1290
|
if (!token) {
|
|
1256
|
-
|
|
1291
|
+
throw authorizationNotCompleted(req);
|
|
1257
1292
|
}
|
|
1258
1293
|
return oauth2authorized(req, next);
|
|
1259
1294
|
} else if (tokenIsExpired && refreshToken) {
|
|
1260
|
-
const token = await refreshAccessToken
|
|
1295
|
+
const token = await requestToken(refreshAccessToken);
|
|
1261
1296
|
if (!token) {
|
|
1262
|
-
|
|
1297
|
+
throw authorizationNotCompleted(req);
|
|
1263
1298
|
}
|
|
1264
1299
|
return oauth2authorized(req, next);
|
|
1265
1300
|
} else {
|
|
@@ -1272,14 +1307,30 @@
|
|
|
1272
1307
|
if (!shouldAuthorizeResponse(res) || retryState.handledRejectedToken) {
|
|
1273
1308
|
return res;
|
|
1274
1309
|
}
|
|
1310
|
+
if (options.tokens.get("access_token")?.value !== accessToken.value) {
|
|
1311
|
+
return oauth2authorized(req, next, { handledRejectedToken: true });
|
|
1312
|
+
}
|
|
1275
1313
|
options.tokens.delete("access_token");
|
|
1276
|
-
|
|
1314
|
+
let tokenRequest = fetchAccessToken;
|
|
1315
|
+
if (refreshToken) {
|
|
1316
|
+
tokenRequest = refreshAccessToken;
|
|
1317
|
+
}
|
|
1318
|
+
const token = await requestToken(tokenRequest);
|
|
1277
1319
|
if (!token) {
|
|
1278
|
-
|
|
1320
|
+
throw authorizationNotCompleted(req);
|
|
1279
1321
|
}
|
|
1280
1322
|
return oauth2authorized(req, next, { handledRejectedToken: true });
|
|
1281
1323
|
}
|
|
1282
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
|
+
}
|
|
1283
1334
|
function getTokensFromLocation() {
|
|
1284
1335
|
if (typeof window !== "undefined" && window?.location) {
|
|
1285
1336
|
let url2 = url(window.location);
|
|
@@ -1295,6 +1346,7 @@
|
|
|
1295
1346
|
history.pushState({}, "", url2.href);
|
|
1296
1347
|
}
|
|
1297
1348
|
if (params) {
|
|
1349
|
+
validateIssuer(params.get("iss"));
|
|
1298
1350
|
if (params.has("error")) {
|
|
1299
1351
|
throw metroError("oauth2mw: authorization failed: " + params.get("error") + (params.get("error_description") ? " (" + params.get("error_description") + ")" : ""));
|
|
1300
1352
|
}
|
|
@@ -1376,6 +1428,9 @@
|
|
|
1376
1428
|
if (oauth2.prompt) {
|
|
1377
1429
|
search.prompt = oauth2.prompt;
|
|
1378
1430
|
}
|
|
1431
|
+
if (oauth2.login_hint) {
|
|
1432
|
+
search.login_hint = oauth2.login_hint;
|
|
1433
|
+
}
|
|
1379
1434
|
if (oauth2.nonce) {
|
|
1380
1435
|
search.nonce = oauth2.nonce;
|
|
1381
1436
|
}
|
|
@@ -1440,6 +1495,7 @@
|
|
|
1440
1495
|
function storeAuthorizationResult(authorization) {
|
|
1441
1496
|
let code = authorization;
|
|
1442
1497
|
if (authorization && typeof authorization === "object") {
|
|
1498
|
+
validateIssuer(authorization.iss);
|
|
1443
1499
|
if (authorization.error) {
|
|
1444
1500
|
throw metroError("oauth2mw: authorization failed: " + authorization.error);
|
|
1445
1501
|
}
|
|
@@ -1451,6 +1507,20 @@
|
|
|
1451
1507
|
}
|
|
1452
1508
|
options.tokens.set("authorization_code", code);
|
|
1453
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
|
+
}
|
|
1454
1524
|
function validateState(state) {
|
|
1455
1525
|
let storedState = options.state.get();
|
|
1456
1526
|
if (!state || state !== storedState) {
|
|
@@ -1458,7 +1528,7 @@
|
|
|
1458
1528
|
}
|
|
1459
1529
|
}
|
|
1460
1530
|
function storeTokenResponse(data) {
|
|
1461
|
-
const token = validateTokenResponse(data);
|
|
1531
|
+
const token = validateTokenResponse(data, oauth2.token_type);
|
|
1462
1532
|
options.tokens.set("access_token", token);
|
|
1463
1533
|
if (data.refresh_token) {
|
|
1464
1534
|
options.tokens.set("refresh_token", { value: data.refresh_token });
|
|
@@ -1469,10 +1539,10 @@
|
|
|
1469
1539
|
if (!res) {
|
|
1470
1540
|
return false;
|
|
1471
1541
|
}
|
|
1542
|
+
const challenge = parseBearerChallenge(res.headers?.get("WWW-Authenticate"));
|
|
1472
1543
|
if (res.status === 400) {
|
|
1473
|
-
return
|
|
1544
|
+
return challenge?.error === "invalid_token";
|
|
1474
1545
|
}
|
|
1475
|
-
const challenge = parseBearerChallenge(res.headers?.get("WWW-Authenticate"));
|
|
1476
1546
|
if (challenge?.error === "insufficient_scope") {
|
|
1477
1547
|
return false;
|
|
1478
1548
|
}
|
|
@@ -1490,7 +1560,7 @@
|
|
|
1490
1560
|
}
|
|
1491
1561
|
return token;
|
|
1492
1562
|
}
|
|
1493
|
-
function validateTokenResponse(data) {
|
|
1563
|
+
function validateTokenResponse(data, requiredTokenType) {
|
|
1494
1564
|
if (!data || typeof data !== "object") {
|
|
1495
1565
|
throw metroError("OAuth2mw: token endpoint did not return a JSON object");
|
|
1496
1566
|
}
|
|
@@ -1501,6 +1571,9 @@
|
|
|
1501
1571
|
throw metroError("OAuth2mw: token response did not include token_type");
|
|
1502
1572
|
}
|
|
1503
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
|
+
}
|
|
1504
1577
|
return {
|
|
1505
1578
|
value: data.access_token,
|
|
1506
1579
|
expires: data.expires_in === void 0 ? null : getExpires(data.expires_in),
|
|
@@ -1590,6 +1663,12 @@
|
|
|
1590
1663
|
}
|
|
1591
1664
|
return randomState;
|
|
1592
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
|
+
}
|
|
1593
1672
|
function isRedirected() {
|
|
1594
1673
|
let url2 = new URL(document.location.href);
|
|
1595
1674
|
if (!url2.searchParams.has("code")) {
|
|
@@ -1889,54 +1968,90 @@
|
|
|
1889
1968
|
}
|
|
1890
1969
|
|
|
1891
1970
|
// ../metro-oauth2/src/oauth2.dpop.mjs
|
|
1971
|
+
var serverNonces = /* @__PURE__ */ new Map();
|
|
1892
1972
|
function dpopmw(options) {
|
|
1893
1973
|
assert(options, {
|
|
1894
1974
|
site: Required(validURL),
|
|
1895
|
-
authorization_endpoint: Required(validURL),
|
|
1896
1975
|
token_endpoint: Required(validURL),
|
|
1897
1976
|
dpop_signing_alg_values_supported: Optional([])
|
|
1898
1977
|
// this property is unfortunately rarely supported
|
|
1899
1978
|
});
|
|
1900
1979
|
return async (req, next) => {
|
|
1901
|
-
const
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
await
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
} else if (req.headers.has("Authorization")) {
|
|
1924
|
-
const nonce = localStorage.getItem(url2.host + ":nonce") || void 0;
|
|
1925
|
-
const accessToken = req.headers.get("Authorization").split(" ")[1];
|
|
1926
|
-
const dpopHeader = await generateProof(keyInfo.keyPair, req.url, req.method, nonce, accessToken);
|
|
1927
|
-
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({
|
|
1928
2002
|
headers: {
|
|
1929
|
-
"
|
|
1930
|
-
"DPoP": dpopHeader
|
|
2003
|
+
"DPoP": proof2
|
|
1931
2004
|
}
|
|
1932
2005
|
});
|
|
1933
2006
|
}
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
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 || "");
|
|
1940
2055
|
}
|
|
1941
2056
|
|
|
1942
2057
|
// ../metro-middleware/src/json.mjs
|
|
@@ -2071,9 +2186,12 @@
|
|
|
2071
2186
|
};
|
|
2072
2187
|
options = Object.assign({}, defaultOptions, options);
|
|
2073
2188
|
options.client = options.client.with(throwermw()).with(jsonmw());
|
|
2074
|
-
|
|
2189
|
+
requireSecureURL("issuer", options.issuer);
|
|
2075
2190
|
function MustUseHTTPS(url2) {
|
|
2076
|
-
|
|
2191
|
+
if (isSecureURL(url2)) {
|
|
2192
|
+
return false;
|
|
2193
|
+
}
|
|
2194
|
+
return error("url must use https", url2);
|
|
2077
2195
|
}
|
|
2078
2196
|
const openid_provider_metadata = {
|
|
2079
2197
|
issuer: Required(allOf(options.issuer, MustUseHTTPS)),
|
|
@@ -2121,9 +2239,57 @@
|
|
|
2121
2239
|
);
|
|
2122
2240
|
const openid_config = response2.data;
|
|
2123
2241
|
assert(openid_config, openid_provider_metadata);
|
|
2124
|
-
|
|
2242
|
+
checkProviderMetadata(openid_config, options.issuer);
|
|
2125
2243
|
return openid_config;
|
|
2126
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
|
+
}
|
|
2127
2293
|
|
|
2128
2294
|
// src/oidc.register.mjs
|
|
2129
2295
|
async function register(options) {
|
|
@@ -3497,6 +3663,21 @@
|
|
|
3497
3663
|
}
|
|
3498
3664
|
|
|
3499
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
|
+
}
|
|
3500
3681
|
function oidcmw(options = {}) {
|
|
3501
3682
|
const defaultOptions = {
|
|
3502
3683
|
client: client(),
|
|
@@ -3519,8 +3700,14 @@
|
|
|
3519
3700
|
oauth2: Optional({}),
|
|
3520
3701
|
openid_configuration: Optional()
|
|
3521
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;
|
|
3522
3708
|
if (!options.store) {
|
|
3523
3709
|
options.store = oidcStore(options.issuer);
|
|
3710
|
+
userStore = oidcStore(site);
|
|
3524
3711
|
}
|
|
3525
3712
|
if (!options.openid_configuration && options.store.has("openid_configuration")) {
|
|
3526
3713
|
options.openid_configuration = options.store.get("openid_configuration");
|
|
@@ -3531,51 +3718,70 @@
|
|
|
3531
3718
|
options.client_info = storedClientInfo;
|
|
3532
3719
|
}
|
|
3533
3720
|
}
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
try {
|
|
3538
|
-
res = await next(req);
|
|
3539
|
-
} catch (err) {
|
|
3540
|
-
res = err?.cause;
|
|
3541
|
-
if (!res || res.status != 401 && res.status != 403) {
|
|
3542
|
-
throw err;
|
|
3543
|
-
}
|
|
3544
|
-
}
|
|
3545
|
-
if (res.ok || res.status != 401 && res.status != 403) {
|
|
3546
|
-
return res;
|
|
3547
|
-
}
|
|
3721
|
+
async function prepareClient() {
|
|
3722
|
+
if (options.openid_configuration && options.client_info?.client_id) {
|
|
3723
|
+
return;
|
|
3548
3724
|
}
|
|
3549
|
-
|
|
3550
|
-
|
|
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({
|
|
3551
3735
|
issuer: options.issuer,
|
|
3552
3736
|
client: options.client.with(options.issuer)
|
|
3553
3737
|
});
|
|
3554
|
-
options.store.set("openid_configuration", options.openid_configuration);
|
|
3555
3738
|
}
|
|
3556
|
-
|
|
3557
|
-
|
|
3739
|
+
let client_info = options.client_info;
|
|
3740
|
+
if (!client_info?.client_id) {
|
|
3741
|
+
if (!openid_configuration.registration_endpoint) {
|
|
3558
3742
|
throw metroError("metro.oidcmw: Error: issuer " + options.issuer + " does not support dynamic client registration, but you haven't specified a client_id");
|
|
3559
3743
|
}
|
|
3560
|
-
|
|
3561
|
-
registration_endpoint:
|
|
3744
|
+
client_info = await register({
|
|
3745
|
+
registration_endpoint: openid_configuration.registration_endpoint,
|
|
3562
3746
|
client: options.client,
|
|
3563
|
-
client_info:
|
|
3747
|
+
client_info: Object.assign({}, BROWSER_CLIENT_METADATA, client_info)
|
|
3564
3748
|
});
|
|
3565
|
-
|
|
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
|
+
}
|
|
3566
3771
|
}
|
|
3567
|
-
|
|
3772
|
+
await prepareClient();
|
|
3773
|
+
const scope = options.scope || defaultScope(options);
|
|
3568
3774
|
const nonce = options.nonce || generateCodeVerifier(32);
|
|
3569
|
-
options.store.set("pending_nonce", nonce);
|
|
3570
3775
|
const oauth2Options = Object.assign(
|
|
3571
3776
|
{
|
|
3572
|
-
site
|
|
3777
|
+
site,
|
|
3573
3778
|
client: options.client,
|
|
3574
3779
|
force_authorization: true,
|
|
3575
|
-
authorize_callback: options.authorize_callback,
|
|
3780
|
+
authorize_callback: rememberNonce2(options.authorize_callback),
|
|
3576
3781
|
oauth2_configuration: {
|
|
3577
3782
|
client_id: options.client_info?.client_id,
|
|
3578
3783
|
client_secret: options.client_info?.client_secret,
|
|
3784
|
+
token_endpoint_auth_method: options.client_info?.token_endpoint_auth_method,
|
|
3579
3785
|
grant_type: "authorization_code",
|
|
3580
3786
|
response_type: "code",
|
|
3581
3787
|
response_mode: "query",
|
|
@@ -3584,6 +3790,10 @@
|
|
|
3584
3790
|
scope,
|
|
3585
3791
|
//FIXME: should only use scopes supported by server
|
|
3586
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,
|
|
3587
3797
|
nonce
|
|
3588
3798
|
}
|
|
3589
3799
|
}
|
|
@@ -3597,7 +3807,7 @@
|
|
|
3597
3807
|
return res2;
|
|
3598
3808
|
}
|
|
3599
3809
|
const contentType = res2.headers.get("content-type");
|
|
3600
|
-
if (!contentType?.startsWith("application/json")) {
|
|
3810
|
+
if (!res2.ok || !contentType?.startsWith("application/json")) {
|
|
3601
3811
|
return res2;
|
|
3602
3812
|
}
|
|
3603
3813
|
let data = res2.data && typeof res2.data === "object" ? res2.data : null;
|
|
@@ -3606,16 +3816,24 @@
|
|
|
3606
3816
|
data = await res22.json();
|
|
3607
3817
|
}
|
|
3608
3818
|
const id_token = data?.id_token;
|
|
3819
|
+
const isRefresh = await grantTypeOf(req2) == "refresh_token";
|
|
3820
|
+
if (isRefresh && !id_token) {
|
|
3821
|
+
return res2;
|
|
3822
|
+
}
|
|
3609
3823
|
const jwks = await getJwks();
|
|
3610
3824
|
const validation = await validateIdToken(id_token, {
|
|
3611
3825
|
issuer: options.openid_configuration.issuer,
|
|
3612
3826
|
client_id: options.client_info.client_id,
|
|
3613
3827
|
jwks,
|
|
3614
3828
|
openid_configuration: options.openid_configuration,
|
|
3615
|
-
nonce:
|
|
3829
|
+
nonce: isRefresh ? void 0 : userStore.get("pending_nonce")
|
|
3616
3830
|
});
|
|
3617
|
-
|
|
3618
|
-
|
|
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);
|
|
3619
3837
|
return res2;
|
|
3620
3838
|
};
|
|
3621
3839
|
const getJwks = async () => {
|
|
@@ -3630,7 +3848,6 @@
|
|
|
3630
3848
|
if (options.use_dpop) {
|
|
3631
3849
|
const dpopOptions = {
|
|
3632
3850
|
site: options.issuer,
|
|
3633
|
-
authorization_endpoint: options.openid_configuration.authorization_endpoint,
|
|
3634
3851
|
token_endpoint: options.openid_configuration.token_endpoint,
|
|
3635
3852
|
dpop_signing_alg_values_supported: options.openid_configuration.dpop_signing_alg_values_supported
|
|
3636
3853
|
};
|
|
@@ -3652,6 +3869,52 @@
|
|
|
3652
3869
|
const storedRedirectUris = new Set(storedClientInfo.redirect_uris || []);
|
|
3653
3870
|
return requestedClientInfo.redirect_uris.every((uri) => storedRedirectUris.has(uri));
|
|
3654
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
|
+
}
|
|
3655
3918
|
function isRedirected2() {
|
|
3656
3919
|
return isRedirected();
|
|
3657
3920
|
}
|
|
@@ -3660,7 +3923,7 @@
|
|
|
3660
3923
|
if (!options.issuer) {
|
|
3661
3924
|
throw metroError("Must supply options.issuer or options.store to get the id_token");
|
|
3662
3925
|
}
|
|
3663
|
-
options.store = oidcStore(options.issuer);
|
|
3926
|
+
options.store = oidcStore(siteFor(options.issuer, accountFor(options)));
|
|
3664
3927
|
}
|
|
3665
3928
|
return options.store.get("id_token");
|
|
3666
3929
|
}
|
|
@@ -3669,7 +3932,7 @@
|
|
|
3669
3932
|
if (!options.issuer) {
|
|
3670
3933
|
throw metroError("Must supply options.issuer or options.store to get the id_token claims");
|
|
3671
3934
|
}
|
|
3672
|
-
options.store = oidcStore(options.issuer);
|
|
3935
|
+
options.store = oidcStore(siteFor(options.issuer, accountFor(options)));
|
|
3673
3936
|
}
|
|
3674
3937
|
return options.store.get("id_token_claims");
|
|
3675
3938
|
}
|