@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/docs/reference.md CHANGED
@@ -29,11 +29,23 @@ const api = client('https://example.solidcommunity.net/')
29
29
  }))
30
30
  ```
31
31
 
32
- Adds OpenID Connect authorization to a Metro client. By default it tries the request first and authorizes after a `401` or `403`. Set `force_authorization: true` to authorize immediately.
32
+ Adds OpenID Connect authorization to a Metro client. By default it tries the request first and authorizes after a `401` or `403`. Set `force_authorization: true` to authorize immediately. `oidcmw()` checks the status of the response itself, so add middleware that turns responses into errors or data, such as `thrower()` and `getdata()`, after it: `client.with(oidcmw(options)).with(thrower())`.
33
33
 
34
- Important options: `issuer`, `client_info`, `client`, `openid_configuration`, `oauth2`, `store`, `scope`, `nonce`, `use_dpop`, `force_authorization`, and `authorize_callback`.
34
+ Important options: `issuer`, `client_info`, `webid`, `login_hint`, `expected_claims`, `client`, `openid_configuration`, `oauth2`, `store`, `scope`, `nonce`, `use_dpop`, `force_authorization`, and `authorize_callback`.
35
35
 
36
- `use_dpop` defaults to `true`. Disable it only for providers or tests that do not support DPoP.
36
+ `use_dpop` defaults to `true`. Disable it only for providers or tests that do not support DPoP. With `use_dpop`, only DPoP-bound access tokens are accepted: a Bearer token from the issuer is refused, as RFC 9449 section 5.10 requires when DPoP protection matters.
37
+
38
+ ### Which user
39
+
40
+ Tokens are stored per issuer. To keep different users at the same issuer apart, tell `oidcmw()` which user it acts for:
41
+
42
+ - `webid`: the user's WebID. It is sent to the issuer as `login_hint`, and the ID token's `webid` claim must match it. Older Solid issuers without a `webid` claim are matched on `sub`.
43
+ - `login_hint`: without a WebID, a hint for the issuer about which user should log in.
44
+ - `expected_claims`: without a WebID, claims the ID token must contain, e.g. `{ sub: '…' }`.
45
+
46
+ With `webid` or `login_hint`, tokens and the ID token are stored for that user only, so logging in as another user never reuses them. An ID token for a different user than expected is refused and not stored, even when the issuer still has a login session for that other user. Pass the same `webid` or `login_hint` to `idToken()` and `idTokenClaims()`.
47
+
48
+ Without a `scope` option, `oidcmw()` requests `openid webid` when a `webid` is given or the issuer lists `webid` in its `scopes_supported`: Solid-OIDC issuers only include the `webid` claim for that scope. Otherwise it requests `openid`.
37
49
 
38
50
  ## `discover(options)`
39
51
 
@@ -43,6 +55,8 @@ const config = await discover({ issuer: 'https://solidcommunity.net/' })
43
55
 
44
56
  Fetches OIDC discovery metadata. Pass a Metro client with `client` when you want custom middleware or tests.
45
57
 
58
+ The metadata must be for the requested issuer (only a trailing slash may differ), must contain `authorization_endpoint`, `token_endpoint` and `jwks_uri`, and the issuer and all its endpoints must use `https`. Plain `http` is only accepted on the local machine (`localhost`, `127.0.0.1`, `[::1]`). These checks always run; the complete metadata schema is only checked when assertions are enabled.
59
+
46
60
  ## `register(options)`
47
61
 
48
62
  ```js
@@ -55,7 +69,7 @@ const info = await register({
55
69
  })
56
70
  ```
57
71
 
58
- Performs dynamic client registration and returns client information. `oidcmw()` calls this automatically when no `client_info.client_id` is present and the issuer supports registration.
72
+ Performs dynamic client registration and returns client information. `oidcmw()` calls this automatically when no `client_info.client_id` is present and the issuer supports registration. It then registers a public client: it asks for `token_endpoint_auth_method: 'none'`, `grant_types: ['authorization_code', 'refresh_token']` and `response_types: ['code']`, unless `client_info` says otherwise, and it uses the authentication method the issuer registered. A public client's secret, if an issuer returns one anyway, is not stored.
59
73
 
60
74
  ## ID token helpers
61
75
 
@@ -64,7 +78,7 @@ const raw = idToken({ issuer: 'https://solidcommunity.net/' })
64
78
  const claims = idTokenClaims({ issuer: 'https://solidcommunity.net/' })
65
79
  ```
66
80
 
67
- Returns the stored raw ID token or validated claims. Pass the same `issuer` or `store` used by the middleware.
81
+ Returns the stored raw ID token or validated claims. Pass the same `issuer` and `webid` or `login_hint`, or the same `store`, used by the middleware.
68
82
 
69
83
  ## `validateIdToken(idToken, options)`
70
84
 
@@ -95,3 +109,7 @@ import oidcmockserver from '@muze-nl/metro-oidc/testing'
95
109
  ```
96
110
 
97
111
  The mock server exposes discovery metadata, dynamic client registration, JWKS, signed ID tokens, userinfo, and OAuth2-backed protected-resource behaviour for tests.
112
+
113
+ The middleware captures the token request grant type before sending the request.
114
+ ID-token validation and refresh handling therefore work with transports that
115
+ consume request bodies; callers do not need to buffer or replay token POSTs.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muze-nl/metro-oidc",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "oidc client middleware for @muze-nl/metro",
5
5
  "type": "module",
6
6
  "source": [
@@ -30,10 +30,13 @@ export default async function oidcDiscovery(options={}) {
30
30
 
31
31
  options = Object.assign({},defaultOptions,options)
32
32
  options.client = options.client.with(throwermw()).with(jsonmw())
33
+ requireSecureURL('issuer', options.issuer)
33
34
 
34
- const TestSucceeded = false
35
35
  function MustUseHTTPS(url) {
36
- return TestSucceeded // FIXME todo
36
+ if (isSecureURL(url)) {
37
+ return false
38
+ }
39
+ return error('url must use https', url)
37
40
  }
38
41
 
39
42
  // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
@@ -91,9 +94,89 @@ export default async function oidcDiscovery(options={}) {
91
94
  configURL
92
95
  )
93
96
  const openid_config = response.data
97
+ // the full metadata schema is a diagnostic, only checked when assertions
98
+ // are enabled; the checks that protect the login always run
94
99
  assert(openid_config, openid_provider_metadata)
95
-
96
- // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationValidation
97
- assert(openid_config.issuer, options.issuer)
100
+ checkProviderMetadata(openid_config, options.issuer)
98
101
  return openid_config
102
+ }
103
+
104
+ const REQUIRED_ENDPOINTS = ['authorization_endpoint', 'token_endpoint', 'jwks_uri']
105
+
106
+ /**
107
+ * The metadata the login depends on, checked whether or not assertions are
108
+ * enabled: the document must describe the issuer it was fetched for
109
+ * (Discovery section 4.3), so iss checks compare against the right issuer,
110
+ * and every endpoint that receives codes, tokens or keys must use https.
111
+ */
112
+ function checkProviderMetadata(config, issuer)
113
+ {
114
+ if (!config || typeof config !== 'object') {
115
+ throw metro.metroError('metro.oidc.discovery: openid-configuration for '+issuer+' is not a JSON object')
116
+ }
117
+ if (!sameIssuer(config.issuer, issuer)) {
118
+ throw metro.metroError('metro.oidc.discovery: openid-configuration is for issuer '+config.issuer+', expected '+issuer)
119
+ }
120
+ requireSecureURL('issuer', config.issuer)
121
+ for (const name of REQUIRED_ENDPOINTS) {
122
+ if (!config[name]) {
123
+ throw metro.metroError('metro.oidc.discovery: openid-configuration for '+issuer+' has no '+name)
124
+ }
125
+ requireSecureURL(name, config[name])
126
+ }
127
+ if (config.registration_endpoint) {
128
+ requireSecureURL('registration_endpoint', config.registration_endpoint)
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Issuer identifiers must be identical. The only difference allowed is a
134
+ * trailing slash, which WebID profiles and discovery documents often
135
+ * disagree on.
136
+ */
137
+ function sameIssuer(discovered, requested)
138
+ {
139
+ if (typeof discovered != 'string') {
140
+ return false
141
+ }
142
+ return withoutTrailingSlash(discovered) === withoutTrailingSlash(String(requested))
143
+ }
144
+
145
+ function withoutTrailingSlash(value)
146
+ {
147
+ return value.replace(/\/$/, '')
148
+ }
149
+
150
+ function requireSecureURL(name, value)
151
+ {
152
+ if (!isSecureURL(value)) {
153
+ throw metro.metroError('metro.oidc.discovery: '+name+' must use https: '+value)
154
+ }
155
+ }
156
+
157
+ /**
158
+ * https is required; plain http is only accepted on the local machine, for
159
+ * development servers.
160
+ */
161
+ function isSecureURL(value)
162
+ {
163
+ let url
164
+ try {
165
+ url = new URL(String(value))
166
+ }
167
+ catch(e) {
168
+ return false
169
+ }
170
+ if (url.protocol == 'https:') {
171
+ return true
172
+ }
173
+ return url.protocol == 'http:' && isLoopback(url.hostname)
174
+ }
175
+
176
+ function isLoopback(hostname)
177
+ {
178
+ return hostname == 'localhost'
179
+ || hostname.endsWith('.localhost')
180
+ || hostname == '127.0.0.1'
181
+ || hostname == '[::1]'
99
182
  }
@@ -73,7 +73,12 @@ export default function oidcmockserver(options = {}) {
73
73
  client_secret: options.client_secret,
74
74
  redirect_uri: options.redirect_uri,
75
75
  scope: 'openid profile email',
76
- id_token: tokenContext => idToken(tokenContext),
76
+ id_token: tokenContext => {
77
+ if (tokenContext.grant_type == 'refresh_token' && options.refreshIdToken === false) {
78
+ return undefined
79
+ }
80
+ return idToken(tokenContext)
81
+ },
77
82
  requirePKCE: options.requirePKCE ?? false
78
83
  })
79
84
  const clients = new Map()
@@ -109,7 +114,7 @@ export default function oidcmockserver(options = {}) {
109
114
  userinfo_endpoint: endpoint('/userinfo/'),
110
115
  jwks_uri: endpoint('/jwks/'),
111
116
  registration_endpoint: endpoint('/register/'),
112
- scopes_supported: ['openid', 'profile', 'email'],
117
+ scopes_supported: options.scopes_supported ?? ['openid', 'profile', 'email'],
113
118
  response_types_supported: ['code', 'id_token', 'id_token token'],
114
119
  grant_types_supported: ['authorization_code', 'refresh_token'],
115
120
  subject_types_supported: ['public'],
package/src/oidcmw.mjs CHANGED
@@ -9,6 +9,30 @@ import jsonmw from '@muze-nl/metro-middleware/json'
9
9
  import throwermw from '@muze-nl/metro-middleware/thrower'
10
10
  import { validateIdToken } from './oidc.jwt.mjs'
11
11
 
12
+ const pendingClientSetups = new Map()
13
+
14
+ /**
15
+ * A browser app cannot keep a client secret, so it registers as a public
16
+ * client (Solid-OIDC section 11.3: secrets should not be stored in browser
17
+ * storage). The refresh_token grant is requested so issuers hand out refresh
18
+ * tokens. Metadata supplied by the app takes precedence.
19
+ */
20
+ const BROWSER_CLIENT_METADATA = {
21
+ token_endpoint_auth_method: 'none',
22
+ grant_types: ['authorization_code', 'refresh_token'],
23
+ response_types: ['code']
24
+ }
25
+
26
+ function sharedClientSetup(key, setup) {
27
+ if (!pendingClientSetups.has(key)) {
28
+ const pending = setup().finally(() => {
29
+ pendingClientSetups.delete(key)
30
+ })
31
+ pendingClientSetups.set(key, pending)
32
+ }
33
+ return pendingClientSetups.get(key)
34
+ }
35
+
12
36
  export default function oidcmw(options={}) {
13
37
 
14
38
  const defaultOptions = {
@@ -34,8 +58,18 @@ export default function oidcmw(options={}) {
34
58
  openid_configuration: Optional()
35
59
  })
36
60
 
61
+ // Discovery and client registration belong to the issuer. Tokens belong to
62
+ // the user they were issued for, so a login as one user can never reuse
63
+ // another user's tokens at the same issuer.
64
+ const account = accountFor(options)
65
+ const expectedClaims = expectedClaimsFor(options)
66
+ const site = siteFor(options.issuer, account)
67
+ const clientSetupKey = options.issuer + ' '
68
+ + JSON.stringify(requestedClientInfo?.redirect_uris ?? null)
69
+ let userStore = options.store
37
70
  if (!options.store) {
38
71
  options.store = oidcStore(options.issuer)
72
+ userStore = oidcStore(site)
39
73
  }
40
74
  if (!options.openid_configuration && options.store.has('openid_configuration')) {
41
75
  options.openid_configuration = options.store.get('openid_configuration')
@@ -47,56 +81,95 @@ export default function oidcmw(options={}) {
47
81
  }
48
82
  }
49
83
 
50
- return async (req, next) => {
51
- let res
52
- if (!options.force_authorization) {
53
- try {
54
- res = await next(req)
55
- } catch(err) {
56
- res = err?.cause
57
- if (!res || (res.status!=401 && res.status!=403)) {
58
- throw err
59
- }
60
- }
61
- if (res.ok || (res.status!=401 && res.status!=403)) {
62
- return res
63
- }
84
+ /**
85
+ * Discovers the issuer and registers this client once. Concurrent requests
86
+ * wait for the same registration, so tokens are never issued to one
87
+ * registered client while another client_id is stored.
88
+ */
89
+ async function prepareClient()
90
+ {
91
+ if (options.openid_configuration && options.client_info?.client_id) {
92
+ return
64
93
  }
65
- if (!options.openid_configuration) {
66
- options.openid_configuration = await discover({
94
+ const prepared = await sharedClientSetup(clientSetupKey, discoverAndRegister)
95
+ options.openid_configuration = prepared.openid_configuration
96
+ options.client_info = prepared.client_info
97
+ options.store.set('openid_configuration', options.openid_configuration)
98
+ options.store.set('client_info', options.client_info)
99
+ }
100
+
101
+ async function discoverAndRegister()
102
+ {
103
+ let openid_configuration = options.openid_configuration
104
+ if (!openid_configuration) {
105
+ openid_configuration = await discover({
67
106
  issuer: options.issuer,
68
107
  client: options.client.with(options.issuer)
69
108
  })
70
- options.store.set('openid_configuration', options.openid_configuration)
71
109
  }
72
-
73
- if (!options.client_info?.client_id) {
74
- if (!options.openid_configuration.registration_endpoint) {
110
+ let client_info = options.client_info
111
+ if (!client_info?.client_id) {
112
+ if (!openid_configuration.registration_endpoint) {
75
113
  throw metro.metroError('metro.oidcmw: Error: issuer '+options.issuer+' does not support dynamic client registration, but you haven\'t specified a client_id')
76
114
  }
77
- options.client_info = await register({
78
- registration_endpoint: options.openid_configuration.registration_endpoint,
115
+ client_info = await register({
116
+ registration_endpoint: openid_configuration.registration_endpoint,
79
117
  client: options.client,
80
- client_info: options.client_info
118
+ client_info: Object.assign({}, BROWSER_CLIENT_METADATA, client_info)
81
119
  })
82
- options.store.set('client_info', options.client_info)
120
+ if (client_info.token_endpoint_auth_method == 'none') {
121
+ // a public client has no use for a secret, so it is not stored
122
+ delete client_info.client_secret
123
+ }
124
+ }
125
+ return { openid_configuration, client_info }
126
+ }
127
+
128
+ /**
129
+ * The id_token must carry the nonce of the authorization request that
130
+ * produced it. Storing the nonce when that request starts means a request
131
+ * waiting for someone else's login, or the first request after a redirect
132
+ * back from the issuer, cannot replace it.
133
+ */
134
+ function rememberNonce(authorizeCallback)
135
+ {
136
+ if (typeof authorizeCallback != 'function') {
137
+ return authorizeCallback
138
+ }
139
+ return async url => {
140
+ userStore.set('pending_nonce', url.searchParams.get('nonce'))
141
+ return authorizeCallback(url)
142
+ }
143
+ }
144
+
145
+ return async (req, next) => {
146
+ let res
147
+ if (!options.force_authorization) {
148
+ // Middleware that turns responses into errors or data, such as
149
+ // thrower and getdata, belongs outside oidcmw, so the status of the
150
+ // actual response is visible here.
151
+ res = await next(req)
152
+ if (res.ok || (res.status!=401 && res.status!=403)) {
153
+ return res
154
+ }
83
155
  }
156
+ await prepareClient()
84
157
 
85
158
  // now initialize an oauth2 client stack, using options.client as default
86
159
  // with forceAuthentication: true
87
- const scope = options.scope || 'openid'
160
+ const scope = options.scope || defaultScope(options)
88
161
  const nonce = options.nonce || oauth2.generateCodeVerifier(32)
89
- options.store.set('pending_nonce', nonce)
90
162
 
91
163
  const oauth2Options = Object.assign(
92
164
  {
93
- site: options.issuer,
165
+ site,
94
166
  client: options.client,
95
167
  force_authorization: true,
96
- authorize_callback: options.authorize_callback,
168
+ authorize_callback: rememberNonce(options.authorize_callback),
97
169
  oauth2_configuration: {
98
170
  client_id: options.client_info?.client_id,
99
171
  client_secret: options.client_info?.client_secret,
172
+ token_endpoint_auth_method: options.client_info?.token_endpoint_auth_method,
100
173
  grant_type: 'authorization_code',
101
174
  response_type: 'code',
102
175
  response_mode: 'query',
@@ -104,6 +177,11 @@ export default function oidcmw(options={}) {
104
177
  token_endpoint: options.openid_configuration.token_endpoint,
105
178
  scope, //FIXME: should only use scopes supported by server
106
179
  redirect_uri: options.client_info.redirect_uris[0],
180
+ login_hint: options.login_hint ?? options.webid,
181
+ issuer: options.openid_configuration.issuer,
182
+ authorization_response_iss_parameter_supported:
183
+ options.openid_configuration.authorization_response_iss_parameter_supported,
184
+ token_type: options.use_dpop ? 'DPoP' : undefined,
107
185
  nonce
108
186
  }
109
187
  }
@@ -111,34 +189,45 @@ export default function oidcmw(options={}) {
111
189
  )
112
190
 
113
191
  const storeIdToken = async (req, next) => {
114
- const res = await next(req)
115
192
  const tokenEndpoint = metro.url(options.openid_configuration.token_endpoint, { hash: '' }).href
116
193
  const requestUrl = metro.url(req.url, { hash: '' }).href
117
194
  if (requestUrl !== tokenEndpoint) {
118
- return res
195
+ return next(req)
119
196
  }
197
+ // Fetch consumes the outgoing body; inspect the grant before dispatch.
198
+ const isRefresh = await grantTypeOf(req) == 'refresh_token'
199
+ const res = await next(req)
120
200
  const contentType = res.headers.get('content-type')
121
- if (!contentType?.startsWith('application/json')) {
201
+ if (!res.ok || !contentType?.startsWith('application/json')) {
202
+ // oauth2mw reports the token endpoint's own error
122
203
  return res
123
204
  }
124
205
 
125
- let data = res.data && typeof res.data === 'object' ? res.data : null
126
- if (!data) {
206
+ let data = res.data
207
+ if (!data || typeof data !== 'object' || data instanceof ReadableStream) {
127
208
  const res2 = res.clone() // otherwise res.body can't be read again
128
209
  data = await res2.json()
129
210
  }
130
211
 
131
212
  const id_token = data?.id_token
213
+ if (isRefresh && !id_token) {
214
+ // a refresh response may omit the id_token; the stored one stays
215
+ return res
216
+ }
132
217
  const jwks = await getJwks()
133
218
  const validation = await validateIdToken(id_token, {
134
219
  issuer: options.openid_configuration.issuer,
135
220
  client_id: options.client_info.client_id,
136
221
  jwks,
137
222
  openid_configuration: options.openid_configuration,
138
- nonce: options.store.get('pending_nonce')
223
+ nonce: isRefresh ? undefined : userStore.get('pending_nonce')
139
224
  })
140
- options.store.set('id_token', id_token)
141
- options.store.set('id_token_claims', validation.claims)
225
+ if (isRefresh) {
226
+ assertSameUserAsLogin(validation.claims, userStore.get('id_token_claims'))
227
+ }
228
+ assertExpectedUser(validation.claims, expectedClaims)
229
+ userStore.set('id_token', id_token)
230
+ userStore.set('id_token_claims', validation.claims)
142
231
  return res
143
232
  }
144
233
 
@@ -156,7 +245,6 @@ export default function oidcmw(options={}) {
156
245
  if (options.use_dpop) {
157
246
  const dpopOptions = {
158
247
  site: options.issuer,
159
- authorization_endpoint: options.openid_configuration.authorization_endpoint,
160
248
  token_endpoint: options.openid_configuration.token_endpoint,
161
249
  dpop_signing_alg_values_supported: options.openid_configuration.dpop_signing_alg_values_supported
162
250
  }
@@ -184,6 +272,83 @@ function clientInfoMatchesRequest(storedClientInfo, requestedClientInfo) {
184
272
  return requestedClientInfo.redirect_uris.every(uri => storedRedirectUris.has(uri))
185
273
  }
186
274
 
275
+ /**
276
+ * A WebID identifies the user when available (Solid-OIDC puts it in the
277
+ * webid claim); otherwise login_hint and expected_claims do.
278
+ */
279
+ function accountFor(options) {
280
+ return options.webid ?? options.login_hint
281
+ }
282
+
283
+ function expectedClaimsFor(options) {
284
+ if (options.webid) {
285
+ return Object.assign({ webid: options.webid }, options.expected_claims)
286
+ }
287
+ return options.expected_claims || {}
288
+ }
289
+
290
+ /**
291
+ * Solid-OIDC issuers only include the webid claim when the webid scope is
292
+ * requested (Solid-OIDC section 7). It is requested when a WebID is given or
293
+ * the issuer lists it in scopes_supported, which marks a Solid-OIDC issuer.
294
+ */
295
+ function defaultScope(options) {
296
+ const supported = options.openid_configuration?.scopes_supported
297
+ const solidIssuer = Array.isArray(supported) && supported.includes('webid')
298
+ if (options.webid || solidIssuer) {
299
+ return 'openid webid'
300
+ }
301
+ return 'openid'
302
+ }
303
+
304
+ function siteFor(issuer, account) {
305
+ if (!account) {
306
+ return issuer
307
+ }
308
+ return issuer + '|' + account
309
+ }
310
+
311
+ /**
312
+ * Refuses tokens issued for another user than the one requested, e.g. when
313
+ * the issuer still has a login session for a different account.
314
+ */
315
+ function assertExpectedUser(claims, expectedClaims) {
316
+ for (const [name, expected] of Object.entries(expectedClaims)) {
317
+ const actual = claimValue(claims, name)
318
+ if (actual !== expected) {
319
+ throw metro.metroError('metro.oidcmw: id_token is for a different user: '
320
+ + 'expected ' + name + ' ' + expected + ', got ' + actual)
321
+ }
322
+ }
323
+ }
324
+
325
+ async function grantTypeOf(req) {
326
+ const body = await req.clone().text()
327
+ return new URLSearchParams(body).get('grant_type')
328
+ }
329
+
330
+ /**
331
+ * An id_token from a refresh has no nonce of its own. It must describe the
332
+ * same login: same issuer (checked by validateIdToken) and same subject.
333
+ */
334
+ function assertSameUserAsLogin(claims, loginClaims) {
335
+ if (loginClaims && claims.sub !== loginClaims.sub) {
336
+ throw metro.metroError('metro.oidcmw: refreshed id_token is for a different user: '
337
+ + 'expected sub ' + loginClaims.sub + ', got ' + claims.sub)
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Solid-OIDC issuers put the WebID in the webid claim; older issuers only
343
+ * use sub for it.
344
+ */
345
+ function claimValue(claims, name) {
346
+ if (name == 'webid' && claims.webid === undefined) {
347
+ return claims.sub
348
+ }
349
+ return claims[name]
350
+ }
351
+
187
352
  export function isRedirected() {
188
353
  return oauth2.isRedirected()
189
354
  }
@@ -193,7 +358,7 @@ export function idToken(options) {
193
358
  if (!options.issuer) {
194
359
  throw metro.metroError('Must supply options.issuer or options.store to get the id_token')
195
360
  }
196
- options.store = oidcStore(options.issuer)
361
+ options.store = oidcStore(siteFor(options.issuer, accountFor(options)))
197
362
  }
198
363
  return options.store.get('id_token')
199
364
  }
@@ -203,7 +368,7 @@ export function idTokenClaims(options) {
203
368
  if (!options.issuer) {
204
369
  throw metro.metroError('Must supply options.issuer or options.store to get the id_token claims')
205
370
  }
206
- options.store = oidcStore(options.issuer)
371
+ options.store = oidcStore(siteFor(options.issuer, accountFor(options)))
207
372
  }
208
373
  return options.store.get('id_token_claims')
209
374
  }