mirror of
https://github.com/hanzoai/openid-client.git
synced 2026-09-24 17:01:10 +00:00
feat: support response_type=id_token OIDC Authentication Responses
This commit is contained in:
@@ -33,11 +33,20 @@ jobs:
|
||||
variant:
|
||||
client_registration: dynamic_client
|
||||
|
||||
# Core 1.0 response_type=id_token
|
||||
- plan: oidcc-client-implicit-certification-test-plan
|
||||
- plan: oidcc-client-implicit-certification-test-plan
|
||||
variant:
|
||||
client_registration: dynamic_client
|
||||
|
||||
# Comprehensive Core 1.0 client test
|
||||
- plan: oidcc-client-test-plan
|
||||
- plan: oidcc-client-test-plan
|
||||
variant:
|
||||
response_type: code id_token
|
||||
- plan: oidcc-client-test-plan
|
||||
variant:
|
||||
response_type: id_token
|
||||
|
||||
# Financial-grade API Security Profile 1.0 - Part 2: Advanced
|
||||
- plan: fapi1-advanced-final-client-test-plan
|
||||
|
||||
@@ -19,6 +19,7 @@ const {
|
||||
|
||||
switch (PLAN_NAME) {
|
||||
case 'oidcc-client-basic-certification-test-plan':
|
||||
case 'oidcc-client-implicit-certification-test-plan':
|
||||
case 'oidcc-client-test-plan':
|
||||
case 'oidcc-client-hybrid-certification-test-plan':
|
||||
case 'fapi1-advanced-final-client-test-plan':
|
||||
@@ -68,6 +69,10 @@ const DEFAULTS: Record<typeof PLAN_NAME, Record<string, string>> = {
|
||||
request_type: 'plain_http_request',
|
||||
client_registration: 'static_client', // dynamic_client
|
||||
},
|
||||
'oidcc-client-implicit-certification-test-plan': {
|
||||
request_type: 'plain_http_request',
|
||||
client_registration: 'static_client', // dynamic_client
|
||||
},
|
||||
'oidcc-client-hybrid-certification-test-plan': {
|
||||
request_type: 'plain_http_request',
|
||||
client_registration: 'static_client', // dynamic_client
|
||||
|
||||
+58
-38
@@ -59,6 +59,7 @@ switch (plan.name) {
|
||||
break
|
||||
case 'oidcc-client-test-plan':
|
||||
case 'oidcc-client-basic-certification-test-plan':
|
||||
case 'oidcc-client-implicit-certification-test-plan':
|
||||
case 'oidcc-client-hybrid-certification-test-plan':
|
||||
prefix = 'oidcc-client-test-'
|
||||
break
|
||||
@@ -287,6 +288,10 @@ export const flow = (options?: MacroOptions) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (response_type === 'id_token') {
|
||||
execute.push(client.useIdTokenResponseType)
|
||||
}
|
||||
|
||||
const [jwk] = configuration.client.jwks.keys
|
||||
const clientPrivateKey = {
|
||||
kid: jwk.kid,
|
||||
@@ -472,54 +477,69 @@ export const flow = (options?: MacroOptions) => {
|
||||
|
||||
t.log('response redirect to', currentUrl.href)
|
||||
|
||||
const response = await client.authorizationCodeGrant(
|
||||
config,
|
||||
input,
|
||||
{
|
||||
expectedNonce: nonce,
|
||||
expectedState: state,
|
||||
pkceCodeVerifier: code_verifier,
|
||||
},
|
||||
undefined,
|
||||
{ DPoP },
|
||||
)
|
||||
|
||||
t.log('token endpoint response', { ...response })
|
||||
|
||||
if (
|
||||
!plan.name.startsWith('fapi1') &&
|
||||
scope.includes('openid') &&
|
||||
config.serverMetadata().userinfo_endpoint
|
||||
) {
|
||||
// fetch userinfo response
|
||||
t.log('fetching', config.serverMetadata().userinfo_endpoint)
|
||||
const userinfo = await client.fetchUserInfo(
|
||||
if (response_type === 'id_token') {
|
||||
const response = await client.implicitAuthentication(
|
||||
config,
|
||||
response.access_token,
|
||||
response.claims()?.sub!,
|
||||
currentUrl,
|
||||
nonce!,
|
||||
{
|
||||
DPoP,
|
||||
expectedState: state,
|
||||
},
|
||||
)
|
||||
t.log('userinfo endpoint response', { ...userinfo })
|
||||
}
|
||||
|
||||
if (accounts_endpoint) {
|
||||
const resource = await client.fetchProtectedResource(
|
||||
t.log('validated ID Token Claims Set', {
|
||||
...response,
|
||||
})
|
||||
} else {
|
||||
const response = await client.authorizationCodeGrant(
|
||||
config,
|
||||
response.access_token,
|
||||
new URL(accounts_endpoint),
|
||||
'GET',
|
||||
null,
|
||||
input,
|
||||
{
|
||||
expectedNonce: nonce,
|
||||
expectedState: state,
|
||||
pkceCodeVerifier: code_verifier,
|
||||
},
|
||||
undefined,
|
||||
{ DPoP },
|
||||
)
|
||||
|
||||
const result = await resource.text()
|
||||
try {
|
||||
t.log('accounts endpoint response', JSON.parse(result))
|
||||
} catch {
|
||||
t.log('accounts endpoint response body', result)
|
||||
t.log('token endpoint response', { ...response })
|
||||
|
||||
if (
|
||||
!plan.name.startsWith('fapi1') &&
|
||||
scope.includes('openid') &&
|
||||
config.serverMetadata().userinfo_endpoint
|
||||
) {
|
||||
// fetch userinfo response
|
||||
t.log('fetching', config.serverMetadata().userinfo_endpoint)
|
||||
const userinfo = await client.fetchUserInfo(
|
||||
config,
|
||||
response.access_token,
|
||||
response.claims()?.sub!,
|
||||
{
|
||||
DPoP,
|
||||
},
|
||||
)
|
||||
t.log('userinfo endpoint response', { ...userinfo })
|
||||
}
|
||||
|
||||
if (accounts_endpoint) {
|
||||
const resource = await client.fetchProtectedResource(
|
||||
config,
|
||||
response.access_token,
|
||||
new URL(accounts_endpoint),
|
||||
'GET',
|
||||
null,
|
||||
undefined,
|
||||
{ DPoP },
|
||||
)
|
||||
|
||||
const result = await resource.text()
|
||||
try {
|
||||
t.log('accounts endpoint response', JSON.parse(result))
|
||||
} catch {
|
||||
t.log('accounts endpoint response body', result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ Support from the community to continue maintaining and improving this module is
|
||||
- [getJwksCache](functions/getJwksCache.md)
|
||||
- [setJwksCache](functions/setJwksCache.md)
|
||||
- [useCodeIdTokenResponseType](functions/useCodeIdTokenResponseType.md)
|
||||
- [useIdTokenResponseType](functions/useIdTokenResponseType.md)
|
||||
- [useJwtResponseMode](functions/useJwtResponseMode.md)
|
||||
|
||||
## Client Authentication Methods
|
||||
@@ -80,6 +81,7 @@ Support from the community to continue maintaining and improving this module is
|
||||
- [buildEndSessionUrl](functions/buildEndSessionUrl.md)
|
||||
- [discovery](functions/discovery.md)
|
||||
- [fetchUserInfo](functions/fetchUserInfo.md)
|
||||
- [implicitAuthentication](functions/implicitAuthentication.md)
|
||||
|
||||
## PKCE
|
||||
|
||||
@@ -119,6 +121,7 @@ Support from the community to continue maintaining and improving this module is
|
||||
- [ExportedJWKSCache](interfaces/ExportedJWKSCache.md)
|
||||
- [GenerateKeyPairOptions](interfaces/GenerateKeyPairOptions.md)
|
||||
- [IDToken](interfaces/IDToken.md)
|
||||
- [ImplicitAuthenticationResponseChecks](interfaces/ImplicitAuthenticationResponseChecks.md)
|
||||
- [IntrospectionResponse](interfaces/IntrospectionResponse.md)
|
||||
- [JWK](interfaces/JWK.md)
|
||||
- [JWKS](interfaces/JWKS.md)
|
||||
|
||||
@@ -15,6 +15,9 @@ Note:
|
||||
[URL of the authorization server's authorization endpoint](../interfaces/ServerMetadata.md#authorization_endpoint)
|
||||
must be configured.
|
||||
|
||||
Note: When used, PKCE code challenge, state, and nonce parameter values must
|
||||
always be random and be tied to the user-agent.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|
||||
@@ -18,8 +18,8 @@ memory cache between their code's invocations. Use in runtimes where an in
|
||||
memory cache between requests is available is not desirable.
|
||||
|
||||
Note: the client only uses the authorization server's JWK Set when
|
||||
[enableNonRepudiationChecks](enableNonRepudiationChecks.md), [useJwtResponseMode](useJwtResponseMode.md), or
|
||||
[useCodeIdTokenResponseType](useCodeIdTokenResponseType.md) is used.
|
||||
[enableNonRepudiationChecks](enableNonRepudiationChecks.md), [useJwtResponseMode](useJwtResponseMode.md),
|
||||
[useCodeIdTokenResponseType](useCodeIdTokenResponseType.md), or [useIdTokenResponseType](useIdTokenResponseType.md) is used.
|
||||
|
||||
## Parameters
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# Function: implicitAuthentication()
|
||||
|
||||
[💗 Help the project](https://github.com/sponsors/panva)
|
||||
|
||||
Support from the community to continue maintaining and improving this module is welcome. If you find the module useful, please consider supporting the project by [becoming a sponsor](https://github.com/sponsors/panva).
|
||||
|
||||
***
|
||||
|
||||
▸ **implicitAuthentication**(`config`, `currentUrl`, `expectedNonce`, `checks`?): [`Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)\<[`IDToken`](../interfaces/IDToken.md)\>
|
||||
|
||||
This method validates the authorization server's
|
||||
[Implicit Authentication Flow](https://openid.net/specs/openid-connect-core-1_0-errata2.html#ImplicitFlowAuth)
|
||||
Response.
|
||||
|
||||
Note:
|
||||
[URL of the authorization server's JWK Set document](../interfaces/ServerMetadata.md#jwks_uri)
|
||||
must be configured.
|
||||
|
||||
Note: Only `response_type=id_token` responses are supported and prior use of
|
||||
[useIdTokenResponseType](useIdTokenResponseType.md) is required.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| ------ | ------ | ------ |
|
||||
| `config` | [`Configuration`](../classes/Configuration.md) | - |
|
||||
| `currentUrl` | [`URL`](https://developer.mozilla.org/docs/Web/API/URL) \| [`Request`](https://developer.mozilla.org/docs/Web/API/Request) | Current [URL](https://developer.mozilla.org/docs/Web/API/URL) the Authorization Server provided an Authorization Response to or a [Request](https://developer.mozilla.org/docs/Web/API/Request), the [Authentication Response Parameters](https://openid.net/specs/openid-connect-core-1_0-errata2.html#ImplicitAuthResponse) are extracted from this. |
|
||||
| `expectedNonce` | `string` | Expected value of the `nonce` ID Token claim. This value must match exactly. |
|
||||
| `checks`? | [`ImplicitAuthenticationResponseChecks`](../interfaces/ImplicitAuthenticationResponseChecks.md) | Additional optional Implicit Authentication Response checks |
|
||||
|
||||
## Returns
|
||||
|
||||
[`Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)\<[`IDToken`](../interfaces/IDToken.md)\>
|
||||
|
||||
ID Token Claims Set
|
||||
|
||||
## Examples
|
||||
|
||||
In a browser
|
||||
|
||||
```ts
|
||||
let config!: client.Configuration
|
||||
let expectedNonce!: string
|
||||
|
||||
let idTokenClaims = await client.implicitAuthentication(
|
||||
config,
|
||||
new URL(location.href),
|
||||
expectedNonce,
|
||||
)
|
||||
```
|
||||
|
||||
On a server in response to a `form_post` response mode
|
||||
|
||||
```ts
|
||||
let config!: client.Configuration
|
||||
let expectedNonce!: string
|
||||
let getCurrentUrl!: (...args: any) => URL
|
||||
let getBody!: (...args: any) => Record<string, string>
|
||||
|
||||
let url = getCurrentUrl()
|
||||
url.hash = new URLSearchParams(getBody()).toString()
|
||||
|
||||
let idTokenClaims = await client.implicitAuthentication(
|
||||
config,
|
||||
url,
|
||||
expectedNonce,
|
||||
)
|
||||
```
|
||||
|
||||
On a server in response to a `form_post` response mode using an incoming
|
||||
[Request](https://developer.mozilla.org/docs/Web/API/Request) object
|
||||
|
||||
```ts
|
||||
let config!: client.Configuration
|
||||
let expectedNonce!: string
|
||||
let request!: Request
|
||||
|
||||
let idTokenClaims = await client.implicitAuthentication(
|
||||
config,
|
||||
request,
|
||||
expectedNonce,
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1,62 @@
|
||||
# Function: useIdTokenResponseType()
|
||||
|
||||
[💗 Help the project](https://github.com/sponsors/panva)
|
||||
|
||||
Support from the community to continue maintaining and improving this module is welcome. If you find the module useful, please consider supporting the project by [becoming a sponsor](https://github.com/sponsors/panva).
|
||||
|
||||
***
|
||||
|
||||
▸ **useIdTokenResponseType**(`config`): `void`
|
||||
|
||||
This changes the `response_type` used by the client to be `id_token`, this
|
||||
subsequently requires that the authorization server response be passed to
|
||||
[implicitAuthentication](implicitAuthentication.md) (instead of [authorizationCodeGrant](authorizationCodeGrant.md)) and
|
||||
for it to be one described by
|
||||
[OpenID Connect 1.0 Implicit Flow](https://openid.net/specs/openid-connect-core-1_0-errata2.html#ImplicitFlowAuth).
|
||||
|
||||
Note:
|
||||
[URL of the authorization server's JWK Set document](../interfaces/ServerMetadata.md#jwks_uri)
|
||||
must be configured.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type |
|
||||
| ------ | ------ |
|
||||
| `config` | [`Configuration`](../classes/Configuration.md) |
|
||||
|
||||
## Returns
|
||||
|
||||
`void`
|
||||
|
||||
## Examples
|
||||
|
||||
Usage with a [Configuration](../classes/Configuration.md) obtained through [discovery](discovery.md)
|
||||
|
||||
```ts
|
||||
let server!: URL
|
||||
let clientId!: string
|
||||
let clientMetadata!: Partial<client.ClientMetadata> | undefined
|
||||
let clientAuth = client.None()
|
||||
|
||||
let config = await client.discovery(
|
||||
server,
|
||||
clientId,
|
||||
clientMetadata,
|
||||
clientAuth,
|
||||
{
|
||||
execute: [client.useIdTokenResponseType],
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
Usage with a [Configuration](../classes/Configuration.md) instance
|
||||
|
||||
```ts
|
||||
let config!: client.Configuration
|
||||
|
||||
client.useIdTokenResponseType(config)
|
||||
```
|
||||
|
||||
## See
|
||||
|
||||
[OpenID Connect 1.0 Hybrid Flow](https://openid.net/specs/openid-connect-core-1_0-errata2.html#HybridFlowAuth)
|
||||
@@ -99,6 +99,7 @@ let config = await client.discovery(
|
||||
- [allowInsecureRequests](../functions/allowInsecureRequests.md)
|
||||
- [enableNonRepudiationChecks](../functions/enableNonRepudiationChecks.md)
|
||||
- [useCodeIdTokenResponseType](../functions/useCodeIdTokenResponseType.md)
|
||||
- [useIdTokenResponseType](../functions/useIdTokenResponseType.md)
|
||||
- [enableDetachedSignatureResponseChecks](../functions/enableDetachedSignatureResponseChecks.md)
|
||||
- [useJwtResponseMode](../functions/useJwtResponseMode.md)
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ let config = await client.discovery(
|
||||
- [allowInsecureRequests](../functions/allowInsecureRequests.md)
|
||||
- [enableNonRepudiationChecks](../functions/enableNonRepudiationChecks.md)
|
||||
- [useCodeIdTokenResponseType](../functions/useCodeIdTokenResponseType.md)
|
||||
- [useIdTokenResponseType](../functions/useIdTokenResponseType.md)
|
||||
- [enableDetachedSignatureResponseChecks](../functions/enableDetachedSignatureResponseChecks.md)
|
||||
- [useJwtResponseMode](../functions/useJwtResponseMode.md)
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Interface: ImplicitAuthenticationResponseChecks
|
||||
|
||||
[💗 Help the project](https://github.com/sponsors/panva)
|
||||
|
||||
Support from the community to continue maintaining and improving this module is welcome. If you find the module useful, please consider supporting the project by [becoming a sponsor](https://github.com/sponsors/panva).
|
||||
|
||||
***
|
||||
|
||||
## Properties
|
||||
|
||||
### expectedState?
|
||||
|
||||
• `optional` **expectedState**: `string` \| *typeof* `skipStateCheck`
|
||||
|
||||
Expected value of the `state` authorization response parameter. This value
|
||||
must match exactly. When `undefined` the expectation is that there is no
|
||||
`state` in the authorization response.
|
||||
|
||||
***
|
||||
|
||||
### maxAge?
|
||||
|
||||
• `optional` **maxAge**: `number`
|
||||
|
||||
ID Token [\`auth\_time\`](IDToken.md#auth_time) claim value will be checked
|
||||
to be present and conform to this `maxAge` value. Use of this option is
|
||||
required if you sent a `max_age` parameter in the authorization request.
|
||||
Default is [ClientMetadata.default\_max\_age](ClientMetadata.md#default_max_age) and falls back to not
|
||||
checking the claim's value beyond it being a number when present.
|
||||
+285
-16
@@ -40,6 +40,7 @@ interface Internal {
|
||||
nonRepudiation?: NonRepudiationImplementation
|
||||
decrypt?: oauth.JweDecryptFunction
|
||||
jwksCache: oauth.JWKSCacheInput
|
||||
implicit?: boolean
|
||||
}
|
||||
|
||||
const int = (config: Configuration) => {
|
||||
@@ -1061,6 +1062,7 @@ export interface DiscoveryRequestOptions {
|
||||
* @see {@link allowInsecureRequests}
|
||||
* @see {@link enableNonRepudiationChecks}
|
||||
* @see {@link useCodeIdTokenResponseType}
|
||||
* @see {@link useIdTokenResponseType}
|
||||
* @see {@link enableDetachedSignatureResponseChecks}
|
||||
* @see {@link useJwtResponseMode}
|
||||
*/
|
||||
@@ -2556,8 +2558,8 @@ export function setJwksCache(
|
||||
* memory cache between requests is available is not desirable.
|
||||
*
|
||||
* Note: the client only uses the authorization server's JWK Set when
|
||||
* {@link enableNonRepudiationChecks}, {@link useJwtResponseMode}, or
|
||||
* {@link useCodeIdTokenResponseType} is used.
|
||||
* {@link enableNonRepudiationChecks}, {@link useJwtResponseMode},
|
||||
* {@link useCodeIdTokenResponseType}, or {@link useIdTokenResponseType} is used.
|
||||
*
|
||||
* @group Advanced Configuration
|
||||
*/
|
||||
@@ -2687,9 +2689,11 @@ export function enableNonRepudiationChecks(config: Configuration) {
|
||||
export function useJwtResponseMode(config: Configuration) {
|
||||
checkConfig(config)
|
||||
|
||||
if (int(config).hybrid) {
|
||||
const { hybrid, implicit } = int(config)
|
||||
|
||||
if (hybrid || implicit) {
|
||||
throw e(
|
||||
'JARM cannot be combined with a hybrid response mode',
|
||||
'JARM cannot be combined with a hybrid or implicit response types',
|
||||
undefined,
|
||||
oauth.UNSUPPORTED_OPERATION,
|
||||
)
|
||||
@@ -2768,6 +2772,191 @@ export function enableDetachedSignatureResponseChecks(config: Configuration) {
|
||||
)
|
||||
}
|
||||
|
||||
export interface ImplicitAuthenticationResponseChecks
|
||||
extends Pick<AuthorizationCodeGrantChecks, 'expectedState' | 'maxAge'> {}
|
||||
|
||||
/**
|
||||
* This method validates the authorization server's
|
||||
* {@link https://openid.net/specs/openid-connect-core-1_0-errata2.html#ImplicitFlowAuth Implicit Authentication Flow}
|
||||
* Response.
|
||||
*
|
||||
* Note:
|
||||
* {@link ServerMetadata.jwks_uri URL of the authorization server's JWK Set document}
|
||||
* must be configured.
|
||||
*
|
||||
* Note: Only `response_type=id_token` responses are supported and prior use of
|
||||
* {@link useIdTokenResponseType} is required.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* In a browser
|
||||
*
|
||||
* ```ts
|
||||
* let config!: client.Configuration
|
||||
* let expectedNonce!: string
|
||||
*
|
||||
* let idTokenClaims = await client.implicitAuthentication(
|
||||
* config,
|
||||
* new URL(location.href),
|
||||
* expectedNonce,
|
||||
* )
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* On a server in response to a `form_post` response mode
|
||||
*
|
||||
* ```ts
|
||||
* let config!: client.Configuration
|
||||
* let expectedNonce!: string
|
||||
* let getCurrentUrl!: (...args: any) => URL
|
||||
* let getBody!: (...args: any) => Record<string, string>
|
||||
*
|
||||
* let url = getCurrentUrl()
|
||||
* url.hash = new URLSearchParams(getBody()).toString()
|
||||
*
|
||||
* let idTokenClaims = await client.implicitAuthentication(
|
||||
* config,
|
||||
* url,
|
||||
* expectedNonce,
|
||||
* )
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* On a server in response to a `form_post` response mode using an incoming
|
||||
* {@link !Request} object
|
||||
*
|
||||
* ```ts
|
||||
* let config!: client.Configuration
|
||||
* let expectedNonce!: string
|
||||
* let request!: Request
|
||||
*
|
||||
* let idTokenClaims = await client.implicitAuthentication(
|
||||
* config,
|
||||
* request,
|
||||
* expectedNonce,
|
||||
* )
|
||||
* ```
|
||||
*
|
||||
* @param currentUrl Current {@link !URL} the Authorization Server provided an
|
||||
* Authorization Response to or a {@link !Request}, the
|
||||
* {@link https://openid.net/specs/openid-connect-core-1_0-errata2.html#ImplicitAuthResponse Authentication Response Parameters}
|
||||
* are extracted from this.
|
||||
* @param expectedNonce Expected value of the `nonce` ID Token claim. This value
|
||||
* must match exactly.
|
||||
* @param checks Additional optional Implicit Authentication Response checks
|
||||
*
|
||||
* @returns End-User Claims from the ID Token.
|
||||
*
|
||||
* @group OpenID Connect 1.0
|
||||
*/
|
||||
export async function implicitAuthentication(
|
||||
config: Configuration,
|
||||
currentUrl: URL | Request,
|
||||
expectedNonce: string,
|
||||
checks?: ImplicitAuthenticationResponseChecks,
|
||||
): Promise<oauth.IDToken> {
|
||||
checkConfig(config)
|
||||
|
||||
if (
|
||||
!(currentUrl instanceof URL) &&
|
||||
!webInstanceOf<Request>(currentUrl, 'Request')
|
||||
) {
|
||||
throw CodedTypeError(
|
||||
'"currentUrl" must be an instance of URL, or Request',
|
||||
ERR_INVALID_ARG_TYPE,
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof expectedNonce !== 'string') {
|
||||
throw CodedTypeError(
|
||||
'"expectedNonce" must be a string',
|
||||
ERR_INVALID_ARG_TYPE,
|
||||
)
|
||||
}
|
||||
|
||||
const { as, c, fetch, tlsOnly, timeout, decrypt, implicit, jwksCache } =
|
||||
int(config)
|
||||
|
||||
if (!implicit) {
|
||||
throw new TypeError(
|
||||
'implicitAuthentication() cannot be used by clients using flows other than response_type=id_token',
|
||||
)
|
||||
}
|
||||
|
||||
let params: URLSearchParams
|
||||
if (!(currentUrl instanceof URL)) {
|
||||
const request: Request = currentUrl
|
||||
switch (request.method) {
|
||||
case 'GET':
|
||||
params = new URLSearchParams(new URL(request.url).hash.slice(1))
|
||||
break
|
||||
case 'POST':
|
||||
// @ts-expect-error
|
||||
params = new URLSearchParams(await oauth.formPostResponse(request))
|
||||
break
|
||||
default:
|
||||
throw CodedTypeError(
|
||||
'unexpected Request HTTP method',
|
||||
ERR_INVALID_ARG_VALUE,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
params = new URLSearchParams(currentUrl.hash.slice(1))
|
||||
}
|
||||
|
||||
try {
|
||||
// validate state, iss, error codes etc
|
||||
{
|
||||
const decoy = new URLSearchParams(params)
|
||||
decoy.delete('id_token')
|
||||
oauth.validateAuthResponse(
|
||||
{
|
||||
...as,
|
||||
authorization_response_iss_parameter_supported: undefined,
|
||||
},
|
||||
c,
|
||||
decoy,
|
||||
checks?.expectedState,
|
||||
)
|
||||
}
|
||||
|
||||
// validate id_token
|
||||
{
|
||||
const decoy = new Response(
|
||||
JSON.stringify({
|
||||
access_token: 'decoy',
|
||||
token_type: 'bearer',
|
||||
id_token: params.get('id_token'),
|
||||
}),
|
||||
{
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
},
|
||||
)
|
||||
|
||||
const ref = await oauth.processAuthorizationCodeResponse(as, c, decoy, {
|
||||
expectedNonce,
|
||||
maxAge: checks?.maxAge,
|
||||
[oauth.jweDecrypt]: decrypt,
|
||||
})
|
||||
|
||||
// validate ID Token signature
|
||||
await oauth.validateApplicationLevelSignature(as, decoy, {
|
||||
[oauth.customFetch]: fetch,
|
||||
[oauth.allowInsecureRequests]: !tlsOnly,
|
||||
headers: new Headers(headers),
|
||||
signal: signal(timeout),
|
||||
[oauth.jwksCache]: jwksCache,
|
||||
})
|
||||
|
||||
return oauth.getValidatedIdTokenClaims(ref)!
|
||||
}
|
||||
} catch (err) {
|
||||
errorHandler(err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This changes the `response_type` used by the client to be `code id_token` and
|
||||
* expects the authorization server response passed to
|
||||
@@ -2816,9 +3005,11 @@ export function enableDetachedSignatureResponseChecks(config: Configuration) {
|
||||
export function useCodeIdTokenResponseType(config: Configuration) {
|
||||
checkConfig(config)
|
||||
|
||||
if (int(config).jarm) {
|
||||
const { jarm, implicit } = int(config)
|
||||
|
||||
if (jarm || implicit) {
|
||||
throw e(
|
||||
'"code id_token" response type cannot be combined with JARM',
|
||||
'"code id_token" response type cannot be combined with JARM or implicit response type',
|
||||
undefined,
|
||||
oauth.UNSUPPORTED_OPERATION,
|
||||
)
|
||||
@@ -2840,6 +3031,68 @@ export function useCodeIdTokenResponseType(config: Configuration) {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* This changes the `response_type` used by the client to be `id_token`, this
|
||||
* subsequently requires that the authorization server response be passed to
|
||||
* {@link implicitAuthentication} (instead of {@link authorizationCodeGrant}) and
|
||||
* for it to be one described by
|
||||
* {@link https://openid.net/specs/openid-connect-core-1_0-errata2.html#ImplicitFlowAuth OpenID Connect 1.0 Implicit Flow}.
|
||||
*
|
||||
* Note:
|
||||
* {@link ServerMetadata.jwks_uri URL of the authorization server's JWK Set document}
|
||||
* must be configured.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* Usage with a {@link Configuration} obtained through {@link discovery}
|
||||
*
|
||||
* ```ts
|
||||
* let server!: URL
|
||||
* let clientId!: string
|
||||
* let clientMetadata!: Partial<client.ClientMetadata> | undefined
|
||||
* let clientAuth = client.None()
|
||||
*
|
||||
* let config = await client.discovery(
|
||||
* server,
|
||||
* clientId,
|
||||
* clientMetadata,
|
||||
* clientAuth,
|
||||
* {
|
||||
* execute: [client.useIdTokenResponseType],
|
||||
* },
|
||||
* )
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* Usage with a {@link Configuration} instance
|
||||
*
|
||||
* ```ts
|
||||
* let config!: client.Configuration
|
||||
*
|
||||
* client.useIdTokenResponseType(config)
|
||||
* ```
|
||||
*
|
||||
* @group Advanced Configuration
|
||||
*
|
||||
* @see {@link https://openid.net/specs/openid-connect-core-1_0-errata2.html#HybridFlowAuth OpenID Connect 1.0 Hybrid Flow}
|
||||
*/
|
||||
export function useIdTokenResponseType(config: Configuration) {
|
||||
checkConfig(config)
|
||||
|
||||
const { jarm, hybrid } = int(config)
|
||||
|
||||
if (jarm || hybrid) {
|
||||
throw e(
|
||||
'"id_token" response type cannot be combined with JARM or hybrid response type',
|
||||
undefined,
|
||||
oauth.UNSUPPORTED_OPERATION,
|
||||
)
|
||||
}
|
||||
|
||||
int(config).implicit = true
|
||||
}
|
||||
|
||||
export interface AuthorizationCodeGrantChecks {
|
||||
/**
|
||||
* Expected value of the `nonce` ID Token claim. This value must match
|
||||
@@ -2968,7 +3221,13 @@ export async function authorizationCodeGrant(
|
||||
|
||||
let redirectUri: string
|
||||
|
||||
const { as, c, auth, fetch, tlsOnly, jarm, hybrid, nonRepudiation, timeout, decrypt } = int(config) // prettier-ignore
|
||||
const { as, c, auth, fetch, tlsOnly, jarm, hybrid, nonRepudiation, timeout, decrypt, implicit } = int(config) // prettier-ignore
|
||||
|
||||
if (implicit) {
|
||||
throw new TypeError(
|
||||
'authorizationCodeGrant() cannot be used by response_type=id_token clients',
|
||||
)
|
||||
}
|
||||
|
||||
if (options?.flag === retry) {
|
||||
authResponse = options.authResponse!
|
||||
@@ -3004,7 +3263,7 @@ export async function authorizationCodeGrant(
|
||||
checks?.expectedState,
|
||||
)
|
||||
} catch (err) {
|
||||
return errorHandler(err)
|
||||
errorHandler(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3295,6 +3554,9 @@ export async function clientCredentialsGrant(
|
||||
* {@link ServerMetadata.authorization_endpoint URL of the authorization server's authorization endpoint}
|
||||
* must be configured.
|
||||
*
|
||||
* Note: When used, PKCE code challenge, state, and nonce parameter values must
|
||||
* always be random and be tied to the user-agent.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
@@ -3332,7 +3594,7 @@ export function buildAuthorizationUrl(
|
||||
): URL {
|
||||
checkConfig(config)
|
||||
|
||||
const { as, c, tlsOnly, hybrid, jarm } = int(config)
|
||||
const { as, c, tlsOnly, hybrid, jarm, implicit } = int(config)
|
||||
// @ts-expect-error
|
||||
const authorizationEndpoint = oauth.resolveEndpoint(
|
||||
as,
|
||||
@@ -3348,7 +3610,16 @@ export function buildAuthorizationUrl(
|
||||
}
|
||||
if (!parameters.has('request_uri') && !parameters.has('request')) {
|
||||
if (!parameters.has('response_type')) {
|
||||
parameters.set('response_type', hybrid ? 'code id_token' : 'code')
|
||||
parameters.set(
|
||||
'response_type',
|
||||
hybrid ? 'code id_token' : implicit ? 'id_token' : 'code',
|
||||
)
|
||||
}
|
||||
if (implicit && !parameters.has('nonce')) {
|
||||
throw CodedTypeError(
|
||||
'response_type=id_token clients must provide a nonce parameter in their authorization request parameters',
|
||||
ERR_INVALID_ARG_VALUE,
|
||||
)
|
||||
}
|
||||
if (jarm) {
|
||||
parameters.set('response_mode', 'jwt')
|
||||
@@ -3727,7 +3998,8 @@ export async function fetchUserInfo(
|
||||
errorHandler(err)
|
||||
}
|
||||
|
||||
getContentType(response) === 'application/jwt' &&
|
||||
// @ts-expect-error
|
||||
oauth.getContentType(response) === 'application/jwt' &&
|
||||
(await nonRepudiation?.(response))
|
||||
|
||||
return result
|
||||
@@ -3788,7 +4060,8 @@ export async function tokenIntrospection(
|
||||
})
|
||||
.catch(errorHandler)
|
||||
|
||||
getContentType(response) === 'application/token-introspection+jwt' &&
|
||||
// @ts-expect-error
|
||||
oauth.getContentType(response) === 'application/token-introspection+jwt' &&
|
||||
(await nonRepudiation?.(response))
|
||||
|
||||
return result
|
||||
@@ -3997,10 +4270,6 @@ export async function fetchProtectedResource(
|
||||
return result
|
||||
}
|
||||
|
||||
function getContentType(response: Response): string | undefined {
|
||||
return response.headers.get('content-type')?.split(';')[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*
|
||||
|
||||
+47
-33
@@ -8,7 +8,7 @@ function label(testCase: Record<string, boolean>) {
|
||||
const keys = Object.keys(
|
||||
Object.fromEntries(Object.entries(testCase).filter(([, v]) => v === true)),
|
||||
)
|
||||
let msg = `w/ response_type=${testCase.hybrid ? 'code id_token' : 'code'}`
|
||||
let msg = `w/ response_type=${testCase.hybrid ? 'code id_token' : testCase.implicit ? 'id_token' : 'code'}`
|
||||
return keys.length ? `${msg}, ${keys.join(', ')}` : msg
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export default (QUnit: QUnit) => {
|
||||
| 'hybrid'
|
||||
| 'nonrepudiation'
|
||||
| 'encryption'
|
||||
| 'login'
|
||||
| 'implicit'
|
||||
>
|
||||
) => {
|
||||
const conf = {
|
||||
@@ -38,8 +38,8 @@ export default (QUnit: QUnit) => {
|
||||
dpop: false,
|
||||
jwtUserinfo: false,
|
||||
hybrid: false,
|
||||
login: false,
|
||||
encryption: false,
|
||||
implicit: false,
|
||||
nonrepudiation: false,
|
||||
}
|
||||
for (const flag of flags) {
|
||||
@@ -64,6 +64,8 @@ export default (QUnit: QUnit) => {
|
||||
options('jwtUserinfo', 'encryption'),
|
||||
options('hybrid'),
|
||||
options('hybrid', 'encryption'),
|
||||
options('implicit'),
|
||||
options('implicit', 'encryption'),
|
||||
]
|
||||
|
||||
for (const testCase of testCases) {
|
||||
@@ -74,6 +76,7 @@ export default (QUnit: QUnit) => {
|
||||
dpop,
|
||||
jwtUserinfo,
|
||||
hybrid,
|
||||
implicit,
|
||||
encryption,
|
||||
nonrepudiation,
|
||||
} = testCase
|
||||
@@ -95,7 +98,9 @@ export default (QUnit: QUnit) => {
|
||||
false,
|
||||
hybrid
|
||||
? ['implicit', 'authorization_code', 'refresh_token']
|
||||
: ['authorization_code', 'refresh_token'],
|
||||
: implicit
|
||||
? ['implicit']
|
||||
: ['authorization_code', 'refresh_token'],
|
||||
encryption,
|
||||
)
|
||||
|
||||
@@ -112,7 +117,9 @@ export default (QUnit: QUnit) => {
|
||||
if (hybrid) {
|
||||
execute.push(client.useCodeIdTokenResponseType)
|
||||
}
|
||||
|
||||
if (implicit) {
|
||||
execute.push(client.useIdTokenResponseType)
|
||||
}
|
||||
const config = await client.discovery(
|
||||
issuerIdentifier,
|
||||
metadata.client_id,
|
||||
@@ -158,7 +165,7 @@ export default (QUnit: QUnit) => {
|
||||
const maxAge = random() ? 30 : undefined
|
||||
|
||||
let nonce: string | undefined
|
||||
if (hybrid) {
|
||||
if (hybrid || implicit) {
|
||||
nonce = client.randomNonce()
|
||||
params.set('nonce', nonce)
|
||||
}
|
||||
@@ -229,36 +236,43 @@ export default (QUnit: QUnit) => {
|
||||
throw new Error('unreachable')
|
||||
}
|
||||
|
||||
const response = await client.authorizationCodeGrant(
|
||||
config,
|
||||
input,
|
||||
{
|
||||
expectedNonce: nonce,
|
||||
pkceCodeVerifier: code_verifier,
|
||||
},
|
||||
undefined,
|
||||
{ DPoP },
|
||||
)
|
||||
if (implicit) {
|
||||
await client.signIn(config, input, nonce!, {
|
||||
maxAge,
|
||||
})
|
||||
} else {
|
||||
const response = await client.authorizationCodeGrant(
|
||||
config,
|
||||
input,
|
||||
{
|
||||
expectedNonce: nonce,
|
||||
pkceCodeVerifier: code_verifier,
|
||||
maxAge,
|
||||
},
|
||||
undefined,
|
||||
{ DPoP },
|
||||
)
|
||||
|
||||
await client.fetchUserInfo(
|
||||
config,
|
||||
response.access_token,
|
||||
response.claims()?.sub!,
|
||||
{
|
||||
DPoP,
|
||||
},
|
||||
)
|
||||
await client.fetchUserInfo(
|
||||
config,
|
||||
response.access_token,
|
||||
response.claims()?.sub!,
|
||||
{
|
||||
DPoP,
|
||||
},
|
||||
)
|
||||
|
||||
await client.refreshTokenGrant(
|
||||
config,
|
||||
response.refresh_token!,
|
||||
undefined,
|
||||
{
|
||||
DPoP,
|
||||
},
|
||||
)
|
||||
await client.refreshTokenGrant(
|
||||
config,
|
||||
response.refresh_token!,
|
||||
undefined,
|
||||
{
|
||||
DPoP,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (jarm || hybrid || nonrepudiation) {
|
||||
if (jarm || hybrid || nonrepudiation || implicit) {
|
||||
const cache = client.getJwksCache(config)
|
||||
t.ok(cache?.uat)
|
||||
t.ok(cache?.jwks)
|
||||
|
||||
Reference in New Issue
Block a user