@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/docs/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: '@muze-nl/metro-oidc'
|
|
3
|
+
---
|
|
4
|
+
# @muze-nl/metro-oidc
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
npm install @muze-nl/metro-core @muze-nl/metro-oidc
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
import { client } from '@muze-nl/metro-core'
|
|
12
|
+
import { oidcmw } from '@muze-nl/metro-oidc'
|
|
13
|
+
|
|
14
|
+
const api = client('https://example.solidcommunity.net/')
|
|
15
|
+
.with(oidcmw({
|
|
16
|
+
issuer: 'https://solidcommunity.net/',
|
|
17
|
+
client_info: {
|
|
18
|
+
client_name: 'My Metro App',
|
|
19
|
+
redirect_uris: [location.href]
|
|
20
|
+
}
|
|
21
|
+
}))
|
|
22
|
+
|
|
23
|
+
const result = api.get('foo')
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Use this package when a resource is protected by OpenID Connect. It can discover the issuer metadata, dynamically register a client when supported, configure OAuth2, validate the ID token, and store the resulting ID token claims.
|
|
27
|
+
|
|
28
|
+
## Reference
|
|
29
|
+
|
|
30
|
+
See [reference](reference.md).
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Reference'
|
|
3
|
+
---
|
|
4
|
+
# @muze-nl/metro-oidc reference
|
|
5
|
+
|
|
6
|
+
```js
|
|
7
|
+
import oidc, {
|
|
8
|
+
oidcmw,
|
|
9
|
+
discover,
|
|
10
|
+
register,
|
|
11
|
+
isRedirected,
|
|
12
|
+
idToken,
|
|
13
|
+
idTokenClaims,
|
|
14
|
+
oidcStore,
|
|
15
|
+
validateIdToken
|
|
16
|
+
} from '@muze-nl/metro-oidc'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## `oidcmw(options)`
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
const api = client('https://example.solidcommunity.net/')
|
|
23
|
+
.with(oidcmw({
|
|
24
|
+
issuer: 'https://solidcommunity.net/',
|
|
25
|
+
client_info: {
|
|
26
|
+
client_name: 'My Metro App',
|
|
27
|
+
redirect_uris: [location.href]
|
|
28
|
+
}
|
|
29
|
+
}))
|
|
30
|
+
```
|
|
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.
|
|
33
|
+
|
|
34
|
+
Important options: `issuer`, `client_info`, `client`, `openid_configuration`, `oauth2`, `store`, `scope`, `nonce`, `use_dpop`, `force_authorization`, and `authorize_callback`.
|
|
35
|
+
|
|
36
|
+
`use_dpop` defaults to `true`. Disable it only for providers or tests that do not support DPoP.
|
|
37
|
+
|
|
38
|
+
## `discover(options)`
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
const config = await discover({ issuer: 'https://solidcommunity.net/' })
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Fetches OIDC discovery metadata. Pass a Metro client with `client` when you want custom middleware or tests.
|
|
45
|
+
|
|
46
|
+
## `register(options)`
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
const info = await register({
|
|
50
|
+
registration_endpoint: config.registration_endpoint,
|
|
51
|
+
client_info: {
|
|
52
|
+
client_name: 'My Metro App',
|
|
53
|
+
redirect_uris: [location.href]
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
```
|
|
57
|
+
|
|
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.
|
|
59
|
+
|
|
60
|
+
## ID token helpers
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
const raw = idToken({ issuer: 'https://solidcommunity.net/' })
|
|
64
|
+
const claims = idTokenClaims({ issuer: 'https://solidcommunity.net/' })
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Returns the stored raw ID token or validated claims. Pass the same `issuer` or `store` used by the middleware.
|
|
68
|
+
|
|
69
|
+
## `validateIdToken(idToken, options)`
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
const validation = await validateIdToken(idToken, {
|
|
73
|
+
issuer: config.issuer,
|
|
74
|
+
client_id: clientInfo.client_id,
|
|
75
|
+
jwks,
|
|
76
|
+
openid_configuration: config,
|
|
77
|
+
nonce
|
|
78
|
+
})
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Validates the token signature and standard claims, including issuer, audience, expiry, required claims, and nonce.
|
|
82
|
+
|
|
83
|
+
## Store
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
const store = oidcStore('https://solidcommunity.net/')
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Creates a simple storage object backed by `localStorage` when available or memory otherwise.
|
|
90
|
+
|
|
91
|
+
## Test mock server
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
import oidcmockserver from '@muze-nl/metro-oidc/testing'
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The mock server exposes discovery metadata, dynamic client registration, JWKS, signed ID tokens, userinfo, and OAuth2-backed protected-resource behaviour for tests.
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muze-nl/metro-oidc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "oidc client middleware for @muze-nl/metro",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"source": [
|
|
7
7
|
"src/browser.mjs"
|
|
8
8
|
],
|
|
9
|
-
"main": "src/
|
|
9
|
+
"main": "src/index.mjs",
|
|
10
10
|
"targets": {
|
|
11
11
|
"default": {
|
|
12
12
|
"distDir": "./dist",
|
|
@@ -18,23 +18,28 @@
|
|
|
18
18
|
"test": "test"
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
|
-
"test": "tap test/*.mjs",
|
|
21
|
+
"test": "tap --disable-coverage --jobs=1 test/*.mjs",
|
|
22
22
|
"tap": "tap",
|
|
23
|
-
"build": "
|
|
24
|
-
"build-dev": "
|
|
23
|
+
"build": "esbuild --bundle src/browser.mjs --outfile=dist/browser.min.js --minify --sourcemap",
|
|
24
|
+
"build-dev": "esbuild --bundle src/browser.mjs --outfile=dist/browser.js --sourcemap",
|
|
25
|
+
"build:dev": "esbuild --bundle src/browser.mjs --outfile=dist/browser.js --sourcemap"
|
|
25
26
|
},
|
|
26
27
|
"repository": {
|
|
27
28
|
"type": "git",
|
|
28
|
-
"url": "git+https://github.com/muze-nl/metro
|
|
29
|
+
"url": "git+https://github.com/muze-nl/metro.git",
|
|
30
|
+
"directory": "packages/metro-oidc"
|
|
29
31
|
},
|
|
30
32
|
"author": "auke@muze.nl",
|
|
31
33
|
"license": "MIT",
|
|
32
34
|
"bugs": {
|
|
33
|
-
"url": "https://github.com/muze-nl/metro
|
|
35
|
+
"url": "https://github.com/muze-nl/metro/issues"
|
|
34
36
|
},
|
|
35
|
-
"homepage": "https://github.com/muze-nl/metro-oidc
|
|
37
|
+
"homepage": "https://github.com/muze-nl/metro/tree/main/packages/metro-oidc#readme",
|
|
36
38
|
"devDependencies": {
|
|
37
|
-
"eslint": "^
|
|
39
|
+
"@eslint/js": "^10.0.1",
|
|
40
|
+
"eslint": "^10.5.0",
|
|
41
|
+
"globals": "^17.7.0",
|
|
42
|
+
"tap": "^21.7.4"
|
|
38
43
|
},
|
|
39
44
|
"engines": {
|
|
40
45
|
"node": ">=20.0.0"
|
|
@@ -43,11 +48,39 @@
|
|
|
43
48
|
"/src",
|
|
44
49
|
"/dist",
|
|
45
50
|
"/LICENSE",
|
|
46
|
-
"/README.md"
|
|
51
|
+
"/README.md",
|
|
52
|
+
"/docs"
|
|
47
53
|
],
|
|
48
54
|
"dependencies": {
|
|
49
|
-
"@muze-nl/assert": "^0.
|
|
50
|
-
"@muze-nl/metro": "^0.
|
|
51
|
-
"@muze-nl/metro-
|
|
52
|
-
|
|
55
|
+
"@muze-nl/assert": "^0.6.0",
|
|
56
|
+
"@muze-nl/metro-core": "^0.9.0",
|
|
57
|
+
"@muze-nl/metro-middleware": "^0.9.0",
|
|
58
|
+
"@muze-nl/metro-oauth2": "^0.9.0",
|
|
59
|
+
"jose": "^6.2.3"
|
|
60
|
+
},
|
|
61
|
+
"exports": {
|
|
62
|
+
".": "./src/index.mjs",
|
|
63
|
+
"./browser": "./src/browser.mjs",
|
|
64
|
+
"./browser.mjs": "./src/browser.mjs",
|
|
65
|
+
"./middleware": "./src/oidcmw.mjs",
|
|
66
|
+
"./middleware.mjs": "./src/oidcmw.mjs",
|
|
67
|
+
"./oidcmw": "./src/oidcmw.mjs",
|
|
68
|
+
"./oidcmw.mjs": "./src/oidcmw.mjs",
|
|
69
|
+
"./discovery": "./src/oidc.discovery.mjs",
|
|
70
|
+
"./discovery.mjs": "./src/oidc.discovery.mjs",
|
|
71
|
+
"./register": "./src/oidc.register.mjs",
|
|
72
|
+
"./register.mjs": "./src/oidc.register.mjs",
|
|
73
|
+
"./store": "./src/oidc.store.mjs",
|
|
74
|
+
"./store.mjs": "./src/oidc.store.mjs",
|
|
75
|
+
"./jwt": "./src/oidc.jwt.mjs",
|
|
76
|
+
"./jwt.mjs": "./src/oidc.jwt.mjs",
|
|
77
|
+
"./testing": "./src/testing.mjs",
|
|
78
|
+
"./src/*": "./src/*",
|
|
79
|
+
"./package.json": "./package.json"
|
|
80
|
+
},
|
|
81
|
+
"sideEffects": [
|
|
82
|
+
"./src/browser.mjs",
|
|
83
|
+
"./dist/browser.js",
|
|
84
|
+
"./dist/browser.min.js"
|
|
85
|
+
]
|
|
53
86
|
}
|
package/src/browser.mjs
CHANGED
|
@@ -1,19 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import oidcDiscover from './oidc.discovery.mjs'
|
|
4
|
-
import oidcRegister from './oidc.register.mjs'
|
|
5
|
-
import oidcmw, {isRedirected, idToken} from './oidcmw.mjs'
|
|
1
|
+
import * as core from '@muze-nl/metro-core'
|
|
2
|
+
import oidc from './index.mjs'
|
|
6
3
|
|
|
7
|
-
const
|
|
8
|
-
oidcmw,
|
|
9
|
-
discover: oidcDiscover,
|
|
10
|
-
register: oidcRegister,
|
|
11
|
-
isRedirected,
|
|
12
|
-
idToken
|
|
13
|
-
}
|
|
4
|
+
const metro = Object.assign({}, core, globalThis.metro || {})
|
|
14
5
|
|
|
15
|
-
if (!
|
|
16
|
-
|
|
6
|
+
if (!metro.oidc) {
|
|
7
|
+
metro.oidc = oidc
|
|
17
8
|
}
|
|
18
9
|
|
|
19
|
-
|
|
10
|
+
globalThis.metro = metro
|
|
11
|
+
|
|
12
|
+
export * from './index.mjs'
|
|
13
|
+
export default oidc
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import oidcmw, { isRedirected, idToken, idTokenClaims } from './oidcmw.mjs'
|
|
2
|
+
import discover from './oidc.discovery.mjs'
|
|
3
|
+
import register from './oidc.register.mjs'
|
|
4
|
+
|
|
5
|
+
const oidc = {
|
|
6
|
+
oidcmw,
|
|
7
|
+
discover,
|
|
8
|
+
register,
|
|
9
|
+
isRedirected,
|
|
10
|
+
idToken,
|
|
11
|
+
idTokenClaims
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default oidc
|
|
15
|
+
export { default as oidcmw } from './oidcmw.mjs'
|
|
16
|
+
export { isRedirected, idToken, idTokenClaims } from './oidcmw.mjs'
|
|
17
|
+
export { default as discover } from './oidc.discovery.mjs'
|
|
18
|
+
export { default as register } from './oidc.register.mjs'
|
|
19
|
+
export { default as oidcStore } from './oidc.store.mjs'
|
|
20
|
+
export { validateIdToken } from './oidc.jwt.mjs'
|
package/src/oidc.discovery.mjs
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* This module adheres to OpenID Connect Discovery 1.0 incorporating errata set 2 - december 15, 2023
|
|
3
3
|
* https://openid.net/specs/openid-connect-discovery-1_0.html
|
|
4
4
|
*/
|
|
5
|
-
import metro from '@muze-nl/metro'
|
|
6
|
-
import jsonmw from '@muze-nl/metro/
|
|
7
|
-
import throwermw from '@muze-nl/metro/
|
|
5
|
+
import * as metro from '@muze-nl/metro-core'
|
|
6
|
+
import jsonmw from '@muze-nl/metro-middleware/json'
|
|
7
|
+
import throwermw from '@muze-nl/metro-middleware/thrower'
|
|
8
8
|
import { validJWA, MustInclude, validAuthMethods } from './oidc.util.mjs'
|
|
9
9
|
import { assert, fails, Required, Recommended, Optional, oneOf, anyOf, allOf, validURL, instanceOf, not, error } from '@muze-nl/assert'
|
|
10
10
|
|
|
@@ -29,6 +29,7 @@ export default async function oidcDiscovery(options={}) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
options = Object.assign({},defaultOptions,options)
|
|
32
|
+
options.client = options.client.with(throwermw()).with(jsonmw())
|
|
32
33
|
|
|
33
34
|
const TestSucceeded = false
|
|
34
35
|
function MustUseHTTPS(url) {
|
package/src/oidc.jwt.mjs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createLocalJWKSet, jwtVerify } from 'jose'
|
|
2
|
+
import * as metro from '@muze-nl/metro-core'
|
|
3
|
+
|
|
4
|
+
const jwksCache = new WeakMap()
|
|
5
|
+
|
|
6
|
+
function keySetFor(jwks) {
|
|
7
|
+
if (!jwks || !Array.isArray(jwks.keys)) {
|
|
8
|
+
throw metro.metroError('metro.oidc: Error: jwks must be an object with a keys array')
|
|
9
|
+
}
|
|
10
|
+
if (!jwksCache.has(jwks)) {
|
|
11
|
+
jwksCache.set(jwks, createLocalJWKSet(jwks))
|
|
12
|
+
}
|
|
13
|
+
return jwksCache.get(jwks)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function supportedAlgorithms(openidConfiguration = {}) {
|
|
17
|
+
const algorithms = openidConfiguration.id_token_signing_alg_values_supported
|
|
18
|
+
if (Array.isArray(algorithms) && algorithms.length) {
|
|
19
|
+
return algorithms.filter(algorithm => algorithm !== 'none')
|
|
20
|
+
}
|
|
21
|
+
return ['RS256']
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validate an OpenID Connect ID Token.
|
|
26
|
+
*
|
|
27
|
+
* This intentionally keeps jose behind a tiny Metro/OIDC helper so the public
|
|
28
|
+
* OIDC middleware API can stay small while still delegating JWT/JWS/JWKS
|
|
29
|
+
* validation to a mature library.
|
|
30
|
+
*/
|
|
31
|
+
export async function validateIdToken(idToken, options = {}) {
|
|
32
|
+
if (!idToken) {
|
|
33
|
+
throw metro.metroError('metro.oidc: Error: token response did not include an id_token')
|
|
34
|
+
}
|
|
35
|
+
if (!options.jwks) {
|
|
36
|
+
throw metro.metroError('metro.oidc: Error: cannot validate id_token without jwks')
|
|
37
|
+
}
|
|
38
|
+
if (!options.issuer) {
|
|
39
|
+
throw metro.metroError('metro.oidc: Error: cannot validate id_token without issuer')
|
|
40
|
+
}
|
|
41
|
+
if (!options.client_id) {
|
|
42
|
+
throw metro.metroError('metro.oidc: Error: cannot validate id_token without client_id')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { payload, protectedHeader } = await jwtVerify(
|
|
46
|
+
idToken,
|
|
47
|
+
keySetFor(options.jwks),
|
|
48
|
+
{
|
|
49
|
+
issuer: options.issuer,
|
|
50
|
+
audience: options.client_id,
|
|
51
|
+
algorithms: options.algorithms || supportedAlgorithms(options.openid_configuration),
|
|
52
|
+
clockTolerance: options.clockTolerance ?? 60,
|
|
53
|
+
requiredClaims: ['iss', 'sub', 'aud', 'exp', 'iat']
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if (options.nonce !== undefined && payload.nonce !== options.nonce) {
|
|
58
|
+
throw metro.metroError('metro.oidc: Error: id_token nonce mismatch')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
header: protectedHeader,
|
|
63
|
+
claims: payload
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import * as metro from '@muze-nl/metro-core'
|
|
2
|
+
import oauth2mockserver from '@muze-nl/metro-oauth2/testing'
|
|
3
|
+
import { exportJWK, generateKeyPair, SignJWT } from 'jose'
|
|
4
|
+
|
|
5
|
+
const defaultIssuer = 'https://issuer.example/'
|
|
6
|
+
const defaultRedirectUri = 'https://client.example/callback'
|
|
7
|
+
const jsonHeaders = {
|
|
8
|
+
'Content-Type': 'application/json'
|
|
9
|
+
}
|
|
10
|
+
const textHeaders = {
|
|
11
|
+
'Content-Type': 'text/plain'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function jsonResponse(body, status = 200, statusText = 'OK') {
|
|
15
|
+
return metro.response({
|
|
16
|
+
status,
|
|
17
|
+
statusText,
|
|
18
|
+
headers: jsonHeaders,
|
|
19
|
+
body: JSON.stringify(body)
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function errorResponse(error, description, status = 400) {
|
|
24
|
+
return jsonResponse({
|
|
25
|
+
error,
|
|
26
|
+
error_description: description
|
|
27
|
+
}, status, status === 401 ? 'Unauthorized' : 'Bad Request')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function requestData(req) {
|
|
31
|
+
if (req.data instanceof URLSearchParams) {
|
|
32
|
+
return Object.fromEntries(req.data.entries())
|
|
33
|
+
}
|
|
34
|
+
if (req.data && typeof req.data === 'object') {
|
|
35
|
+
return req.data
|
|
36
|
+
}
|
|
37
|
+
if (!req.body) {
|
|
38
|
+
return {}
|
|
39
|
+
}
|
|
40
|
+
const text = await req.clone().text()
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(text)
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return Object.fromEntries(new URLSearchParams(text).entries())
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Small OIDC provider mock for Metro tests.
|
|
50
|
+
*
|
|
51
|
+
* It exposes discovery metadata, dynamic client registration, userinfo, JWKS
|
|
52
|
+
* and delegates the OAuth2 authorization/token/protected-resource endpoints to
|
|
53
|
+
* the OAuth2 mock server. Its token endpoint returns real signed ID Tokens so
|
|
54
|
+
* the OIDC client tests exercise actual JWT/JWS/JWKS validation.
|
|
55
|
+
*/
|
|
56
|
+
export default function oidcmockserver(options = {}) {
|
|
57
|
+
options = Object.assign({
|
|
58
|
+
issuer: defaultIssuer,
|
|
59
|
+
client_id: 'mockClientId',
|
|
60
|
+
client_secret: 'mockClientSecret',
|
|
61
|
+
redirect_uri: defaultRedirectUri,
|
|
62
|
+
sub: 'mockSubject',
|
|
63
|
+
alg: 'RS256',
|
|
64
|
+
kid: 'mock-signing-key',
|
|
65
|
+
publishSigningKey: true,
|
|
66
|
+
idTokenClaims: {}
|
|
67
|
+
}, options)
|
|
68
|
+
|
|
69
|
+
const issuer = metro.url(options.issuer)
|
|
70
|
+
const keyPair = createSigningKey(options)
|
|
71
|
+
const oauth2 = oauth2mockserver({
|
|
72
|
+
client_id: options.client_id,
|
|
73
|
+
client_secret: options.client_secret,
|
|
74
|
+
redirect_uri: options.redirect_uri,
|
|
75
|
+
scope: 'openid profile email',
|
|
76
|
+
id_token: tokenContext => idToken(tokenContext),
|
|
77
|
+
requirePKCE: options.requirePKCE ?? false
|
|
78
|
+
})
|
|
79
|
+
const clients = new Map()
|
|
80
|
+
|
|
81
|
+
return async (req, next) => {
|
|
82
|
+
const url = metro.url(req.url)
|
|
83
|
+
switch (url.pathname) {
|
|
84
|
+
case '/.well-known/openid-configuration':
|
|
85
|
+
return discovery()
|
|
86
|
+
case '/register/':
|
|
87
|
+
case '/register':
|
|
88
|
+
return register(req)
|
|
89
|
+
case '/userinfo/':
|
|
90
|
+
case '/userinfo':
|
|
91
|
+
return userinfo(req)
|
|
92
|
+
case '/jwks/':
|
|
93
|
+
case '/jwks':
|
|
94
|
+
return jwks()
|
|
95
|
+
default:
|
|
96
|
+
return oauth2(req, next)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function endpoint(path) {
|
|
101
|
+
return metro.url(issuer, path).href
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function discovery() {
|
|
105
|
+
return jsonResponse({
|
|
106
|
+
issuer: issuer.href,
|
|
107
|
+
authorization_endpoint: endpoint('/authorize/'),
|
|
108
|
+
token_endpoint: endpoint('/token/'),
|
|
109
|
+
userinfo_endpoint: endpoint('/userinfo/'),
|
|
110
|
+
jwks_uri: endpoint('/jwks/'),
|
|
111
|
+
registration_endpoint: endpoint('/register/'),
|
|
112
|
+
scopes_supported: ['openid', 'profile', 'email'],
|
|
113
|
+
response_types_supported: ['code', 'id_token', 'id_token token'],
|
|
114
|
+
grant_types_supported: ['authorization_code', 'refresh_token'],
|
|
115
|
+
subject_types_supported: ['public'],
|
|
116
|
+
id_token_signing_alg_values_supported: [options.alg],
|
|
117
|
+
claims_supported: ['iss', 'sub', 'aud', 'exp', 'iat', 'nonce', 'name', 'email']
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function jwks() {
|
|
122
|
+
if (!options.publishSigningKey) {
|
|
123
|
+
return jsonResponse({ keys: [] })
|
|
124
|
+
}
|
|
125
|
+
const { publicKey } = await keyPair
|
|
126
|
+
const jwk = await exportJWK(publicKey)
|
|
127
|
+
jwk.kid = options.jwksKid || options.kid
|
|
128
|
+
jwk.alg = options.alg
|
|
129
|
+
jwk.use = 'sig'
|
|
130
|
+
return jsonResponse({ keys: [jwk] })
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function register(req) {
|
|
134
|
+
if (req.method !== 'POST') {
|
|
135
|
+
return errorResponse('invalid_request', 'registration endpoint requires POST')
|
|
136
|
+
}
|
|
137
|
+
const body = await requestData(req)
|
|
138
|
+
if (!Array.isArray(body.redirect_uris) || body.redirect_uris.length === 0) {
|
|
139
|
+
return errorResponse('invalid_client_metadata', 'redirect_uris is required')
|
|
140
|
+
}
|
|
141
|
+
const info = {
|
|
142
|
+
...body,
|
|
143
|
+
client_id: options.client_id,
|
|
144
|
+
client_id_issued_at: Math.floor(Date.now() / 1000),
|
|
145
|
+
token_endpoint_auth_method: options.client_secret ? 'client_secret_post' : 'none'
|
|
146
|
+
}
|
|
147
|
+
if (options.client_secret) {
|
|
148
|
+
info.client_secret = options.client_secret
|
|
149
|
+
}
|
|
150
|
+
clients.set(info.client_id, info)
|
|
151
|
+
return jsonResponse(info, 201, 'Created')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function userinfo(req) {
|
|
155
|
+
const auth = req.headers.get('Authorization')
|
|
156
|
+
if (auth !== 'Bearer mockAccessToken') {
|
|
157
|
+
return metro.response({
|
|
158
|
+
status: 401,
|
|
159
|
+
statusText: 'Unauthorized',
|
|
160
|
+
headers: textHeaders,
|
|
161
|
+
body: '401 Unauthorized'
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
return jsonResponse({
|
|
165
|
+
sub: options.sub,
|
|
166
|
+
name: 'Mock User',
|
|
167
|
+
email: 'mock@example.test'
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function idToken(tokenContext = {}) {
|
|
172
|
+
const now = Math.floor(Date.now() / 1000)
|
|
173
|
+
const authorization = tokenContext.authorization || {}
|
|
174
|
+
const claims = {
|
|
175
|
+
iss: issuer.href,
|
|
176
|
+
sub: options.sub,
|
|
177
|
+
aud: options.client_id,
|
|
178
|
+
exp: now + 3600,
|
|
179
|
+
iat: now,
|
|
180
|
+
...(authorization.nonce ? { nonce: authorization.nonce } : {}),
|
|
181
|
+
...options.idTokenClaims
|
|
182
|
+
}
|
|
183
|
+
const { privateKey } = await keyPair
|
|
184
|
+
return new SignJWT(claims)
|
|
185
|
+
.setProtectedHeader({ alg: options.alg, kid: options.kid })
|
|
186
|
+
.sign(privateKey)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function createSigningKey(options) {
|
|
191
|
+
return generateKeyPair(options.alg, {
|
|
192
|
+
extractable: true,
|
|
193
|
+
modulusLength: 2048
|
|
194
|
+
})
|
|
195
|
+
}
|
package/src/oidc.register.mjs
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* incorporating errata set 2 - december 15, 2023
|
|
4
4
|
* https://openid.net/specs/openid-connect-registration-1_0.html
|
|
5
5
|
*/
|
|
6
|
-
import metro from '@muze-nl/metro'
|
|
7
|
-
import jsonmw from '@muze-nl/metro/
|
|
8
|
-
import throwermw from '@muze-nl/metro/
|
|
6
|
+
import * as metro from '@muze-nl/metro-core'
|
|
7
|
+
import jsonmw from '@muze-nl/metro-middleware/json'
|
|
8
|
+
import throwermw from '@muze-nl/metro-middleware/thrower'
|
|
9
9
|
import { validJWA, validAuthMethods, MustHave } from './oidc.util.mjs'
|
|
10
10
|
import { assert, Required, Optional, oneOf, anyOf, validURL, validEmail, not, instanceOf } from '@muze-nl/assert'
|
|
11
11
|
|
|
@@ -60,6 +60,7 @@ export default async function register(options)
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
options = Object.assign({}, defaultOptions, options)
|
|
63
|
+
options.client = options.client.with(throwermw()).with(jsonmw())
|
|
63
64
|
if (!options.client_info) {
|
|
64
65
|
options.client_info = {}
|
|
65
66
|
}
|
|
@@ -71,8 +72,8 @@ export default async function register(options)
|
|
|
71
72
|
body: options.client_info
|
|
72
73
|
})
|
|
73
74
|
let info = response.data
|
|
74
|
-
if (!info.client_id
|
|
75
|
-
throw metro.metroError('metro.oidc: Error: dynamic registration of client failed, no client_id
|
|
75
|
+
if (!info.client_id) {
|
|
76
|
+
throw metro.metroError('metro.oidc: Error: dynamic registration of client failed, no client_id returned', response)
|
|
76
77
|
}
|
|
77
78
|
options.client_info = Object.assign(options.client_info, info)
|
|
78
79
|
return options.client_info
|
package/src/oidc.util.mjs
CHANGED
|
@@ -30,5 +30,5 @@ export const validJWA = [
|
|
|
30
30
|
|
|
31
31
|
//TODO: add link to spec
|
|
32
32
|
export const validAuthMethods = [
|
|
33
|
-
'client_secret_post', 'client_secret_basic','client_secret_jwt','private_key_jwt'
|
|
33
|
+
'none', 'client_secret_post', 'client_secret_basic','client_secret_jwt','private_key_jwt'
|
|
34
34
|
]
|