@muze-nl/metro-oidc 0.5.6 → 0.8.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
 
@@ -46,7 +49,8 @@ export default function oidcmw(options={}) {
46
49
  try {
47
50
  res = await next(req)
48
51
  } catch(err) {
49
- if (res.status!=401 && res.status!=403) {
52
+ res = err?.cause
53
+ if (!res || (res.status!=401 && res.status!=403)) {
50
54
  throw err
51
55
  }
52
56
  }
@@ -56,7 +60,8 @@ export default function oidcmw(options={}) {
56
60
  }
57
61
  if (!options.openid_configuration) {
58
62
  options.openid_configuration = await discover({
59
- issuer: options.issuer
63
+ issuer: options.issuer,
64
+ client: options.client.with(options.issuer)
60
65
  })
61
66
  options.store.set('openid_configuration', options.openid_configuration)
62
67
  }
@@ -67,6 +72,7 @@ export default function oidcmw(options={}) {
67
72
  }
68
73
  options.client_info = await register({
69
74
  registration_endpoint: options.openid_configuration.registration_endpoint,
75
+ client: options.client,
70
76
  client_info: options.client_info
71
77
  })
72
78
  options.store.set('client_info', options.client_info)
@@ -75,6 +81,8 @@ export default function oidcmw(options={}) {
75
81
  // now initialize an oauth2 client stack, using options.client as default
76
82
  // with forceAuthentication: true
77
83
  const scope = options.scope || 'openid'
84
+ const nonce = options.nonce || oauth2.generateCodeVerifier(32)
85
+ options.store.set('pending_nonce', nonce)
78
86
 
79
87
  const oauth2Options = Object.assign(
80
88
  {
@@ -91,7 +99,8 @@ export default function oidcmw(options={}) {
91
99
  authorization_endpoint: options.openid_configuration.authorization_endpoint,
92
100
  token_endpoint: options.openid_configuration.token_endpoint,
93
101
  scope, //FIXME: should only use scopes supported by server
94
- redirect_uri: options.client_info.redirect_uris[0]
102
+ redirect_uri: options.client_info.redirect_uris[0],
103
+ nonce
95
104
  }
96
105
  }
97
106
  //...
@@ -99,28 +108,45 @@ export default function oidcmw(options={}) {
99
108
 
100
109
  const storeIdToken = async (req, next) => {
101
110
  const res = await next(req)
111
+ const tokenEndpoint = metro.url(options.openid_configuration.token_endpoint, { hash: '' }).href
112
+ const requestUrl = metro.url(req.url, { hash: '' }).href
113
+ if (requestUrl !== tokenEndpoint) {
114
+ return res
115
+ }
102
116
  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
- }
117
+ if (!contentType?.startsWith('application/json')) {
118
+ return res
119
+ }
120
+
121
+ let data = res.data && typeof res.data === 'object' ? res.data : null
122
+ if (!data) {
123
+ const res2 = res.clone() // otherwise res.body can't be read again
124
+ data = await res2.json()
120
125
  }
126
+
127
+ const id_token = data?.id_token
128
+ const jwks = await getJwks()
129
+ const validation = await validateIdToken(id_token, {
130
+ issuer: options.openid_configuration.issuer,
131
+ client_id: options.client_info.client_id,
132
+ jwks,
133
+ openid_configuration: options.openid_configuration,
134
+ nonce: options.store.get('pending_nonce')
135
+ })
136
+ options.store.set('id_token', id_token)
137
+ options.store.set('id_token_claims', validation.claims)
121
138
  return res
122
139
  }
123
140
 
141
+ const getJwks = async () => {
142
+ if (!options.jwks) {
143
+ const jwksClient = options.client.with(throwermw()).with(jsonmw())
144
+ const response = await jwksClient.get(options.openid_configuration.jwks_uri)
145
+ options.jwks = response.data
146
+ }
147
+ return options.jwks
148
+ }
149
+
124
150
  let oauth2client = options.client.with(options.issuer).with(storeIdToken)
125
151
 
126
152
  if (options.use_dpop) {
@@ -131,8 +157,8 @@ export default function oidcmw(options={}) {
131
157
  dpop_signing_alg_values_supported: options.openid_configuration.dpop_signing_alg_values_supported
132
158
  }
133
159
  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
160
  }
161
+ oauth2Options.client = oauth2client // make sure token requests use the OIDC token-observing stack
136
162
 
137
163
  oauth2client = oauth2client.with(oauth2mw(oauth2Options))
138
164
 
@@ -156,3 +182,13 @@ export function idToken(options) {
156
182
  }
157
183
  return options.store.get('id_token')
158
184
  }
185
+
186
+ export function idTokenClaims(options) {
187
+ if (!options.store) {
188
+ if (!options.issuer) {
189
+ throw metro.metroError('Must supply options.issuer or options.store to get the id_token claims')
190
+ }
191
+ options.store = oidcStore(options.issuer)
192
+ }
193
+ return options.store.get('id_token_claims')
194
+ }
@@ -0,0 +1,4 @@
1
+ import oidcmockserver from './oidc.mockserver.mjs'
2
+ export * from './oidc.mockserver.mjs'
3
+ export { oidcmockserver }
4
+ export default oidcmockserver