@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 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;
@@ -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
- tracer[method].call(tracer, ...args);
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
- return response("false");
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
- return response("false");
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
- const token = refreshToken ? await refreshAccessToken() : await fetchAccessToken();
1314
+ let tokenRequest = fetchAccessToken;
1315
+ if (refreshToken) {
1316
+ tokenRequest = refreshAccessToken;
1317
+ }
1318
+ const token = await requestToken(tokenRequest);
1277
1319
  if (!token) {
1278
- return response("false");
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 true;
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 keys = await keysStore();
1902
- let keyInfo = await keys.get(options.site);
1903
- if (!keyInfo) {
1904
- let keyPair = await generateKeyPair("ES256");
1905
- keyInfo = { domain: options.site, keyPair };
1906
- await keys.set(keyInfo);
1907
- }
1908
- const url2 = url(req.url);
1909
- if (req.url.startsWith(options.authorization_endpoint)) {
1910
- let params = req.body;
1911
- if (params instanceof URLSearchParams || params instanceof FormData) {
1912
- params.set("dpop_jkt", keyInfo.keyPair.publicKey);
1913
- } else {
1914
- params.dpop_jkt = keyInfo.keyPair.publicKey;
1915
- }
1916
- } else if (req.url.startsWith(options.token_endpoint)) {
1917
- const dpopHeader = await generateProof(keyInfo.keyPair, req.url, req.method);
1918
- req = req.with({
1919
- headers: {
1920
- "DPoP": dpopHeader
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
- "Authorization": "DPoP " + accessToken,
1930
- "DPoP": dpopHeader
2003
+ "DPoP": proof2
1931
2004
  }
1932
2005
  });
1933
2006
  }
1934
- let response2 = await next(req);
1935
- if (response2.headers.get("DPoP-Nonce")) {
1936
- localStorage.setItem(url2.host + ":nonce", response2.headers.get("DPoP-Nonce"));
1937
- }
1938
- return response2;
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
- const TestSucceeded = false;
2189
+ requireSecureURL("issuer", options.issuer);
2075
2190
  function MustUseHTTPS(url2) {
2076
- return TestSucceeded;
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
- assert(openid_config.issuer, options.issuer);
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
- return async (req, next) => {
3535
- let res;
3536
- if (!options.force_authorization) {
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
- if (!options.openid_configuration) {
3550
- 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({
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
- if (!options.client_info?.client_id) {
3557
- 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) {
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
- options.client_info = await register({
3561
- registration_endpoint: options.openid_configuration.registration_endpoint,
3744
+ client_info = await register({
3745
+ registration_endpoint: openid_configuration.registration_endpoint,
3562
3746
  client: options.client,
3563
- client_info: options.client_info
3747
+ client_info: Object.assign({}, BROWSER_CLIENT_METADATA, client_info)
3564
3748
  });
3565
- 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
+ }
3566
3771
  }
3567
- const scope = options.scope || "openid";
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: options.issuer,
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: options.store.get("pending_nonce")
3829
+ nonce: isRefresh ? void 0 : userStore.get("pending_nonce")
3616
3830
  });
3617
- options.store.set("id_token", id_token);
3618
- 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);
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
  }