@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/LICENSE +1 -1
- package/README.md +3 -50
- package/dist/browser.js +2336 -929
- package/dist/browser.js.map +4 -4
- package/dist/browser.min.js +3 -1
- package/dist/browser.min.js.map +4 -4
- package/docs/README.md +30 -0
- package/docs/reference.md +97 -0
- package/package.json +47 -14
- package/src/browser.mjs +9 -15
- package/src/index.mjs +20 -0
- package/src/oidc.discovery.mjs +4 -3
- package/src/oidc.jwt.mjs +65 -0
- package/src/oidc.mockserver.mjs +195 -0
- package/src/oidc.register.mjs +6 -5
- package/src/oidc.util.mjs +1 -1
- package/src/oidcmw.mjs +76 -25
- package/src/testing.mjs +4 -0
package/src/oidcmw.mjs
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import * as metro from '@muze-nl/metro
|
|
2
|
-
import oauth2mw, * as oauth2 from '@muze-nl/metro-oauth2/
|
|
3
|
-
import dpopmw from '@muze-nl/metro-oauth2/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/testing.mjs
ADDED