@muze-nl/metro-oidc 0.6.0 → 0.9.0

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/src/oidcmw.mjs CHANGED
@@ -1,10 +1,13 @@
1
- import * as metro from '@muze-nl/metro/src/metro.mjs'
2
- import oauth2mw, * as oauth2 from '@muze-nl/metro-oauth2/src/oauth2.mjs'
3
- import dpopmw from '@muze-nl/metro-oauth2/src/oauth2.dpop.mjs'
1
+ import * as metro from '@muze-nl/metro-core'
2
+ import oauth2mw, * as oauth2 from '@muze-nl/metro-oauth2/oauth2'
3
+ import dpopmw from '@muze-nl/metro-oauth2/dpop'
4
4
  import { assert, Required, Optional, validURL, instanceOf } from '@muze-nl/assert'
5
5
  import discover from './oidc.discovery.mjs'
6
6
  import register from './oidc.register.mjs'
7
7
  import oidcStore from './oidc.store.mjs'
8
+ import jsonmw from '@muze-nl/metro-middleware/json'
9
+ import throwermw from '@muze-nl/metro-middleware/thrower'
10
+ import { validateIdToken } from './oidc.jwt.mjs'
8
11
 
9
12
  export default function oidcmw(options={}) {
10
13
 
@@ -21,6 +24,7 @@ export default function oidcmw(options={}) {
21
24
  }
22
25
 
23
26
  options = Object.assign({}, defaultOptions, options)
27
+ const requestedClientInfo = options.client_info
24
28
 
25
29
  assert(options, {
26
30
  client: Required(instanceOf(metro.client().constructor)), // required because it is set in defaultOptions
@@ -37,7 +41,10 @@ export default function oidcmw(options={}) {
37
41
  options.openid_configuration = options.store.get('openid_configuration')
38
42
  }
39
43
  if (!options.client_info?.client_id && options.store.has('client_info')) {
40
- options.client_info = options.store.get('client_info')
44
+ const storedClientInfo = options.store.get('client_info')
45
+ if (clientInfoMatchesRequest(storedClientInfo, requestedClientInfo)) {
46
+ options.client_info = storedClientInfo
47
+ }
41
48
  }
42
49
 
43
50
  return async (req, next) => {
@@ -46,7 +53,8 @@ export default function oidcmw(options={}) {
46
53
  try {
47
54
  res = await next(req)
48
55
  } catch(err) {
49
- if (res.status!=401 && res.status!=403) {
56
+ res = err?.cause
57
+ if (!res || (res.status!=401 && res.status!=403)) {
50
58
  throw err
51
59
  }
52
60
  }
@@ -56,7 +64,8 @@ export default function oidcmw(options={}) {
56
64
  }
57
65
  if (!options.openid_configuration) {
58
66
  options.openid_configuration = await discover({
59
- issuer: options.issuer
67
+ issuer: options.issuer,
68
+ client: options.client.with(options.issuer)
60
69
  })
61
70
  options.store.set('openid_configuration', options.openid_configuration)
62
71
  }
@@ -67,6 +76,7 @@ export default function oidcmw(options={}) {
67
76
  }
68
77
  options.client_info = await register({
69
78
  registration_endpoint: options.openid_configuration.registration_endpoint,
79
+ client: options.client,
70
80
  client_info: options.client_info
71
81
  })
72
82
  options.store.set('client_info', options.client_info)
@@ -75,6 +85,8 @@ export default function oidcmw(options={}) {
75
85
  // now initialize an oauth2 client stack, using options.client as default
76
86
  // with forceAuthentication: true
77
87
  const scope = options.scope || 'openid'
88
+ const nonce = options.nonce || oauth2.generateCodeVerifier(32)
89
+ options.store.set('pending_nonce', nonce)
78
90
 
79
91
  const oauth2Options = Object.assign(
80
92
  {
@@ -91,7 +103,8 @@ export default function oidcmw(options={}) {
91
103
  authorization_endpoint: options.openid_configuration.authorization_endpoint,
92
104
  token_endpoint: options.openid_configuration.token_endpoint,
93
105
  scope, //FIXME: should only use scopes supported by server
94
- redirect_uri: options.client_info.redirect_uris[0]
106
+ redirect_uri: options.client_info.redirect_uris[0],
107
+ nonce
95
108
  }
96
109
  }
97
110
  //...
@@ -99,28 +112,45 @@ export default function oidcmw(options={}) {
99
112
 
100
113
  const storeIdToken = async (req, next) => {
101
114
  const res = await next(req)
115
+ const tokenEndpoint = metro.url(options.openid_configuration.token_endpoint, { hash: '' }).href
116
+ const requestUrl = metro.url(req.url, { hash: '' }).href
117
+ if (requestUrl !== tokenEndpoint) {
118
+ return res
119
+ }
102
120
  const contentType = res.headers.get('content-type')
103
- if (contentType?.startsWith('application/json')) {
104
- //FIXME: check that this is actually the token endpoint
105
- let id_token = res.data?.id_token
106
- if (!id_token) {
107
- const res2 = res.clone() // otherwise res.body can't be read again
108
- try {
109
- let data = await res2.json()
110
- if (data && data.id_token) {
111
- id_token = data.id_token
112
- }
113
- } catch(e) {
114
- // ignore errors
115
- }
116
- }
117
- if (id_token) {
118
- options.store.set('id_token', id_token)
119
- }
121
+ if (!contentType?.startsWith('application/json')) {
122
+ return res
120
123
  }
124
+
125
+ let data = res.data && typeof res.data === 'object' ? res.data : null
126
+ if (!data) {
127
+ const res2 = res.clone() // otherwise res.body can't be read again
128
+ data = await res2.json()
129
+ }
130
+
131
+ const id_token = data?.id_token
132
+ const jwks = await getJwks()
133
+ const validation = await validateIdToken(id_token, {
134
+ issuer: options.openid_configuration.issuer,
135
+ client_id: options.client_info.client_id,
136
+ jwks,
137
+ openid_configuration: options.openid_configuration,
138
+ nonce: options.store.get('pending_nonce')
139
+ })
140
+ options.store.set('id_token', id_token)
141
+ options.store.set('id_token_claims', validation.claims)
121
142
  return res
122
143
  }
123
144
 
145
+ const getJwks = async () => {
146
+ if (!options.jwks) {
147
+ const jwksClient = options.client.with(throwermw()).with(jsonmw())
148
+ const response = await jwksClient.get(options.openid_configuration.jwks_uri)
149
+ options.jwks = response.data
150
+ }
151
+ return options.jwks
152
+ }
153
+
124
154
  let oauth2client = options.client.with(options.issuer).with(storeIdToken)
125
155
 
126
156
  if (options.use_dpop) {
@@ -131,8 +161,8 @@ export default function oidcmw(options={}) {
131
161
  dpop_signing_alg_values_supported: options.openid_configuration.dpop_signing_alg_values_supported
132
162
  }
133
163
  oauth2client = oauth2client.with(dpopmw(dpopOptions)) // add DPoP headers in requests with Authorization headers
134
- oauth2Options.client = oauth2client // make sure oath2 token request use dpop
135
164
  }
165
+ oauth2Options.client = oauth2client // make sure token requests use the OIDC token-observing stack
136
166
 
137
167
  oauth2client = oauth2client.with(oauth2mw(oauth2Options))
138
168
 
@@ -143,6 +173,17 @@ export default function oidcmw(options={}) {
143
173
 
144
174
  }
145
175
 
176
+ function clientInfoMatchesRequest(storedClientInfo, requestedClientInfo) {
177
+ if (!storedClientInfo?.client_id) {
178
+ return false
179
+ }
180
+ if (!Array.isArray(requestedClientInfo?.redirect_uris)) {
181
+ return true
182
+ }
183
+ const storedRedirectUris = new Set(storedClientInfo.redirect_uris || [])
184
+ return requestedClientInfo.redirect_uris.every(uri => storedRedirectUris.has(uri))
185
+ }
186
+
146
187
  export function isRedirected() {
147
188
  return oauth2.isRedirected()
148
189
  }
@@ -156,3 +197,13 @@ export function idToken(options) {
156
197
  }
157
198
  return options.store.get('id_token')
158
199
  }
200
+
201
+ export function idTokenClaims(options) {
202
+ if (!options.store) {
203
+ if (!options.issuer) {
204
+ throw metro.metroError('Must supply options.issuer or options.store to get the id_token claims')
205
+ }
206
+ options.store = oidcStore(options.issuer)
207
+ }
208
+ return options.store.get('id_token_claims')
209
+ }
@@ -0,0 +1,4 @@
1
+ import oidcmockserver from './oidc.mockserver.mjs'
2
+ export * from './oidc.mockserver.mjs'
3
+ export { oidcmockserver }
4
+ export default oidcmockserver