@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 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
- let next;
122
- for (let middleware of middlewares) {
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.span(name, fn, data, context);
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
- return response("false");
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
- return response("false");
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
- const token = refreshToken ? await refreshAccessToken() : await fetchAccessToken();
1314
+ let tokenRequest = fetchAccessToken;
1315
+ if (refreshToken) {
1316
+ tokenRequest = refreshAccessToken;
1317
+ }
1318
+ const token = await requestToken(tokenRequest);
1290
1319
  if (!token) {
1291
- return response("false");
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 true;
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 keys = await keysStore();
1915
- let keyInfo = await keys.get(options.site);
1916
- if (!keyInfo) {
1917
- let keyPair = await generateKeyPair("ES256");
1918
- keyInfo = { domain: options.site, keyPair };
1919
- await keys.set(keyInfo);
1920
- }
1921
- const url2 = url(req.url);
1922
- if (req.url.startsWith(options.authorization_endpoint)) {
1923
- let params = req.body;
1924
- if (params instanceof URLSearchParams || params instanceof FormData) {
1925
- params.set("dpop_jkt", keyInfo.keyPair.publicKey);
1926
- } else {
1927
- params.dpop_jkt = keyInfo.keyPair.publicKey;
1928
- }
1929
- } else if (req.url.startsWith(options.token_endpoint)) {
1930
- const dpopHeader = await generateProof(keyInfo.keyPair, req.url, req.method);
1931
- req = req.with({
1932
- headers: {
1933
- "DPoP": dpopHeader
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
- "Authorization": "DPoP " + accessToken,
1943
- "DPoP": dpopHeader
2003
+ "DPoP": proof2
1944
2004
  }
1945
2005
  });
1946
2006
  }
1947
- let response2 = await next(req);
1948
- if (response2.headers.get("DPoP-Nonce")) {
1949
- localStorage.setItem(url2.host + ":nonce", response2.headers.get("DPoP-Nonce"));
1950
- }
1951
- return response2;
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
- const TestSucceeded = false;
2189
+ requireSecureURL("issuer", options.issuer);
2088
2190
  function MustUseHTTPS(url2) {
2089
- return TestSucceeded;
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
- assert(openid_config.issuer, options.issuer);
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
- return async (req, next) => {
3548
- let res;
3549
- if (!options.force_authorization) {
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
- if (!options.openid_configuration) {
3563
- options.openid_configuration = await oidcDiscovery({
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
- if (!options.client_info?.client_id) {
3570
- if (!options.openid_configuration.registration_endpoint) {
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
- options.client_info = await register({
3574
- registration_endpoint: options.openid_configuration.registration_endpoint,
3744
+ client_info = await register({
3745
+ registration_endpoint: openid_configuration.registration_endpoint,
3575
3746
  client: options.client,
3576
- client_info: options.client_info
3747
+ client_info: Object.assign({}, BROWSER_CLIENT_METADATA, client_info)
3577
3748
  });
3578
- options.store.set("client_info", options.client_info);
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
- const scope = options.scope || "openid";
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: options.issuer,
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 res2;
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 && typeof res2.data === "object" ? res2.data : null;
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: options.store.get("pending_nonce")
3829
+ nonce: isRefresh ? void 0 : userStore.get("pending_nonce")
3629
3830
  });
3630
- options.store.set("id_token", id_token);
3631
- options.store.set("id_token_claims", validation.claims);
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
  }