mirror of
https://github.com/hanzoai/openid-client.git
synced 2026-09-24 17:01:10 +00:00
fix: allow client secret based auth factories to be used with DCR
This commit is contained in:
@@ -6,7 +6,7 @@ Support from the community to continue maintaining and improving this module is
|
||||
|
||||
***
|
||||
|
||||
▸ **ClientSecretBasic**(`clientSecret`): [`ClientAuth`](../type-aliases/ClientAuth.md)
|
||||
▸ **ClientSecretBasic**(`clientSecret`?): [`ClientAuth`](../type-aliases/ClientAuth.md)
|
||||
|
||||
**`client_secret_basic`** uses the HTTP `Basic` authentication scheme to send
|
||||
`client_id` and `client_secret` in an `Authorization` HTTP Header.
|
||||
@@ -15,7 +15,7 @@ Support from the community to continue maintaining and improving this module is
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| ------ | ------ | ------ |
|
||||
| `clientSecret` | `string` | Client Secret |
|
||||
| `clientSecret`? | `string` | Client Secret |
|
||||
|
||||
## Returns
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ Support from the community to continue maintaining and improving this module is
|
||||
|
||||
***
|
||||
|
||||
▸ **ClientSecretJwt**(`clientSecret`, `options`?): [`ClientAuth`](../type-aliases/ClientAuth.md)
|
||||
▸ **ClientSecretJwt**(`clientSecret`?, `options`?): [`ClientAuth`](../type-aliases/ClientAuth.md)
|
||||
|
||||
**`client_secret_jwt`** uses the HTTP request body to send `client_id`,
|
||||
`client_assertion_type`, and `client_assertion` as
|
||||
@@ -17,7 +17,7 @@ assertion's authenticity and integrity.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| ------ | ------ | ------ |
|
||||
| `clientSecret` | `string` | Client Secret |
|
||||
| `clientSecret`? | `string` | Client Secret |
|
||||
| `options`? | [`ModifyAssertionOptions`](../interfaces/ModifyAssertionOptions.md) | |
|
||||
|
||||
## Returns
|
||||
|
||||
@@ -6,7 +6,7 @@ Support from the community to continue maintaining and improving this module is
|
||||
|
||||
***
|
||||
|
||||
▸ **ClientSecretPost**(`clientSecret`): [`ClientAuth`](../type-aliases/ClientAuth.md)
|
||||
▸ **ClientSecretPost**(`clientSecret`?): [`ClientAuth`](../type-aliases/ClientAuth.md)
|
||||
|
||||
**`client_secret_post`** uses the HTTP request body to send `client_id` and
|
||||
`client_secret` as `application/x-www-form-urlencoded` body parameters
|
||||
@@ -15,7 +15,7 @@ Support from the community to continue maintaining and improving this module is
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| ------ | ------ | ------ |
|
||||
| `clientSecret` | `string` | Client Secret |
|
||||
| `clientSecret`? | `string` | Client Secret |
|
||||
|
||||
## Returns
|
||||
|
||||
|
||||
@@ -17,13 +17,19 @@ Note: This method also accepts a URL pointing directly to the Authorization
|
||||
Server's discovery document. Doing so is NOT RECOMMENDED as it disables the
|
||||
[ServerMetadata.issuer](../interfaces/ServerMetadata.md#issuer) validation.
|
||||
|
||||
Note: The method does not contain any logic to default the registered
|
||||
"token_endpoint_auth_method" based on
|
||||
[ServerMetadata.token\_endpoint\_auth\_methods\_supported](../interfaces/ServerMetadata.md#token_endpoint_auth_methods_supported), nor does it
|
||||
default the "clientAuthentication" argument value beyond what its description
|
||||
says.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| ------ | ------ | ------ |
|
||||
| `server` | [`URL`](https://developer.mozilla.org/docs/Web/API/URL) | URL representation of the Authorization Server's Issuer Identifier |
|
||||
| `metadata` | [`Partial`](https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype)\<[`ClientMetadata`](../interfaces/ClientMetadata.md)\> | Client Metadata to register at the Authorization Server |
|
||||
| `clientAuthentication`? | [`ClientAuth`](../type-aliases/ClientAuth.md) | Implementation of the Client's Authentication Method at the Authorization Server. Default is [ClientSecretPost](ClientSecretPost.md) using the [ClientMetadata.client\_secret](../interfaces/ClientMetadata.md#client_secret) that the Authorization Server issued. |
|
||||
| `clientAuthentication`? | [`ClientAuth`](../type-aliases/ClientAuth.md) | Implementation of the Client's Authentication Method at the Authorization Server. Default is [ClientSecretPost](ClientSecretPost.md) using the [ClientMetadata.client\_secret](../interfaces/ClientMetadata.md#client_secret) that the Authorization Server issued, [None](None.md) otherwise. |
|
||||
| `options`? | [`DynamicClientRegistrationRequestOptions`](../interfaces/DynamicClientRegistrationRequestOptions.md) | |
|
||||
|
||||
## Returns
|
||||
|
||||
+70
-6
@@ -106,6 +106,8 @@ export type ClientAuth = (
|
||||
headers: Headers,
|
||||
) => void
|
||||
|
||||
let tbi!: WeakMap<Internal['c'], ClientAuth>
|
||||
|
||||
/**
|
||||
* **`client_secret_post`** uses the HTTP request body to send `client_id` and
|
||||
* `client_secret` as `application/x-www-form-urlencoded` body parameters
|
||||
@@ -154,10 +156,34 @@ export type ClientAuth = (
|
||||
* @see [RFC 6749 - The OAuth 2.0 Authorization Framework](https://www.rfc-editor.org/rfc/rfc6749.html#section-2.3)
|
||||
* @see [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication)
|
||||
*/
|
||||
export function ClientSecretPost(clientSecret: string): ClientAuth {
|
||||
export function ClientSecretPost(clientSecret?: string): ClientAuth {
|
||||
if (clientSecret !== undefined) {
|
||||
return oauth.ClientSecretPost(clientSecret)
|
||||
}
|
||||
|
||||
tbi ||= new WeakMap()
|
||||
|
||||
return (as, client, body, headers) => {
|
||||
let auth: ClientAuth | undefined
|
||||
if (!(auth = tbi.get(client))) {
|
||||
assertString(client.client_secret, '"metadata.client_secret"')
|
||||
auth = oauth.ClientSecretPost(client.client_secret)
|
||||
tbi.set(client, auth)
|
||||
}
|
||||
return auth(as, client, body, headers)
|
||||
}
|
||||
}
|
||||
|
||||
function assertString(input: unknown, it: string): asserts input is string {
|
||||
if (typeof input !== 'string') {
|
||||
throw CodedTypeError(`${it} must be a string`, ERR_INVALID_ARG_TYPE)
|
||||
}
|
||||
|
||||
if (input.length === 0) {
|
||||
throw CodedTypeError(`${it} must not be empty`, ERR_INVALID_ARG_VALUE)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* **`client_secret_basic`** uses the HTTP `Basic` authentication scheme to send
|
||||
* `client_id` and `client_secret` in an `Authorization` HTTP Header.
|
||||
@@ -206,10 +232,24 @@ export function ClientSecretPost(clientSecret: string): ClientAuth {
|
||||
* @see [RFC 6749 - The OAuth 2.0 Authorization Framework](https://www.rfc-editor.org/rfc/rfc6749.html#section-2.3)
|
||||
* @see [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication)
|
||||
*/
|
||||
export function ClientSecretBasic(clientSecret: string): ClientAuth {
|
||||
export function ClientSecretBasic(clientSecret?: string): ClientAuth {
|
||||
if (clientSecret !== undefined) {
|
||||
return oauth.ClientSecretBasic(clientSecret)
|
||||
}
|
||||
|
||||
tbi ||= new WeakMap()
|
||||
|
||||
return (as, client, body, headers) => {
|
||||
let auth: ClientAuth | undefined
|
||||
if (!(auth = tbi.get(client))) {
|
||||
assertString(client.client_secret, '"metadata.client_secret"')
|
||||
auth = oauth.ClientSecretBasic(client.client_secret)
|
||||
tbi.set(client, auth)
|
||||
}
|
||||
return auth(as, client, body, headers)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* **`client_secret_jwt`** uses the HTTP request body to send `client_id`,
|
||||
* `client_assertion_type`, and `client_assertion` as
|
||||
@@ -261,12 +301,26 @@ export function ClientSecretBasic(clientSecret: string): ClientAuth {
|
||||
* @see [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication)
|
||||
*/
|
||||
export function ClientSecretJwt(
|
||||
clientSecret: string,
|
||||
clientSecret?: string,
|
||||
options?: oauth.ModifyAssertionOptions,
|
||||
): ClientAuth {
|
||||
if (clientSecret !== undefined) {
|
||||
return oauth.ClientSecretJwt(clientSecret, options)
|
||||
}
|
||||
|
||||
tbi ||= new WeakMap()
|
||||
|
||||
return (as, client, body, headers) => {
|
||||
let auth: ClientAuth | undefined
|
||||
if (!(auth = tbi.get(client))) {
|
||||
assertString(client.client_secret, '"metadata.client_secret"')
|
||||
auth = oauth.ClientSecretJwt(client.client_secret, options)
|
||||
tbi.set(client, auth)
|
||||
}
|
||||
return auth(as, client, body, headers)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* **`none`** (public client) uses the HTTP request body to send only
|
||||
* `client_id` as `application/x-www-form-urlencoded` body parameter.
|
||||
@@ -1080,13 +1134,19 @@ export interface DynamicClientRegistrationRequestOptions
|
||||
* Server's discovery document. Doing so is NOT RECOMMENDED as it disables the
|
||||
* {@link ServerMetadata.issuer} validation.
|
||||
*
|
||||
* Note: The method does not contain any logic to default the registered
|
||||
* "token_endpoint_auth_method" based on
|
||||
* {@link ServerMetadata.token_endpoint_auth_methods_supported}, nor does it
|
||||
* default the "clientAuthentication" argument value beyond what its description
|
||||
* says.
|
||||
*
|
||||
* @param server URL representation of the Authorization Server's Issuer
|
||||
* Identifier
|
||||
* @param metadata Client Metadata to register at the Authorization Server
|
||||
* @param clientAuthentication Implementation of the Client's Authentication
|
||||
* Method at the Authorization Server. Default is {@link ClientSecretPost}
|
||||
* using the {@link ClientMetadata.client_secret} that the Authorization Server
|
||||
* issued.
|
||||
* issued, {@link None} otherwise.
|
||||
* @param options
|
||||
*
|
||||
* @group Dynamic Client Registration
|
||||
@@ -1104,6 +1164,10 @@ export async function dynamicClientRegistration(
|
||||
as = await performDiscovery(server, options)
|
||||
}
|
||||
|
||||
const clockSkew = metadata[oauth.clockSkew] ?? 0
|
||||
const clockTolerance = metadata[oauth.clockTolerance] ?? 30
|
||||
metadata = structuredClone(metadata)
|
||||
|
||||
const timeout = options?.timeout ?? 30
|
||||
const signal = AbortSignal.timeout(timeout * 1000)
|
||||
|
||||
@@ -1133,8 +1197,8 @@ export async function dynamicClientRegistration(
|
||||
errorHandler(err)
|
||||
}
|
||||
|
||||
registered[oauth.clockSkew] = metadata[oauth.clockSkew] ?? 0
|
||||
registered[oauth.clockTolerance] = metadata[oauth.clockTolerance] ?? 30
|
||||
registered[oauth.clockSkew] = clockSkew
|
||||
registered[oauth.clockTolerance] = clockTolerance
|
||||
|
||||
const instance = new Configuration(
|
||||
as,
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
import test from 'ava'
|
||||
import * as client from '../src/index.js'
|
||||
import * as oauth from 'oauth4webapi'
|
||||
import * as undici from 'undici'
|
||||
import * as jose from 'jose'
|
||||
|
||||
test('dpop may be used with DCR w/ initial access token', async (t) => {
|
||||
let agent = new undici.MockAgent()
|
||||
agent.disableNetConnect()
|
||||
|
||||
const issuer = new URL('https://op.example.com')
|
||||
const kp = await client.randomDPoPKeyPair('ES256')
|
||||
|
||||
const mockAgent = agent.get(issuer.origin)
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'GET',
|
||||
path: '/.well-known/openid-configuration',
|
||||
})
|
||||
.reply(200, {
|
||||
issuer: issuer.href,
|
||||
registration_endpoint: `${issuer.origin}/reg`,
|
||||
})
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'POST',
|
||||
path: '/reg',
|
||||
headers({ dpop, authorization }) {
|
||||
return typeof dpop === 'string' && authorization === 'DPoP token'
|
||||
},
|
||||
})
|
||||
.reply(
|
||||
400,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
'dpop-nonce': 'use this',
|
||||
'www-authenticate': 'dpop error="use_dpop_nonce"',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'POST',
|
||||
path: '/reg',
|
||||
headers({ dpop, authorization }) {
|
||||
const { nonce } = jose.decodeJwt(dpop)
|
||||
return nonce === 'use this' && authorization === 'DPoP token'
|
||||
},
|
||||
})
|
||||
.reply(201, {
|
||||
client_id: 'foo',
|
||||
})
|
||||
|
||||
await client.dynamicClientRegistration(issuer, {}, undefined, {
|
||||
initialAccessToken: 'token',
|
||||
DPoP: oauth.DPoP({}, kp),
|
||||
// @ts-ignore
|
||||
[client.customFetch](url, options) {
|
||||
return undici.fetch(url, { ...options, dispatcher: agent })
|
||||
},
|
||||
})
|
||||
|
||||
t.notThrows(() => agent.assertNoPendingInterceptors())
|
||||
})
|
||||
|
||||
test('dpop may be used with DCR w/o initial access token', async (t) => {
|
||||
let agent = new undici.MockAgent()
|
||||
agent.disableNetConnect()
|
||||
|
||||
const issuer = new URL('https://op.example.com')
|
||||
const kp = await client.randomDPoPKeyPair('ES256')
|
||||
|
||||
const mockAgent = agent.get(issuer.origin)
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'GET',
|
||||
path: '/.well-known/openid-configuration',
|
||||
})
|
||||
.reply(200, {
|
||||
issuer: issuer.href,
|
||||
registration_endpoint: `${issuer.origin}/reg`,
|
||||
})
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'POST',
|
||||
path: '/reg',
|
||||
headers({ dpop, authorization }) {
|
||||
return typeof dpop === 'string' && authorization === undefined
|
||||
},
|
||||
})
|
||||
.reply(
|
||||
400,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
'dpop-nonce': 'use this',
|
||||
'www-authenticate': 'dpop error="use_dpop_nonce"',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'POST',
|
||||
path: '/reg',
|
||||
headers({ dpop, authorization }) {
|
||||
const { nonce } = jose.decodeJwt(dpop)
|
||||
return nonce === 'use this' && authorization === undefined
|
||||
},
|
||||
})
|
||||
.reply(201, {
|
||||
client_id: 'foo',
|
||||
})
|
||||
|
||||
await client.dynamicClientRegistration(issuer, {}, undefined, {
|
||||
DPoP: oauth.DPoP({}, kp),
|
||||
// @ts-ignore
|
||||
[client.customFetch](url, options) {
|
||||
return undici.fetch(url, { ...options, dispatcher: agent })
|
||||
},
|
||||
})
|
||||
|
||||
t.notThrows(() => agent.assertNoPendingInterceptors())
|
||||
})
|
||||
@@ -0,0 +1,443 @@
|
||||
import test from 'ava'
|
||||
import * as client from '../src/index.js'
|
||||
import * as oauth from 'oauth4webapi'
|
||||
import * as undici from 'undici'
|
||||
import * as jose from 'jose'
|
||||
|
||||
test('dpop may be used with DCR w/ initial access token', async (t) => {
|
||||
let agent = new undici.MockAgent()
|
||||
agent.disableNetConnect()
|
||||
|
||||
const issuer = new URL('https://op.example.com')
|
||||
const kp = await client.randomDPoPKeyPair('ES256')
|
||||
|
||||
const mockAgent = agent.get(issuer.origin)
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'GET',
|
||||
path: '/.well-known/openid-configuration',
|
||||
})
|
||||
.reply(200, {
|
||||
issuer: issuer.href,
|
||||
registration_endpoint: `${issuer.origin}/reg`,
|
||||
})
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'POST',
|
||||
path: '/reg',
|
||||
headers({ dpop, authorization }) {
|
||||
return typeof dpop === 'string' && authorization === 'DPoP token'
|
||||
},
|
||||
})
|
||||
.reply(
|
||||
400,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
'dpop-nonce': 'use this',
|
||||
'www-authenticate': 'dpop error="use_dpop_nonce"',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'POST',
|
||||
path: '/reg',
|
||||
headers({ dpop, authorization }) {
|
||||
const { nonce } = jose.decodeJwt(dpop)
|
||||
return nonce === 'use this' && authorization === 'DPoP token'
|
||||
},
|
||||
})
|
||||
.reply(201, {
|
||||
client_id: 'foo',
|
||||
})
|
||||
|
||||
await client.dynamicClientRegistration(issuer, {}, undefined, {
|
||||
initialAccessToken: 'token',
|
||||
DPoP: oauth.DPoP({}, kp),
|
||||
// @ts-ignore
|
||||
[client.customFetch](url, options) {
|
||||
return undici.fetch(url, { ...options, dispatcher: agent })
|
||||
},
|
||||
})
|
||||
|
||||
t.notThrows(() => agent.assertNoPendingInterceptors())
|
||||
})
|
||||
|
||||
test('dpop may be used with DCR w/o initial access token', async (t) => {
|
||||
let agent = new undici.MockAgent()
|
||||
agent.disableNetConnect()
|
||||
|
||||
const issuer = new URL('https://op.example.com')
|
||||
const kp = await client.randomDPoPKeyPair('ES256')
|
||||
|
||||
const mockAgent = agent.get(issuer.origin)
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'GET',
|
||||
path: '/.well-known/openid-configuration',
|
||||
})
|
||||
.reply(200, {
|
||||
issuer: issuer.href,
|
||||
registration_endpoint: `${issuer.origin}/reg`,
|
||||
token_endpoint: `${issuer.origin}/token`,
|
||||
})
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'POST',
|
||||
path: '/reg',
|
||||
headers({ dpop, authorization }) {
|
||||
return typeof dpop === 'string' && authorization === undefined
|
||||
},
|
||||
})
|
||||
.reply(
|
||||
400,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
'dpop-nonce': 'use this',
|
||||
'www-authenticate': 'dpop error="use_dpop_nonce"',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'POST',
|
||||
path: '/reg',
|
||||
headers({ dpop, authorization }) {
|
||||
const { nonce } = jose.decodeJwt(dpop)
|
||||
return nonce === 'use this' && authorization === undefined
|
||||
},
|
||||
})
|
||||
.reply(201, {
|
||||
client_id: 'foo',
|
||||
client_secret: 'bar',
|
||||
client_secret_expires_at: 0,
|
||||
})
|
||||
|
||||
await client.dynamicClientRegistration(issuer, {}, undefined, {
|
||||
DPoP: oauth.DPoP({}, kp),
|
||||
// @ts-ignore
|
||||
[client.customFetch](url, options) {
|
||||
return undici.fetch(url, { ...options, dispatcher: agent })
|
||||
},
|
||||
})
|
||||
|
||||
t.notThrows(() => agent.assertNoPendingInterceptors())
|
||||
})
|
||||
|
||||
test('DCR uses client_secret_post by default when client_secret was returned', async (t) => {
|
||||
let agent = new undici.MockAgent()
|
||||
agent.disableNetConnect()
|
||||
|
||||
const issuer = new URL('https://op.example.com')
|
||||
|
||||
const mockAgent = agent.get(issuer.origin)
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'GET',
|
||||
path: '/.well-known/openid-configuration',
|
||||
})
|
||||
.reply(200, {
|
||||
issuer: issuer.href,
|
||||
registration_endpoint: `${issuer.origin}/reg`,
|
||||
token_endpoint: `${issuer.origin}/token`,
|
||||
})
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'POST',
|
||||
path: '/reg',
|
||||
})
|
||||
.reply(201, {
|
||||
client_id: 'foo',
|
||||
client_secret: 'bar',
|
||||
client_secret_expires_at: 0,
|
||||
})
|
||||
|
||||
const config = await client.dynamicClientRegistration(issuer, {}, undefined, {
|
||||
// @ts-ignore
|
||||
[client.customFetch](url, options) {
|
||||
return undici.fetch(url, { ...options, dispatcher: agent })
|
||||
},
|
||||
})
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'POST',
|
||||
path: '/token',
|
||||
body(body) {
|
||||
const params = new URLSearchParams(body)
|
||||
t.true(params.has('client_id'))
|
||||
t.true(params.has('client_secret'))
|
||||
return true
|
||||
},
|
||||
})
|
||||
.reply(200, {
|
||||
access_token: 'token',
|
||||
token_type: 'bearer',
|
||||
})
|
||||
.times(2)
|
||||
|
||||
await client.clientCredentialsGrant(config)
|
||||
await client.clientCredentialsGrant(config)
|
||||
|
||||
t.notThrows(() => agent.assertNoPendingInterceptors())
|
||||
})
|
||||
|
||||
test('DCR uses none by default when client_secret was not returned', async (t) => {
|
||||
let agent = new undici.MockAgent()
|
||||
agent.disableNetConnect()
|
||||
|
||||
const issuer = new URL('https://op.example.com')
|
||||
|
||||
const mockAgent = agent.get(issuer.origin)
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'GET',
|
||||
path: '/.well-known/openid-configuration',
|
||||
})
|
||||
.reply(200, {
|
||||
issuer: issuer.href,
|
||||
registration_endpoint: `${issuer.origin}/reg`,
|
||||
token_endpoint: `${issuer.origin}/token`,
|
||||
})
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'POST',
|
||||
path: '/reg',
|
||||
})
|
||||
.reply(201, {
|
||||
client_id: 'foo',
|
||||
})
|
||||
|
||||
const config = await client.dynamicClientRegistration(issuer, {}, undefined, {
|
||||
// @ts-ignore
|
||||
[client.customFetch](url, options) {
|
||||
return undici.fetch(url, { ...options, dispatcher: agent })
|
||||
},
|
||||
})
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'POST',
|
||||
path: '/token',
|
||||
body(body) {
|
||||
const params = new URLSearchParams(body)
|
||||
t.true(params.has('client_id'))
|
||||
return true
|
||||
},
|
||||
})
|
||||
.reply(200, {
|
||||
access_token: 'token',
|
||||
token_type: 'bearer',
|
||||
})
|
||||
.times(2)
|
||||
|
||||
await client.clientCredentialsGrant(config)
|
||||
await client.clientCredentialsGrant(config)
|
||||
|
||||
t.notThrows(() => agent.assertNoPendingInterceptors())
|
||||
})
|
||||
|
||||
test('DCR can use ClientSecretBasic without having a secret at hand prior to the registration', async (t) => {
|
||||
let agent = new undici.MockAgent()
|
||||
agent.disableNetConnect()
|
||||
|
||||
const issuer = new URL('https://op.example.com')
|
||||
|
||||
const mockAgent = agent.get(issuer.origin)
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'GET',
|
||||
path: '/.well-known/openid-configuration',
|
||||
})
|
||||
.reply(200, {
|
||||
issuer: issuer.href,
|
||||
registration_endpoint: `${issuer.origin}/reg`,
|
||||
token_endpoint: `${issuer.origin}/token`,
|
||||
})
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'POST',
|
||||
path: '/reg',
|
||||
})
|
||||
.reply(201, {
|
||||
client_id: 'foo',
|
||||
client_secret: 'bar',
|
||||
client_secret_expires_at: 0,
|
||||
})
|
||||
|
||||
const config = await client.dynamicClientRegistration(
|
||||
issuer,
|
||||
{},
|
||||
client.ClientSecretBasic(),
|
||||
{
|
||||
// @ts-ignore
|
||||
[client.customFetch](url, options) {
|
||||
return undici.fetch(url, { ...options, dispatcher: agent })
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'POST',
|
||||
path: '/token',
|
||||
headers(headers) {
|
||||
return headers.authorization === 'Basic Zm9vOmJhcg=='
|
||||
},
|
||||
})
|
||||
.reply(200, {
|
||||
access_token: 'token',
|
||||
token_type: 'bearer',
|
||||
})
|
||||
.times(2)
|
||||
|
||||
await client.clientCredentialsGrant(config)
|
||||
await client.clientCredentialsGrant(config)
|
||||
|
||||
t.notThrows(() => agent.assertNoPendingInterceptors())
|
||||
})
|
||||
|
||||
test('DCR can use ClientSecretPost without having a secret at hand prior to the registration', async (t) => {
|
||||
let agent = new undici.MockAgent()
|
||||
agent.disableNetConnect()
|
||||
|
||||
const issuer = new URL('https://op.example.com')
|
||||
|
||||
const mockAgent = agent.get(issuer.origin)
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'GET',
|
||||
path: '/.well-known/openid-configuration',
|
||||
})
|
||||
.reply(200, {
|
||||
issuer: issuer.href,
|
||||
registration_endpoint: `${issuer.origin}/reg`,
|
||||
token_endpoint: `${issuer.origin}/token`,
|
||||
})
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'POST',
|
||||
path: '/reg',
|
||||
})
|
||||
.reply(201, {
|
||||
client_id: 'foo',
|
||||
client_secret: 'bar',
|
||||
client_secret_expires_at: 0,
|
||||
})
|
||||
|
||||
const config = await client.dynamicClientRegistration(
|
||||
issuer,
|
||||
{},
|
||||
client.ClientSecretPost(),
|
||||
{
|
||||
// @ts-ignore
|
||||
[client.customFetch](url, options) {
|
||||
return undici.fetch(url, { ...options, dispatcher: agent })
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'POST',
|
||||
path: '/token',
|
||||
body(body) {
|
||||
const params = new URLSearchParams(body)
|
||||
t.true(params.has('client_id'))
|
||||
t.true(params.has('client_secret'))
|
||||
return true
|
||||
},
|
||||
})
|
||||
.reply(200, {
|
||||
access_token: 'token',
|
||||
token_type: 'bearer',
|
||||
})
|
||||
.times(2)
|
||||
|
||||
await client.clientCredentialsGrant(config)
|
||||
await client.clientCredentialsGrant(config)
|
||||
|
||||
t.notThrows(() => agent.assertNoPendingInterceptors())
|
||||
})
|
||||
|
||||
test('DCR can use ClientSecretJwt without having a secret at hand prior to the registration', async (t) => {
|
||||
let agent = new undici.MockAgent()
|
||||
agent.disableNetConnect()
|
||||
|
||||
const issuer = new URL('https://op.example.com')
|
||||
|
||||
const mockAgent = agent.get(issuer.origin)
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'GET',
|
||||
path: '/.well-known/openid-configuration',
|
||||
})
|
||||
.reply(200, {
|
||||
issuer: issuer.href,
|
||||
registration_endpoint: `${issuer.origin}/reg`,
|
||||
token_endpoint: `${issuer.origin}/token`,
|
||||
})
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'POST',
|
||||
path: '/reg',
|
||||
})
|
||||
.reply(201, {
|
||||
client_id: 'foo',
|
||||
client_secret: 'bar',
|
||||
client_secret_expires_at: 0,
|
||||
})
|
||||
|
||||
const config = await client.dynamicClientRegistration(
|
||||
issuer,
|
||||
{},
|
||||
client.ClientSecretJwt(),
|
||||
{
|
||||
// @ts-ignore
|
||||
[client.customFetch](url, options) {
|
||||
return undici.fetch(url, { ...options, dispatcher: agent })
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
mockAgent
|
||||
.intercept({
|
||||
method: 'POST',
|
||||
path: '/token',
|
||||
body(body) {
|
||||
const params = new URLSearchParams(body)
|
||||
t.true(params.has('client_id'))
|
||||
t.true(params.has('client_assertion_type'))
|
||||
t.true(params.has('client_assertion'))
|
||||
return true
|
||||
},
|
||||
})
|
||||
.reply(200, {
|
||||
access_token: 'token',
|
||||
token_type: 'bearer',
|
||||
})
|
||||
.times(2)
|
||||
|
||||
await client.clientCredentialsGrant(config)
|
||||
await client.clientCredentialsGrant(config)
|
||||
|
||||
t.notThrows(() => agent.assertNoPendingInterceptors())
|
||||
})
|
||||
Reference in New Issue
Block a user