From cdcbbdef6d94da9420b906d3dfb68bb618e4f9d7 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 7 Feb 2024 09:42:34 +0100 Subject: [PATCH] docs: update all examples --- README.md | 30 ++- examples/.update-diffs.sh | 16 +- examples/README.md | 29 ++- examples/client_credentials.ts | 59 +++-- examples/client_secret_post.diff | 13 ++ examples/client_secret_post.ts | 117 ++++++++++ examples/device_authorization_grant.ts | 117 ++++++---- examples/dpop.diff | 62 +++--- examples/dpop.ts | 64 +++--- examples/fapi1-advanced.diff | 172 +++++++++++++++ examples/fapi1-advanced.ts | 67 +++++- examples/fapi2-message-signing.diff | 53 ----- examples/fapi2.diff | 208 ++++++++++++++++++ examples/fapi2.ts | 126 ++++++++--- examples/jar.diff | 61 +++++ examples/{fapi2-message-signing.ts => jar.ts} | 122 +++++----- examples/jarm.diff | 21 ++ examples/jarm.ts | 118 ++++++++++ examples/oauth.ts | 81 ++++--- examples/oidc.diff | 87 ++++++++ examples/{code.ts => oidc.ts} | 25 ++- examples/par.diff | 40 ++-- examples/par.ts | 58 ++--- examples/private_key_jwt.diff | 16 +- examples/private_key_jwt.ts | 50 +++-- examples/public.diff | 14 +- examples/public.ts | 50 +++-- examples/refresh_token.diff | 47 ++++ examples/refresh_token.ts | 140 ++++++++++++ 29 files changed, 1617 insertions(+), 446 deletions(-) create mode 100644 examples/client_secret_post.diff create mode 100644 examples/client_secret_post.ts create mode 100644 examples/fapi1-advanced.diff delete mode 100644 examples/fapi2-message-signing.diff create mode 100644 examples/fapi2.diff create mode 100644 examples/jar.diff rename examples/{fapi2-message-signing.ts => jar.ts} (54%) create mode 100644 examples/jarm.diff create mode 100644 examples/jarm.ts create mode 100644 examples/oidc.diff rename examples/{code.ts => oidc.ts} (82%) create mode 100644 examples/refresh_token.diff create mode 100644 examples/refresh_token.ts diff --git a/README.md b/README.md index d5e68996..9ad882dd 100644 --- a/README.md +++ b/README.md @@ -47,16 +47,26 @@ import * as oauth2 from 'oauth4webapi' import * as oauth2 from 'https://deno.land/x/oauth4webapi@v2.10.2/mod.ts' ``` -- Authorization Code Flow - OpenID Connect [source](examples/code.ts), or plain OAuth 2 [source](examples/oauth.ts) -- Public Client Authorization Code Flow - [source](examples/public.ts) | [diff from code flow](examples/public.diff) -- Private Key JWT Client Authentication - [source](examples/private_key_jwt.ts) | [diff from code flow](examples/private_key_jwt.diff) -- DPoP - [source](examples/dpop.ts) | [diff from code flow](examples/dpop.diff) -- Pushed Authorization Request (PAR) - [source](examples/par.ts) | [diff from code flow](examples/par.diff) -- Client Credentials Grant - [source](examples/client_credentials.ts) -- Device Authorization Grant - [source](examples/device_authorization_grant.ts) -- FAPI 1.0 Advanced (Private Key JWT, MTLS, JAR) - [source](examples/fapi1-advanced.ts) -- FAPI 2.0 Security Profile (Private Key JWT, PAR, DPoP) - [source](examples/fapi2.ts) -- FAPI 2.0 Message Signing (Private Key JWT, PAR, DPoP, JAR, JARM) - [source](examples/fapi2-message-signing.ts) | [diff from FAPI 2.0 SP](examples/fapi2-message-signing.diff) +- Authorization Code Flow (OAuth 2.0) - [source](examples/oauth.ts) +- Authorization Code Flow (OpenID Connect) - [source](examples/oidc.ts) | [diff](examples/oidc.diff) +- Extensions + - DPoP - [source](examples/dpop.ts) | [diff](examples/dpop.diff) + - JWT Secured Authorization Request (JAR) - [source](examples/jar.ts) | [diff](examples/jar.diff) + - JWT Secured Authorization Response Mode (JARM) - [source](examples/jarm.ts) | [diff](examples/jarm.diff) + - Pushed Authorization Request (PAR) - [source](examples/par.ts) | [diff](examples/par.diff) +- Client Authentication + - Client Secret in HTTP Authorization Header - [source](examples/oauth.ts) + - Client Secret in HTTP Body - [source](examples/client_secret_post.ts) | [diff](examples/client_secret_post.diff) + - Private Key JWT Client Authentication - [source](examples/private_key_jwt.ts) | [diff](examples/private_key_jwt.diff) + - Public Client - [source](examples/public.ts) | [diff](examples/public.diff) +- Other Grants + - Client Credentials Grant - [source](examples/client_credentials.ts) + - Device Authorization Grant - [source](examples/device_authorization_grant.ts) + - Refresh Token Grant - [source](examples/refresh_token.ts) | [diff](examples/refresh_token.diff) +- FAPI + - FAPI 1.0 Advanced (Private Key JWT, MTLS, JAR) - [source](examples/fapi1-advanced.ts) | [diff](examples/fapi1-advanced.diff) + - FAPI 2.0 Security Profile (Private Key JWT, PAR, DPoP) - [source](examples/fapi2.ts) | [diff](examples/fapi2.diff) + ## Supported Runtimes diff --git a/examples/.update-diffs.sh b/examples/.update-diffs.sh index 166851de..700be877 100755 --- a/examples/.update-diffs.sh +++ b/examples/.update-diffs.sh @@ -1,5 +1,11 @@ -git diff HEAD:examples/code.ts examples/dpop.ts > examples/dpop.diff -git diff HEAD:examples/code.ts examples/par.ts > examples/par.diff -git diff HEAD:examples/code.ts examples/private_key_jwt.ts > examples/private_key_jwt.diff -git diff HEAD:examples/code.ts examples/public.ts > examples/public.diff -git diff HEAD:examples/fapi2.ts examples/fapi2-message-signing.ts > examples/fapi2-message-signing.diff +git diff HEAD:examples/oauth.ts examples/oidc.ts > examples/oidc.diff +git diff HEAD:examples/oauth.ts examples/dpop.ts > examples/dpop.diff +git diff HEAD:examples/oauth.ts examples/par.ts > examples/par.diff +git diff HEAD:examples/oauth.ts examples/jar.ts > examples/jar.diff +git diff HEAD:examples/oauth.ts examples/jarm.ts > examples/jarm.diff +git diff HEAD:examples/oauth.ts examples/client_secret_post.ts > examples/client_secret_post.diff +git diff HEAD:examples/oauth.ts examples/private_key_jwt.ts > examples/private_key_jwt.diff +git diff HEAD:examples/oauth.ts examples/public.ts > examples/public.diff +git diff HEAD:examples/oauth.ts examples/fapi2.ts > examples/fapi2.diff +git diff HEAD:examples/oauth.ts examples/fapi1-advanced.ts > examples/fapi1-advanced.diff +git diff HEAD:examples/oauth.ts examples/refresh_token.ts > examples/refresh_token.diff diff --git a/examples/README.md b/examples/README.md index 91509a79..c4523981 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,12 +1,21 @@ A collection of examples for the most common use cases. -- Authorization Code Flow - OpenID Connect [source](code.ts), or plain OAuth 2 [source](oauth.ts) -- Public Client Authorization Code Flow - [source](public.ts) | [diff from code flow](public.diff) -- Private Key JWT Client Authentication - [source](private_key_jwt.ts) | [diff from code flow](private_key_jwt.diff) -- DPoP - [source](dpop.ts) | [diff from code flow](dpop.diff) -- Pushed Authorization Request (PAR) - [source](par.ts) | [diff from code flow](par.diff) -- Client Credentials Grant - [source](client_credentials.ts) -- Device Authorization Grant - [source](device_authorization_grant.ts) -- FAPI 1.0 Advanced (Private Key JWT, MTLS, JAR) - [source](fapi1-advanced.ts) -- FAPI 2.0 Security Profile (Private Key JWT, PAR, DPoP) - [source](fapi2.ts) -- FAPI 2.0 Message Signing (Private Key JWT, PAR, DPoP, JAR, JARM) - [source](fapi2-message-signing.ts) | [diff from FAPI 2.0 SP](fapi2-message-signing.diff) +- Authorization Code Flow (OAuth 2.0) - [source](oauth.ts) +- Authorization Code Flow (OpenID Connect) - [source](oidc.ts) | [diff](oidc.diff) +- Extensions + - DPoP - [source](dpop.ts) | [diff](dpop.diff) + - JWT Secured Authorization Request (JAR) - [source](jar.ts) | [diff](jar.diff) + - JWT Secured Authorization Response Mode (JARM) - [source](jarm.ts) | [diff](jarm.diff) + - Pushed Authorization Request (PAR) - [source](par.ts) | [diff](par.diff) +- Client Authentication + - Client Secret in HTTP Authorization Header - [source](oauth.ts) + - Client Secret in HTTP Body - [source](client_secret_post.ts) | [diff](client_secret_post.diff) + - Private Key JWT Client Authentication - [source](private_key_jwt.ts) | [diff](private_key_jwt.diff) + - Public Client - [source](public.ts) | [diff](public.diff) +- Other Grants + - Client Credentials Grant - [source](client_credentials.ts) + - Device Authorization Grant - [source](device_authorization_grant.ts) + - Refresh Token Grant - [source](refresh_token.ts) | [diff](refresh_token.diff) +- FAPI + - FAPI 1.0 Advanced (Private Key JWT, MTLS, JAR) - [source](fapi1-advanced.ts) | [diff](fapi1-advanced.diff) + - FAPI 2.0 Security Profile (Private Key JWT, PAR, DPoP) - [source](fapi2.ts) | [diff](fapi2.diff) diff --git a/examples/client_credentials.ts b/examples/client_credentials.ts index 1daf4814..e5c50a4b 100644 --- a/examples/client_credentials.ts +++ b/examples/client_credentials.ts @@ -3,13 +3,17 @@ import * as oauth from '../src/index.js' // replace with an import of oauth4weba // Prerequisites let issuer!: URL // Authorization server's Issuer Identifier URL +let algorithm!: + | 'oauth2' /* For .well-known/oauth-authorization-server discovery */ + | 'oidc' /* For .well-known/openid-configuration discovery */ + | undefined /* Defaults to 'oidc' */ let client_id!: string let client_secret!: string // End of prerequisites const as = await oauth - .discoveryRequest(issuer) + .discoveryRequest(issuer, { algorithm }) .then((response) => oauth.processDiscoveryResponse(issuer, response)) const client: oauth.Client = { @@ -18,24 +22,47 @@ const client: oauth.Client = { token_endpoint_auth_method: 'client_secret_basic', } -const parameters = new URLSearchParams() -parameters.set('scope', 'api:read api:write') -parameters.set('resource', 'urn:example:api') +// Client Credentials Grant Request & Response +let access_token: string +{ + const parameters = new URLSearchParams() + parameters.set('scope', 'api:read') -const response = await oauth.clientCredentialsGrantRequest(as, client, parameters) + const response = await oauth.clientCredentialsGrantRequest(as, client, parameters) -let challenges: oauth.WWWAuthenticateChallenge[] | undefined -if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { - for (const challenge of challenges) { - console.log('challenge', challenge) + let challenges: oauth.WWWAuthenticateChallenge[] | undefined + if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { + for (const challenge of challenges) { + console.error('WWW-Authenticate Challenge', challenge) + } + throw new Error() // Handle WWW-Authenticate Challenges as needed + } + + const result = await oauth.processClientCredentialsResponse(as, client, response) + if (oauth.isOAuth2Error(result)) { + console.error('Error Response', result) + throw new Error() // Handle OAuth 2.0 response body error } - throw new Error() // Handle www-authenticate challenges as needed -} -const result = await oauth.processClientCredentialsResponse(as, client, response) -if (oauth.isOAuth2Error(result)) { - console.log('error', result) - throw new Error() // Handle OAuth 2.0 response body error + console.log('Access Token Response', result) + ;({ access_token } = result) } -console.log('result', result) +// Protected Resource Request +{ + const response = await oauth.protectedResourceRequest( + access_token, + 'GET', + new URL('https://rs.example.com/api'), + ) + + let challenges: oauth.WWWAuthenticateChallenge[] | undefined + if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { + for (const challenge of challenges) { + console.error('WWW-Authenticate Challenge', challenge) + } + throw new Error() // Handle WWW-Authenticate Challenges as needed + } + + console.log('Protected Resource Response', await response.json()) +} diff --git a/examples/client_secret_post.diff b/examples/client_secret_post.diff new file mode 100644 index 00000000..509c059f --- /dev/null +++ b/examples/client_secret_post.diff @@ -0,0 +1,13 @@ +diff --git a/examples/oauth.ts b/examples/client_secret_post.ts +index cc6d632..df5aefd 100644 +--- a/examples/oauth.ts ++++ b/examples/client_secret_post.ts +@@ -24,7 +24,7 @@ const as = await oauth + const client: oauth.Client = { + client_id, + client_secret, +- token_endpoint_auth_method: 'client_secret_basic', ++ token_endpoint_auth_method: 'client_secret_post', + } + + const code_challenge_method = 'S256' diff --git a/examples/client_secret_post.ts b/examples/client_secret_post.ts new file mode 100644 index 00000000..df5aefd6 --- /dev/null +++ b/examples/client_secret_post.ts @@ -0,0 +1,117 @@ +import * as oauth from '../src/index.js' // replace with an import of oauth4webapi + +// Prerequisites + +let issuer!: URL // Authorization server's Issuer Identifier URL +let algorithm!: + | 'oauth2' /* For .well-known/oauth-authorization-server discovery */ + | 'oidc' /* For .well-known/openid-configuration discovery */ + | undefined /* Defaults to 'oidc' */ +let client_id!: string +let client_secret!: string +/** + * Value used in the authorization request as redirect_uri pre-registered at the Authorization + * Server. + */ +let redirect_uri!: string + +// End of prerequisites + +const as = await oauth + .discoveryRequest(issuer, { algorithm }) + .then((response) => oauth.processDiscoveryResponse(issuer, response)) + +const client: oauth.Client = { + client_id, + client_secret, + token_endpoint_auth_method: 'client_secret_post', +} + +const code_challenge_method = 'S256' +/** + * The following MUST be generated for every redirect to the authorization_endpoint. You must store + * the code_verifier and nonce in the end-user session such that it can be recovered as the user + * gets redirected from the authorization server back to your application. + */ +const code_verifier = oauth.generateRandomCodeVerifier() +const code_challenge = await oauth.calculatePKCECodeChallenge(code_verifier) +let state: string | undefined + +{ + // redirect user to as.authorization_endpoint + const authorizationUrl = new URL(as.authorization_endpoint!) + authorizationUrl.searchParams.set('client_id', client.client_id) + authorizationUrl.searchParams.set('redirect_uri', redirect_uri) + authorizationUrl.searchParams.set('response_type', 'code') + authorizationUrl.searchParams.set('scope', 'api:read') + authorizationUrl.searchParams.set('code_challenge', code_challenge) + authorizationUrl.searchParams.set('code_challenge_method', code_challenge_method) + + /** + * We cannot be sure the AS supports PKCE so we're going to use state too. Use of PKCE is + * backwards compatible even if the AS doesn't support it which is why we're using it regardless. + */ + if (as.code_challenge_methods_supported?.includes('S256') !== true) { + state = oauth.generateRandomState() + authorizationUrl.searchParams.set('state', state) + } + + // now redirect the user to authorizationUrl.href +} + +// one eternity later, the user lands back on the redirect_uri +// Authorization Code Grant Request & Response +let access_token: string +{ + // @ts-expect-error + const currentUrl: URL = getCurrentUrl() + const params = oauth.validateAuthResponse(as, client, currentUrl, state) + if (oauth.isOAuth2Error(params)) { + console.error('Error Response', params) + throw new Error() // Handle OAuth 2.0 redirect error + } + + const response = await oauth.authorizationCodeGrantRequest( + as, + client, + params, + redirect_uri, + code_verifier, + ) + + let challenges: oauth.WWWAuthenticateChallenge[] | undefined + if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { + for (const challenge of challenges) { + console.error('WWW-Authenticate Challenge', challenge) + } + throw new Error() // Handle WWW-Authenticate Challenges as needed + } + + const result = await oauth.processAuthorizationCodeOAuth2Response(as, client, response) + if (oauth.isOAuth2Error(result)) { + console.error('Error Response', result) + throw new Error() // Handle OAuth 2.0 response body error + } + + console.log('Access Token Response', result) + ;({ access_token } = result) +} + +// Protected Resource Request +{ + const response = await oauth.protectedResourceRequest( + access_token, + 'GET', + new URL('https://rs.example.com/api'), + ) + + let challenges: oauth.WWWAuthenticateChallenge[] | undefined + if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { + for (const challenge of challenges) { + console.error('WWW-Authenticate Challenge', challenge) + } + throw new Error() // Handle WWW-Authenticate Challenges as needed + } + + console.log('Protected Resource Response', await response.json()) +} diff --git a/examples/device_authorization_grant.ts b/examples/device_authorization_grant.ts index a2aebfc2..ea593dff 100644 --- a/examples/device_authorization_grant.ts +++ b/examples/device_authorization_grant.ts @@ -3,12 +3,16 @@ import * as oauth from '../src/index.js' // replace with an import of oauth4weba // Prerequisites let issuer!: URL // Authorization server's Issuer Identifier URL +let algorithm!: + | 'oauth2' /* For .well-known/oauth-authorization-server discovery */ + | 'oidc' /* For .well-known/openid-configuration discovery */ + | undefined /* Defaults to 'oidc' */ let client_id!: string // End of prerequisites const as = await oauth - .discoveryRequest(issuer) + .discoveryRequest(issuer, { algorithm }) .then((response) => oauth.processDiscoveryResponse(issuer, response)) const client: oauth.Client = { @@ -22,75 +26,100 @@ let verification_uri: string let user_code: string let verification_uri_complete: string | undefined +// Device Authorization Request & Response { const parameters = new URLSearchParams() - parameters.set('scope', 'openid email') + parameters.set('scope', 'api:read') const response = await oauth.deviceAuthorizationRequest(as, client, parameters) let challenges: oauth.WWWAuthenticateChallenge[] | undefined if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { for (const challenge of challenges) { - console.log('challenge', challenge) + console.error('WWW-Authenticate Challenge', challenge) } - throw new Error() // Handle www-authenticate challenges as needed + throw new Error() // Handle WWW-Authenticate Challenges as needed } const result = await oauth.processDeviceAuthorizationResponse(as, client, response) if (oauth.isOAuth2Error(result)) { - console.log('error', result) + console.error('Error Response', result) throw new Error() // Handle OAuth 2.0 response body error } + console.log('Device Authorization Response', result) ;({ device_code, verification_uri, verification_uri_complete, user_code, interval = 5 } = result) } -// user gets shown the verification_uri and user_code, or scans a qr code formed from verification_uri_complete as input -// user starts authenticating on his other device -console.log({ verification_uri, verification_uri_complete, user_code }) +/** + * User gets shown the verification_uri and user_code, or scans a qr code formed from + * verification_uri_complete as input. + * + * User starts authenticating on his other device. + */ +console.table({ verification_uri, verification_uri_complete, user_code }) -function wait() { - return new Promise((resolve) => { - setTimeout(resolve, interval * 1000) - }) -} - -let success: oauth.TokenEndpointResponse | undefined = undefined +// Device Authorization Grant Request & Response +let access_token: string +{ + let success: oauth.TokenEndpointResponse | undefined = undefined + function wait() { + return new Promise((resolve) => { + setTimeout(resolve, interval * 1000) + }) + } -while (success === undefined) { - await wait() - const response = await oauth.deviceCodeGrantRequest(as, client, device_code) - let challenges: oauth.WWWAuthenticateChallenge[] | undefined - if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { - for (const challenge of challenges) { - console.log('challenge', challenge) + while (success === undefined) { + await wait() + const response = await oauth.deviceCodeGrantRequest(as, client, device_code) + let challenges: oauth.WWWAuthenticateChallenge[] | undefined + if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { + for (const challenge of challenges) { + console.error('WWW-Authenticate Challenge', challenge) + } + throw new Error() // Handle WWW-Authenticate Challenges as needed } - throw new Error() // Handle www-authenticate challenges as needed - } - const result = await oauth.processDeviceCodeResponse(as, client, response) - if (oauth.isOAuth2Error(result)) { - console.log('error', result) - throw new Error() // Handle OAuth 2.0 response body error - } + const result = await oauth.processDeviceCodeResponse(as, client, response) + if (oauth.isOAuth2Error(result)) { + console.error('Error Response', result) + throw new Error() // Handle OAuth 2.0 response body error + } - if (oauth.isOAuth2Error(result)) { - // response is oauth style error object - switch (result.error) { - case 'slow_down': - interval += 5 - case 'authorization_pending': - continue - default: - console.log('error', result) - throw new Error() // Handle OAuth 2.0 response body error + if (oauth.isOAuth2Error(result)) { + // response is oauth style error object + switch (result.error) { + case 'slow_down': + interval += 5 + case 'authorization_pending': + continue + default: + console.error('Error Response', result) + throw new Error() // Handle OAuth 2.0 response body error + } + } else { + success = result } - } else { - success = result } + + ;({ access_token } = success) + console.log('Access Token Response', success) } -console.log('result', success) +// Protected Resource Request +{ + const response = await oauth.protectedResourceRequest( + access_token, + 'GET', + new URL('https://rs.example.com/api'), + ) + + let challenges: oauth.WWWAuthenticateChallenge[] | undefined + if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { + for (const challenge of challenges) { + console.error('WWW-Authenticate Challenge', challenge) + } + throw new Error() // Handle WWW-Authenticate Challenges as needed + } -if (success.id_token) { - console.log('ID Token Claims', oauth.getValidatedIdTokenClaims(success)) + console.log('Protected Resource Response', await response.json()) } diff --git a/examples/dpop.diff b/examples/dpop.diff index 1bf53f6e..046b13ca 100644 --- a/examples/dpop.diff +++ b/examples/dpop.diff @@ -1,8 +1,8 @@ -diff --git a/examples/code.ts b/examples/dpop.ts -index a7303f1..7c6f907 100644 ---- a/examples/code.ts +diff --git a/examples/oauth.ts b/examples/dpop.ts +index cc6d632..c3266a7 100644 +--- a/examples/oauth.ts +++ b/examples/dpop.ts -@@ -10,6 +10,12 @@ let client_secret!: string +@@ -14,6 +14,12 @@ let client_secret!: string * Server. */ let redirect_uri!: string @@ -15,7 +15,7 @@ index a7303f1..7c6f907 100644 // End of prerequisites -@@ -67,13 +73,10 @@ let access_token: string +@@ -71,13 +77,10 @@ let access_token: string throw new Error() // Handle OAuth 2.0 redirect error } @@ -33,22 +33,22 @@ index a7303f1..7c6f907 100644 let challenges: oauth.WWWAuthenticateChallenge[] | undefined if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { -@@ -83,10 +86,22 @@ let access_token: string - throw new Error() // Handle www-authenticate challenges as needed +@@ -87,10 +90,22 @@ let access_token: string + throw new Error() // Handle WWW-Authenticate Challenges as needed } -- const result = await oauth.processAuthorizationCodeOpenIDResponse(as, client, response, nonce) -+ const processAuthorizationCodeOpenIDResponse = () => -+ oauth.processAuthorizationCodeOpenIDResponse(as, client, response, nonce) +- const result = await oauth.processAuthorizationCodeOAuth2Response(as, client, response) ++ const processAuthorizationCodeOAuth2Response = () => ++ oauth.processAuthorizationCodeOAuth2Response(as, client, response) + -+ let result = await processAuthorizationCodeOpenIDResponse() ++ let result = await processAuthorizationCodeOAuth2Response() if (oauth.isOAuth2Error(result)) { - console.log('error', result) + console.error('Error Response', result) - throw new Error() // Handle OAuth 2.0 response body error + if (result.error === 'use_dpop_nonce') { + // the AS-signalled nonce is now cached, retrying + response = await authorizationCodeGrantRequest() -+ result = await processAuthorizationCodeOpenIDResponse() ++ result = await processAuthorizationCodeOAuth2Response() + if (oauth.isOAuth2Error(result)) { + throw new Error() // Handle OAuth 2.0 response body error + } @@ -57,19 +57,31 @@ index a7303f1..7c6f907 100644 + } } - console.log('result', result) -@@ -98,14 +113,25 @@ let access_token: string + console.log('Access Token Response', result) +@@ -99,18 +114,33 @@ let access_token: string - // fetch userinfo response + // Protected Resource Request { -- const response = await oauth.userInfoRequest(as, client, access_token) -+ const userInfoRequest = () => oauth.userInfoRequest(as, client, access_token, { DPoP }) -+ let response = await userInfoRequest() +- const response = await oauth.protectedResourceRequest( +- access_token, +- 'GET', +- new URL('https://rs.example.com/api'), +- ) ++ const protectedResourceRequest = () => ++ oauth.protectedResourceRequest( ++ access_token, ++ 'GET', ++ new URL('https://rs.example.com/api'), ++ undefined, ++ undefined, ++ { DPoP }, ++ ) ++ let response = await protectedResourceRequest() let challenges: oauth.WWWAuthenticateChallenge[] | undefined if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { - for (const challenge of challenges) { -- console.log('challenge', challenge) +- console.error('WWW-Authenticate Challenge', challenge) + const { 0: challenge, length } = challenges + if ( + length === 1 && @@ -77,14 +89,14 @@ index a7303f1..7c6f907 100644 + challenge.parameters.error === 'use_dpop_nonce' + ) { + // the AS-signalled nonce is now cached, retrying -+ response = await userInfoRequest() ++ response = await protectedResourceRequest() + } else { + for (const challenge of challenges) { -+ console.log('challenge', challenge) ++ console.error('WWW-Authenticate Challenge', challenge) + } -+ throw new Error() // Handle www-authenticate challenges as needed ++ throw new Error() // Handle WWW-Authenticate Challenges as needed } -- throw new Error() // Handle www-authenticate challenges as needed +- throw new Error() // Handle WWW-Authenticate Challenges as needed } - const result = await oauth.processUserInfoResponse(as, client, sub, response) + console.log('Protected Resource Response', await response.json()) diff --git a/examples/dpop.ts b/examples/dpop.ts index 7c6f9075..c3266a74 100644 --- a/examples/dpop.ts +++ b/examples/dpop.ts @@ -3,6 +3,10 @@ import * as oauth from '../src/index.js' // replace with an import of oauth4weba // Prerequisites let issuer!: URL // Authorization server's Issuer Identifier URL +let algorithm!: + | 'oauth2' /* For .well-known/oauth-authorization-server discovery */ + | 'oidc' /* For .well-known/openid-configuration discovery */ + | undefined /* Defaults to 'oidc' */ let client_id!: string let client_secret!: string /** @@ -20,7 +24,7 @@ let DPoP!: CryptoKeyPair // End of prerequisites const as = await oauth - .discoveryRequest(issuer) + .discoveryRequest(issuer, { algorithm }) .then((response) => oauth.processDiscoveryResponse(issuer, response)) const client: oauth.Client = { @@ -37,7 +41,7 @@ const code_challenge_method = 'S256' */ const code_verifier = oauth.generateRandomCodeVerifier() const code_challenge = await oauth.calculatePKCECodeChallenge(code_verifier) -let nonce: string | undefined +let state: string | undefined { // redirect user to as.authorization_endpoint @@ -45,31 +49,31 @@ let nonce: string | undefined authorizationUrl.searchParams.set('client_id', client.client_id) authorizationUrl.searchParams.set('redirect_uri', redirect_uri) authorizationUrl.searchParams.set('response_type', 'code') - authorizationUrl.searchParams.set('scope', 'openid email') + authorizationUrl.searchParams.set('scope', 'api:read') authorizationUrl.searchParams.set('code_challenge', code_challenge) authorizationUrl.searchParams.set('code_challenge_method', code_challenge_method) /** - * We cannot be sure the AS supports PKCE so we're going to use nonce too. Use of PKCE is + * We cannot be sure the AS supports PKCE so we're going to use state too. Use of PKCE is * backwards compatible even if the AS doesn't support it which is why we're using it regardless. */ if (as.code_challenge_methods_supported?.includes('S256') !== true) { - nonce = oauth.generateRandomNonce() - authorizationUrl.searchParams.set('nonce', nonce) + state = oauth.generateRandomState() + authorizationUrl.searchParams.set('state', state) } // now redirect the user to authorizationUrl.href } // one eternity later, the user lands back on the redirect_uri -let sub: string +// Authorization Code Grant Request & Response let access_token: string { // @ts-expect-error const currentUrl: URL = getCurrentUrl() - const params = oauth.validateAuthResponse(as, client, currentUrl) + const params = oauth.validateAuthResponse(as, client, currentUrl, state) if (oauth.isOAuth2Error(params)) { - console.log('error', params) + console.error('Error Response', params) throw new Error() // Handle OAuth 2.0 redirect error } @@ -81,21 +85,21 @@ let access_token: string let challenges: oauth.WWWAuthenticateChallenge[] | undefined if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { for (const challenge of challenges) { - console.log('challenge', challenge) + console.error('WWW-Authenticate Challenge', challenge) } - throw new Error() // Handle www-authenticate challenges as needed + throw new Error() // Handle WWW-Authenticate Challenges as needed } - const processAuthorizationCodeOpenIDResponse = () => - oauth.processAuthorizationCodeOpenIDResponse(as, client, response, nonce) + const processAuthorizationCodeOAuth2Response = () => + oauth.processAuthorizationCodeOAuth2Response(as, client, response) - let result = await processAuthorizationCodeOpenIDResponse() + let result = await processAuthorizationCodeOAuth2Response() if (oauth.isOAuth2Error(result)) { - console.log('error', result) + console.error('Error Response', result) if (result.error === 'use_dpop_nonce') { // the AS-signalled nonce is now cached, retrying response = await authorizationCodeGrantRequest() - result = await processAuthorizationCodeOpenIDResponse() + result = await processAuthorizationCodeOAuth2Response() if (oauth.isOAuth2Error(result)) { throw new Error() // Handle OAuth 2.0 response body error } @@ -104,17 +108,22 @@ let access_token: string } } - console.log('result', result) + console.log('Access Token Response', result) ;({ access_token } = result) - const claims = oauth.getValidatedIdTokenClaims(result) - console.log('ID Token Claims', claims) - ;({ sub } = claims) } -// fetch userinfo response +// Protected Resource Request { - const userInfoRequest = () => oauth.userInfoRequest(as, client, access_token, { DPoP }) - let response = await userInfoRequest() + const protectedResourceRequest = () => + oauth.protectedResourceRequest( + access_token, + 'GET', + new URL('https://rs.example.com/api'), + undefined, + undefined, + { DPoP }, + ) + let response = await protectedResourceRequest() let challenges: oauth.WWWAuthenticateChallenge[] | undefined if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { @@ -125,15 +134,14 @@ let access_token: string challenge.parameters.error === 'use_dpop_nonce' ) { // the AS-signalled nonce is now cached, retrying - response = await userInfoRequest() + response = await protectedResourceRequest() } else { for (const challenge of challenges) { - console.log('challenge', challenge) + console.error('WWW-Authenticate Challenge', challenge) } - throw new Error() // Handle www-authenticate challenges as needed + throw new Error() // Handle WWW-Authenticate Challenges as needed } } - const result = await oauth.processUserInfoResponse(as, client, sub, response) - console.log('result', result) + console.log('Protected Resource Response', await response.json()) } diff --git a/examples/fapi1-advanced.diff b/examples/fapi1-advanced.diff new file mode 100644 index 00000000..aeb2d09a --- /dev/null +++ b/examples/fapi1-advanced.diff @@ -0,0 +1,172 @@ +diff --git a/examples/oauth.ts b/examples/fapi1-advanced.ts +index cc6d632..ab605a2 100644 +--- a/examples/oauth.ts ++++ b/examples/fapi1-advanced.ts +@@ -1,3 +1,4 @@ ++import * as undici from 'undici' + import * as oauth from '../src/index.js' // replace with an import of oauth4webapi + + // Prerequisites +@@ -8,12 +9,30 @@ let algorithm!: + | 'oidc' /* For .well-known/openid-configuration discovery */ + | undefined /* Defaults to 'oidc' */ + let client_id!: string +-let client_secret!: string + /** + * Value used in the authorization request as redirect_uri pre-registered at the Authorization + * Server. + */ + let redirect_uri!: string ++/** ++ * A key corresponding to the mtlsClientCertificate. ++ */ ++let mtlsClientKey!: string ++/** ++ * A certificate the client has pre-registered at the Authorization Server for use with Mutual-TLS ++ * client authentication method. ++ */ ++let mtlsClientCertificate!: string ++/** ++ * A key that is pre-registered at the Authorization Server that the client is supposed to sign its ++ * Request Objects with. ++ */ ++let jarPrivateKey!: CryptoKey ++/** ++ * A key that the client has pre-registered at the Authorization Server for use with Private Key JWT ++ * client authentication method. ++ */ ++let clientPrivateKey!: CryptoKey + + // End of prerequisites + +@@ -23,8 +42,7 @@ const as = await oauth + + const client: oauth.Client = { + client_id, +- client_secret, +- token_endpoint_auth_method: 'client_secret_basic', ++ token_endpoint_auth_method: 'private_key_jwt', + } + + const code_challenge_method = 'S256' +@@ -33,39 +51,46 @@ const code_challenge_method = 'S256' + * the code_verifier and nonce in the end-user session such that it can be recovered as the user + * gets redirected from the authorization server back to your application. + */ ++const nonce = oauth.generateRandomNonce() + const code_verifier = oauth.generateRandomCodeVerifier() + const code_challenge = await oauth.calculatePKCECodeChallenge(code_verifier) +-let state: string | undefined ++ ++let request: string ++{ ++ const params = new URLSearchParams() ++ params.set('client_id', client.client_id) ++ params.set('code_challenge', code_challenge) ++ params.set('code_challenge_method', code_challenge_method) ++ params.set('redirect_uri', redirect_uri) ++ params.set('response_type', 'code id_token') ++ params.set('scope', 'openid api:read') ++ params.set('nonce', nonce) ++ ++ request = await oauth.issueRequestObject(as, client, params, jarPrivateKey) ++} + + { + // redirect user to as.authorization_endpoint + const authorizationUrl = new URL(as.authorization_endpoint!) + authorizationUrl.searchParams.set('client_id', client.client_id) +- authorizationUrl.searchParams.set('redirect_uri', redirect_uri) +- authorizationUrl.searchParams.set('response_type', 'code') +- authorizationUrl.searchParams.set('scope', 'api:read') +- authorizationUrl.searchParams.set('code_challenge', code_challenge) +- authorizationUrl.searchParams.set('code_challenge_method', code_challenge_method) +- +- /** +- * We cannot be sure the AS supports PKCE so we're going to use state too. Use of PKCE is +- * backwards compatible even if the AS doesn't support it which is why we're using it regardless. +- */ +- if (as.code_challenge_methods_supported?.includes('S256') !== true) { +- state = oauth.generateRandomState() +- authorizationUrl.searchParams.set('state', state) +- } ++ authorizationUrl.searchParams.set('request', request) + + // now redirect the user to authorizationUrl.href + } + + // one eternity later, the user lands back on the redirect_uri ++// Detached Signature ID Token Validation + // Authorization Code Grant Request & Response + let access_token: string + { + // @ts-expect-error +- const currentUrl: URL = getCurrentUrl() +- const params = oauth.validateAuthResponse(as, client, currentUrl, state) ++ const authorizationResponse: URLSearchParams | URL = getAuthorizationResponseOrURLWithFragment() ++ const params = await oauth.validateDetachedSignatureResponse( ++ as, ++ client, ++ authorizationResponse, ++ nonce, ++ ) + if (oauth.isOAuth2Error(params)) { + console.error('Error Response', params) + throw new Error() // Handle OAuth 2.0 redirect error +@@ -77,6 +102,23 @@ let access_token: string + params, + redirect_uri, + code_verifier, ++ { ++ clientPrivateKey, ++ [oauth.useMtlsAlias]: true, ++ // @ts-expect-error ++ [oauth.customFetch]: (...args) => { ++ // @ts-expect-error ++ return undici.fetch(args[0], { ++ ...args[1], ++ dispatcher: new undici.Agent({ ++ connect: { ++ key: mtlsClientKey, ++ cert: mtlsClientCertificate, ++ }, ++ }), ++ }) ++ }, ++ }, + ) + + let challenges: oauth.WWWAuthenticateChallenge[] | undefined +@@ -87,7 +129,7 @@ let access_token: string + throw new Error() // Handle WWW-Authenticate Challenges as needed + } + +- const result = await oauth.processAuthorizationCodeOAuth2Response(as, client, response) ++ const result = await oauth.processAuthorizationCodeOpenIDResponse(as, client, response) + if (oauth.isOAuth2Error(result)) { + console.error('Error Response', result) + throw new Error() // Handle OAuth 2.0 response body error +@@ -103,6 +145,23 @@ let access_token: string + access_token, + 'GET', + new URL('https://rs.example.com/api'), ++ undefined, ++ undefined, ++ { ++ // @ts-expect-error ++ [oauth.customFetch]: (...args) => { ++ // @ts-expect-error ++ return undici.fetch(args[0], { ++ ...args[1], ++ dispatcher: new undici.Agent({ ++ connect: { ++ key: mtlsClientKey, ++ cert: mtlsClientCertificate, ++ }, ++ }), ++ }) ++ }, ++ }, + ) + + let challenges: oauth.WWWAuthenticateChallenge[] | undefined diff --git a/examples/fapi1-advanced.ts b/examples/fapi1-advanced.ts index aa0e222e..ab605a2f 100644 --- a/examples/fapi1-advanced.ts +++ b/examples/fapi1-advanced.ts @@ -4,6 +4,10 @@ import * as oauth from '../src/index.js' // replace with an import of oauth4weba // Prerequisites let issuer!: URL // Authorization server's Issuer Identifier URL +let algorithm!: + | 'oauth2' /* For .well-known/oauth-authorization-server discovery */ + | 'oidc' /* For .well-known/openid-configuration discovery */ + | undefined /* Defaults to 'oidc' */ let client_id!: string /** * Value used in the authorization request as redirect_uri pre-registered at the Authorization @@ -33,7 +37,7 @@ let clientPrivateKey!: CryptoKey // End of prerequisites const as = await oauth - .discoveryRequest(issuer) + .discoveryRequest(issuer, { algorithm }) .then((response) => oauth.processDiscoveryResponse(issuer, response)) const client: oauth.Client = { @@ -41,10 +45,15 @@ const client: oauth.Client = { token_endpoint_auth_method: 'private_key_jwt', } +const code_challenge_method = 'S256' +/** + * The following MUST be generated for every redirect to the authorization_endpoint. You must store + * the code_verifier and nonce in the end-user session such that it can be recovered as the user + * gets redirected from the authorization server back to your application. + */ const nonce = oauth.generateRandomNonce() const code_verifier = oauth.generateRandomCodeVerifier() const code_challenge = await oauth.calculatePKCECodeChallenge(code_verifier) -const code_challenge_method = 'S256' let request: string { @@ -54,7 +63,7 @@ let request: string params.set('code_challenge_method', code_challenge_method) params.set('redirect_uri', redirect_uri) params.set('response_type', 'code id_token') - params.set('scope', 'openid email') + params.set('scope', 'openid api:read') params.set('nonce', nonce) request = await oauth.issueRequestObject(as, client, params, jarPrivateKey) @@ -70,6 +79,9 @@ let request: string } // one eternity later, the user lands back on the redirect_uri +// Detached Signature ID Token Validation +// Authorization Code Grant Request & Response +let access_token: string { // @ts-expect-error const authorizationResponse: URLSearchParams | URL = getAuthorizationResponseOrURLWithFragment() @@ -80,7 +92,7 @@ let request: string nonce, ) if (oauth.isOAuth2Error(params)) { - console.log('error', params) + console.error('Error Response', params) throw new Error() // Handle OAuth 2.0 redirect error } @@ -112,18 +124,53 @@ let request: string let challenges: oauth.WWWAuthenticateChallenge[] | undefined if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { for (const challenge of challenges) { - console.log('challenge', challenge) + console.error('WWW-Authenticate Challenge', challenge) } - throw new Error() // Handle www-authenticate challenges as needed + throw new Error() // Handle WWW-Authenticate Challenges as needed } const result = await oauth.processAuthorizationCodeOpenIDResponse(as, client, response) if (oauth.isOAuth2Error(result)) { - console.log('error', result) + console.error('Error Response', result) throw new Error() // Handle OAuth 2.0 response body error } - console.log('result', result) - const claims = oauth.getValidatedIdTokenClaims(result) - console.log('ID Token Claims', claims) + console.log('Access Token Response', result) + ;({ access_token } = result) +} + +// Protected Resource Request +{ + const response = await oauth.protectedResourceRequest( + access_token, + 'GET', + new URL('https://rs.example.com/api'), + undefined, + undefined, + { + // @ts-expect-error + [oauth.customFetch]: (...args) => { + // @ts-expect-error + return undici.fetch(args[0], { + ...args[1], + dispatcher: new undici.Agent({ + connect: { + key: mtlsClientKey, + cert: mtlsClientCertificate, + }, + }), + }) + }, + }, + ) + + let challenges: oauth.WWWAuthenticateChallenge[] | undefined + if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { + for (const challenge of challenges) { + console.error('WWW-Authenticate Challenge', challenge) + } + throw new Error() // Handle WWW-Authenticate Challenges as needed + } + + console.log('Protected Resource Response', await response.json()) } diff --git a/examples/fapi2-message-signing.diff b/examples/fapi2-message-signing.diff deleted file mode 100644 index b0f37249..00000000 --- a/examples/fapi2-message-signing.diff +++ /dev/null @@ -1,53 +0,0 @@ -diff --git a/examples/fapi2.ts b/examples/fapi2-message-signing.ts -index 5fd72ab..0943c07 100644 ---- a/examples/fapi2.ts -+++ b/examples/fapi2-message-signing.ts -@@ -20,6 +20,11 @@ let DPoP!: CryptoKeyPair - * client authentication method. - */ - let clientPrivateKey!: CryptoKey -+/** -+ * A key that is pre-registered at the Authorization Server that the client is supposed to sign its -+ * Request Objects with. -+ */ -+let jarPrivateKey!: CryptoKey - - // End of prerequisites - -@@ -36,7 +41,7 @@ const code_verifier = oauth.generateRandomCodeVerifier() - const code_challenge = await oauth.calculatePKCECodeChallenge(code_verifier) - const code_challenge_method = 'S256' - --let request_uri: string -+let request: string - { - const params = new URLSearchParams() - params.set('client_id', client.client_id) -@@ -44,8 +49,18 @@ let request_uri: string - params.set('code_challenge_method', code_challenge_method) - params.set('redirect_uri', redirect_uri) - params.set('response_type', 'code') -+ params.set('response_mode', 'jwt') - params.set('scope', 'openid email') - -+ request = await oauth.issueRequestObject(as, client, params, jarPrivateKey) -+} -+ -+let request_uri: string -+{ -+ const params = new URLSearchParams() -+ params.set('client_id', client.client_id) -+ params.set('request', request) -+ - const response = await oauth.pushedAuthorizationRequest(as, client, params, { - DPoP, - clientPrivateKey, -@@ -84,7 +99,7 @@ let request_uri: string - { - // @ts-expect-error - const currentUrl: URL = getCurrentUrl() -- const params = oauth.validateAuthResponse(as, client, currentUrl) -+ const params = await oauth.validateJwtAuthResponse(as, client, currentUrl) - if (oauth.isOAuth2Error(params)) { - console.log('error', params) - throw new Error() // Handle OAuth 2.0 redirect error diff --git a/examples/fapi2.diff b/examples/fapi2.diff new file mode 100644 index 00000000..a657e3e4 --- /dev/null +++ b/examples/fapi2.diff @@ -0,0 +1,208 @@ +diff --git a/examples/oauth.ts b/examples/fapi2.ts +index cc6d632..80ec0f4 100644 +--- a/examples/oauth.ts ++++ b/examples/fapi2.ts +@@ -8,12 +8,22 @@ let algorithm!: + | 'oidc' /* For .well-known/openid-configuration discovery */ + | undefined /* Defaults to 'oidc' */ + let client_id!: string +-let client_secret!: string + /** + * Value used in the authorization request as redirect_uri pre-registered at the Authorization + * Server. + */ + let redirect_uri!: string ++/** ++ * In order to take full advantage of DPoP you shall generate a random private key for every ++ * session. In the browser environment you shall use IndexedDB to persist the generated ++ * CryptoKeyPair. ++ */ ++let DPoP!: CryptoKeyPair ++/** ++ * A key that the client has pre-registered at the Authorization Server for use with Private Key JWT ++ * client authentication method. ++ */ ++let clientPrivateKey!: CryptoKey + + // End of prerequisites + +@@ -23,38 +33,68 @@ const as = await oauth + + const client: oauth.Client = { + client_id, +- client_secret, +- token_endpoint_auth_method: 'client_secret_basic', ++ token_endpoint_auth_method: 'private_key_jwt', + } + + const code_challenge_method = 'S256' + /** + * The following MUST be generated for every redirect to the authorization_endpoint. You must store +- * the code_verifier and nonce in the end-user session such that it can be recovered as the user +- * gets redirected from the authorization server back to your application. ++ * the code_verifier in the end-user session such that it can be recovered as the user gets ++ * redirected from the authorization server back to your application. + */ + const code_verifier = oauth.generateRandomCodeVerifier() + const code_challenge = await oauth.calculatePKCECodeChallenge(code_verifier) +-let state: string | undefined ++ ++// Pushed Authorization Request & Response (PAR) ++let request_uri: string ++{ ++ const params = new URLSearchParams() ++ params.set('client_id', client.client_id) ++ params.set('code_challenge', code_challenge) ++ params.set('code_challenge_method', code_challenge_method) ++ params.set('redirect_uri', redirect_uri) ++ params.set('response_type', 'code') ++ params.set('scope', 'api:read') ++ ++ const pushedAuthorizationRequest = () => ++ oauth.pushedAuthorizationRequest(as, client, params, { ++ DPoP, ++ clientPrivateKey, ++ }) ++ let response = await pushedAuthorizationRequest() ++ let challenges: oauth.WWWAuthenticateChallenge[] | undefined ++ if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { ++ for (const challenge of challenges) { ++ console.error('WWW-Authenticate Challenge', challenge) ++ } ++ throw new Error() // Handle WWW-Authenticate Challenges as needed ++ } ++ ++ const processPushedAuthorizationResponse = () => ++ oauth.processPushedAuthorizationResponse(as, client, response) ++ let result = await processPushedAuthorizationResponse() ++ if (oauth.isOAuth2Error(result)) { ++ console.error('Error Response', result) ++ if (result.error === 'use_dpop_nonce') { ++ // the AS-signalled nonce is now cached, retrying ++ response = await pushedAuthorizationRequest() ++ result = await processPushedAuthorizationResponse() ++ if (oauth.isOAuth2Error(result)) { ++ throw new Error() // Handle OAuth 2.0 response body error ++ } ++ } else { ++ throw new Error() // Handle OAuth 2.0 response body error ++ } ++ } ++ ++ ;({ request_uri } = result) ++} + + { + // redirect user to as.authorization_endpoint + const authorizationUrl = new URL(as.authorization_endpoint!) + authorizationUrl.searchParams.set('client_id', client.client_id) +- authorizationUrl.searchParams.set('redirect_uri', redirect_uri) +- authorizationUrl.searchParams.set('response_type', 'code') +- authorizationUrl.searchParams.set('scope', 'api:read') +- authorizationUrl.searchParams.set('code_challenge', code_challenge) +- authorizationUrl.searchParams.set('code_challenge_method', code_challenge_method) +- +- /** +- * We cannot be sure the AS supports PKCE so we're going to use state too. Use of PKCE is +- * backwards compatible even if the AS doesn't support it which is why we're using it regardless. +- */ +- if (as.code_challenge_methods_supported?.includes('S256') !== true) { +- state = oauth.generateRandomState() +- authorizationUrl.searchParams.set('state', state) +- } ++ authorizationUrl.searchParams.set('request_uri', request_uri) + + // now redirect the user to authorizationUrl.href + } +@@ -65,19 +105,16 @@ let access_token: string + { + // @ts-expect-error + const currentUrl: URL = getCurrentUrl() +- const params = oauth.validateAuthResponse(as, client, currentUrl, state) ++ const params = oauth.validateAuthResponse(as, client, currentUrl) + if (oauth.isOAuth2Error(params)) { + console.error('Error Response', params) + throw new Error() // Handle OAuth 2.0 redirect error + } + +- const response = await oauth.authorizationCodeGrantRequest( +- as, +- client, +- params, +- redirect_uri, +- code_verifier, +- ) ++ const authorizationCodeGrantRequest = () => ++ oauth.authorizationCodeGrantRequest(as, client, params, redirect_uri, code_verifier, { DPoP }) ++ ++ let response = await authorizationCodeGrantRequest() + + let challenges: oauth.WWWAuthenticateChallenge[] | undefined + if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { +@@ -87,10 +124,22 @@ let access_token: string + throw new Error() // Handle WWW-Authenticate Challenges as needed + } + +- const result = await oauth.processAuthorizationCodeOAuth2Response(as, client, response) ++ const processAuthorizationCodeOAuth2Response = () => ++ oauth.processAuthorizationCodeOAuth2Response(as, client, response) ++ ++ let result = await processAuthorizationCodeOAuth2Response() + if (oauth.isOAuth2Error(result)) { + console.error('Error Response', result) +- throw new Error() // Handle OAuth 2.0 response body error ++ if (result.error === 'use_dpop_nonce') { ++ // the AS-signalled nonce is now cached, retrying ++ response = await authorizationCodeGrantRequest() ++ result = await processAuthorizationCodeOAuth2Response() ++ if (oauth.isOAuth2Error(result)) { ++ throw new Error() // Handle OAuth 2.0 response body error ++ } ++ } else { ++ throw new Error() // Handle OAuth 2.0 response body error ++ } + } + + console.log('Access Token Response', result) +@@ -99,18 +148,33 @@ let access_token: string + + // Protected Resource Request + { +- const response = await oauth.protectedResourceRequest( +- access_token, +- 'GET', +- new URL('https://rs.example.com/api'), +- ) ++ const protectedResourceRequest = () => ++ oauth.protectedResourceRequest( ++ access_token, ++ 'GET', ++ new URL('https://rs.example.com/api'), ++ undefined, ++ undefined, ++ { DPoP }, ++ ) ++ let response = await protectedResourceRequest() + + let challenges: oauth.WWWAuthenticateChallenge[] | undefined + if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { +- for (const challenge of challenges) { +- console.error('WWW-Authenticate Challenge', challenge) ++ const { 0: challenge, length } = challenges ++ if ( ++ length === 1 && ++ challenge.scheme === 'dpop' && ++ challenge.parameters.error === 'use_dpop_nonce' ++ ) { ++ // the AS-signalled nonce is now cached, retrying ++ response = await protectedResourceRequest() ++ } else { ++ for (const challenge of challenges) { ++ console.error('WWW-Authenticate Challenge', challenge) ++ } ++ throw new Error() // Handle WWW-Authenticate Challenges as needed + } +- throw new Error() // Handle WWW-Authenticate Challenges as needed + } + + console.log('Protected Resource Response', await response.json()) diff --git a/examples/fapi2.ts b/examples/fapi2.ts index 5fd72ab6..80ec0f41 100644 --- a/examples/fapi2.ts +++ b/examples/fapi2.ts @@ -3,6 +3,10 @@ import * as oauth from '../src/index.js' // replace with an import of oauth4weba // Prerequisites let issuer!: URL // Authorization server's Issuer Identifier URL +let algorithm!: + | 'oauth2' /* For .well-known/oauth-authorization-server discovery */ + | 'oidc' /* For .well-known/openid-configuration discovery */ + | undefined /* Defaults to 'oidc' */ let client_id!: string /** * Value used in the authorization request as redirect_uri pre-registered at the Authorization @@ -24,7 +28,7 @@ let clientPrivateKey!: CryptoKey // End of prerequisites const as = await oauth - .discoveryRequest(issuer) + .discoveryRequest(issuer, { algorithm }) .then((response) => oauth.processDiscoveryResponse(issuer, response)) const client: oauth.Client = { @@ -32,10 +36,16 @@ const client: oauth.Client = { token_endpoint_auth_method: 'private_key_jwt', } +const code_challenge_method = 'S256' +/** + * The following MUST be generated for every redirect to the authorization_endpoint. You must store + * the code_verifier in the end-user session such that it can be recovered as the user gets + * redirected from the authorization server back to your application. + */ const code_verifier = oauth.generateRandomCodeVerifier() const code_challenge = await oauth.calculatePKCECodeChallenge(code_verifier) -const code_challenge_method = 'S256' +// Pushed Authorization Request & Response (PAR) let request_uri: string { const params = new URLSearchParams() @@ -44,30 +54,39 @@ let request_uri: string params.set('code_challenge_method', code_challenge_method) params.set('redirect_uri', redirect_uri) params.set('response_type', 'code') - params.set('scope', 'openid email') + params.set('scope', 'api:read') - const response = await oauth.pushedAuthorizationRequest(as, client, params, { - DPoP, - clientPrivateKey, - }) + const pushedAuthorizationRequest = () => + oauth.pushedAuthorizationRequest(as, client, params, { + DPoP, + clientPrivateKey, + }) + let response = await pushedAuthorizationRequest() let challenges: oauth.WWWAuthenticateChallenge[] | undefined if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { for (const challenge of challenges) { - console.log('challenge', challenge) + console.error('WWW-Authenticate Challenge', challenge) } - throw new Error() // Handle www-authenticate challenges as needed + throw new Error() // Handle WWW-Authenticate Challenges as needed } - const result = await oauth.processPushedAuthorizationResponse(as, client, response) + const processPushedAuthorizationResponse = () => + oauth.processPushedAuthorizationResponse(as, client, response) + let result = await processPushedAuthorizationResponse() if (oauth.isOAuth2Error(result)) { - console.log('error', result) + console.error('Error Response', result) if (result.error === 'use_dpop_nonce') { - // the AS-signalled nonce is now cached, you should retry + // the AS-signalled nonce is now cached, retrying + response = await pushedAuthorizationRequest() + result = await processPushedAuthorizationResponse() + if (oauth.isOAuth2Error(result)) { + throw new Error() // Handle OAuth 2.0 response body error + } + } else { + throw new Error() // Handle OAuth 2.0 response body error } - throw new Error() // Handle OAuth 2.0 response body error } - console.log('result', result) ;({ request_uri } = result) } @@ -81,45 +100,82 @@ let request_uri: string } // one eternity later, the user lands back on the redirect_uri +// Authorization Code Grant Request & Response +let access_token: string { // @ts-expect-error const currentUrl: URL = getCurrentUrl() const params = oauth.validateAuthResponse(as, client, currentUrl) if (oauth.isOAuth2Error(params)) { - console.log('error', params) + console.error('Error Response', params) throw new Error() // Handle OAuth 2.0 redirect error } - const response = await oauth.authorizationCodeGrantRequest( - as, - client, - params, - redirect_uri, - code_verifier, - { - DPoP, - clientPrivateKey, - }, - ) + const authorizationCodeGrantRequest = () => + oauth.authorizationCodeGrantRequest(as, client, params, redirect_uri, code_verifier, { DPoP }) + + let response = await authorizationCodeGrantRequest() let challenges: oauth.WWWAuthenticateChallenge[] | undefined if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { for (const challenge of challenges) { - console.log('challenge', challenge) + console.error('WWW-Authenticate Challenge', challenge) } - throw new Error() // Handle www-authenticate challenges as needed + throw new Error() // Handle WWW-Authenticate Challenges as needed } - const result = await oauth.processAuthorizationCodeOpenIDResponse(as, client, response) + const processAuthorizationCodeOAuth2Response = () => + oauth.processAuthorizationCodeOAuth2Response(as, client, response) + + let result = await processAuthorizationCodeOAuth2Response() if (oauth.isOAuth2Error(result)) { - console.log('error', result) + console.error('Error Response', result) if (result.error === 'use_dpop_nonce') { - // the AS-signalled nonce is now cached, you should retry + // the AS-signalled nonce is now cached, retrying + response = await authorizationCodeGrantRequest() + result = await processAuthorizationCodeOAuth2Response() + if (oauth.isOAuth2Error(result)) { + throw new Error() // Handle OAuth 2.0 response body error + } + } else { + throw new Error() // Handle OAuth 2.0 response body error + } + } + + console.log('Access Token Response', result) + ;({ access_token } = result) +} + +// Protected Resource Request +{ + const protectedResourceRequest = () => + oauth.protectedResourceRequest( + access_token, + 'GET', + new URL('https://rs.example.com/api'), + undefined, + undefined, + { DPoP }, + ) + let response = await protectedResourceRequest() + + let challenges: oauth.WWWAuthenticateChallenge[] | undefined + if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { + const { 0: challenge, length } = challenges + if ( + length === 1 && + challenge.scheme === 'dpop' && + challenge.parameters.error === 'use_dpop_nonce' + ) { + // the AS-signalled nonce is now cached, retrying + response = await protectedResourceRequest() + } else { + for (const challenge of challenges) { + console.error('WWW-Authenticate Challenge', challenge) + } + throw new Error() // Handle WWW-Authenticate Challenges as needed } - throw new Error() // Handle OAuth 2.0 response body error } - console.log('result', result) - const claims = oauth.getValidatedIdTokenClaims(result) - console.log('ID Token Claims', claims) + console.log('Protected Resource Response', await response.json()) } diff --git a/examples/jar.diff b/examples/jar.diff new file mode 100644 index 00000000..e37ca5d0 --- /dev/null +++ b/examples/jar.diff @@ -0,0 +1,61 @@ +diff --git a/examples/oauth.ts b/examples/jar.ts +index cc6d632..118c367 100644 +--- a/examples/oauth.ts ++++ b/examples/jar.ts +@@ -14,6 +14,11 @@ let client_secret!: string + * Server. + */ + let redirect_uri!: string ++/** ++ * A key that is pre-registered at the Authorization Server that the client is supposed to sign its ++ * Request Objects with. ++ */ ++let jarPrivateKey!: CryptoKey + + // End of prerequisites + +@@ -37,15 +42,16 @@ const code_verifier = oauth.generateRandomCodeVerifier() + const code_challenge = await oauth.calculatePKCECodeChallenge(code_verifier) + let state: string | undefined + ++// Signed Request Object (JAR) ++let request: string + { +- // redirect user to as.authorization_endpoint +- const authorizationUrl = new URL(as.authorization_endpoint!) +- authorizationUrl.searchParams.set('client_id', client.client_id) +- authorizationUrl.searchParams.set('redirect_uri', redirect_uri) +- authorizationUrl.searchParams.set('response_type', 'code') +- authorizationUrl.searchParams.set('scope', 'api:read') +- authorizationUrl.searchParams.set('code_challenge', code_challenge) +- authorizationUrl.searchParams.set('code_challenge_method', code_challenge_method) ++ const params = new URLSearchParams() ++ params.set('client_id', client.client_id) ++ params.set('redirect_uri', redirect_uri) ++ params.set('response_type', 'code') ++ params.set('scope', 'api:read') ++ params.set('code_challenge', code_challenge) ++ params.set('code_challenge_method', code_challenge_method) + + /** + * We cannot be sure the AS supports PKCE so we're going to use state too. Use of PKCE is +@@ -53,9 +59,18 @@ let state: string | undefined + */ + if (as.code_challenge_methods_supported?.includes('S256') !== true) { + state = oauth.generateRandomState() +- authorizationUrl.searchParams.set('state', state) ++ params.set('state', state) + } + ++ request = await oauth.issueRequestObject(as, client, params, jarPrivateKey) ++} ++ ++{ ++ // redirect user to as.authorization_endpoint ++ const authorizationUrl = new URL(as.authorization_endpoint!) ++ authorizationUrl.searchParams.set('client_id', client.client_id) ++ authorizationUrl.searchParams.set('request', request) ++ + // now redirect the user to authorizationUrl.href + } + diff --git a/examples/fapi2-message-signing.ts b/examples/jar.ts similarity index 54% rename from examples/fapi2-message-signing.ts rename to examples/jar.ts index 0943c074..118c3672 100644 --- a/examples/fapi2-message-signing.ts +++ b/examples/jar.ts @@ -3,23 +3,17 @@ import * as oauth from '../src/index.js' // replace with an import of oauth4weba // Prerequisites let issuer!: URL // Authorization server's Issuer Identifier URL +let algorithm!: + | 'oauth2' /* For .well-known/oauth-authorization-server discovery */ + | 'oidc' /* For .well-known/openid-configuration discovery */ + | undefined /* Defaults to 'oidc' */ let client_id!: string +let client_secret!: string /** * Value used in the authorization request as redirect_uri pre-registered at the Authorization * Server. */ let redirect_uri!: string -/** - * In order to take full advantage of DPoP you shall generate a random private key for every - * session. In the browser environment you shall use IndexedDB to persist the generated - * CryptoKeyPair. - */ -let DPoP!: CryptoKeyPair -/** - * A key that the client has pre-registered at the Authorization Server for use with Private Key JWT - * client authentication method. - */ -let clientPrivateKey!: CryptoKey /** * A key that is pre-registered at the Authorization Server that the client is supposed to sign its * Request Objects with. @@ -29,79 +23,66 @@ let jarPrivateKey!: CryptoKey // End of prerequisites const as = await oauth - .discoveryRequest(issuer) + .discoveryRequest(issuer, { algorithm }) .then((response) => oauth.processDiscoveryResponse(issuer, response)) const client: oauth.Client = { client_id, - token_endpoint_auth_method: 'private_key_jwt', + client_secret, + token_endpoint_auth_method: 'client_secret_basic', } +const code_challenge_method = 'S256' +/** + * The following MUST be generated for every redirect to the authorization_endpoint. You must store + * the code_verifier and nonce in the end-user session such that it can be recovered as the user + * gets redirected from the authorization server back to your application. + */ const code_verifier = oauth.generateRandomCodeVerifier() const code_challenge = await oauth.calculatePKCECodeChallenge(code_verifier) -const code_challenge_method = 'S256' +let state: string | undefined +// Signed Request Object (JAR) let request: string { const params = new URLSearchParams() params.set('client_id', client.client_id) - params.set('code_challenge', code_challenge) - params.set('code_challenge_method', code_challenge_method) params.set('redirect_uri', redirect_uri) params.set('response_type', 'code') - params.set('response_mode', 'jwt') - params.set('scope', 'openid email') - - request = await oauth.issueRequestObject(as, client, params, jarPrivateKey) -} - -let request_uri: string -{ - const params = new URLSearchParams() - params.set('client_id', client.client_id) - params.set('request', request) - - const response = await oauth.pushedAuthorizationRequest(as, client, params, { - DPoP, - clientPrivateKey, - }) - let challenges: oauth.WWWAuthenticateChallenge[] | undefined - if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { - for (const challenge of challenges) { - console.log('challenge', challenge) - } - throw new Error() // Handle www-authenticate challenges as needed - } + params.set('scope', 'api:read') + params.set('code_challenge', code_challenge) + params.set('code_challenge_method', code_challenge_method) - const result = await oauth.processPushedAuthorizationResponse(as, client, response) - if (oauth.isOAuth2Error(result)) { - console.log('error', result) - if (result.error === 'use_dpop_nonce') { - // the AS-signalled nonce is now cached, you should retry - } - throw new Error() // Handle OAuth 2.0 response body error + /** + * We cannot be sure the AS supports PKCE so we're going to use state too. Use of PKCE is + * backwards compatible even if the AS doesn't support it which is why we're using it regardless. + */ + if (as.code_challenge_methods_supported?.includes('S256') !== true) { + state = oauth.generateRandomState() + params.set('state', state) } - console.log('result', result) - ;({ request_uri } = result) + request = await oauth.issueRequestObject(as, client, params, jarPrivateKey) } { // redirect user to as.authorization_endpoint const authorizationUrl = new URL(as.authorization_endpoint!) authorizationUrl.searchParams.set('client_id', client.client_id) - authorizationUrl.searchParams.set('request_uri', request_uri) + authorizationUrl.searchParams.set('request', request) // now redirect the user to authorizationUrl.href } // one eternity later, the user lands back on the redirect_uri +// Authorization Code Grant Request & Response +let access_token: string { // @ts-expect-error const currentUrl: URL = getCurrentUrl() - const params = await oauth.validateJwtAuthResponse(as, client, currentUrl) + const params = oauth.validateAuthResponse(as, client, currentUrl, state) if (oauth.isOAuth2Error(params)) { - console.log('error', params) + console.error('Error Response', params) throw new Error() // Handle OAuth 2.0 redirect error } @@ -111,30 +92,41 @@ let request_uri: string params, redirect_uri, code_verifier, - { - DPoP, - clientPrivateKey, - }, ) let challenges: oauth.WWWAuthenticateChallenge[] | undefined if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { for (const challenge of challenges) { - console.log('challenge', challenge) + console.error('WWW-Authenticate Challenge', challenge) } - throw new Error() // Handle www-authenticate challenges as needed + throw new Error() // Handle WWW-Authenticate Challenges as needed } - const result = await oauth.processAuthorizationCodeOpenIDResponse(as, client, response) + const result = await oauth.processAuthorizationCodeOAuth2Response(as, client, response) if (oauth.isOAuth2Error(result)) { - console.log('error', result) - if (result.error === 'use_dpop_nonce') { - // the AS-signalled nonce is now cached, you should retry - } + console.error('Error Response', result) throw new Error() // Handle OAuth 2.0 response body error } - console.log('result', result) - const claims = oauth.getValidatedIdTokenClaims(result) - console.log('ID Token Claims', claims) + console.log('Access Token Response', result) + ;({ access_token } = result) +} + +// Protected Resource Request +{ + const response = await oauth.protectedResourceRequest( + access_token, + 'GET', + new URL('https://rs.example.com/api'), + ) + + let challenges: oauth.WWWAuthenticateChallenge[] | undefined + if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { + for (const challenge of challenges) { + console.error('WWW-Authenticate Challenge', challenge) + } + throw new Error() // Handle WWW-Authenticate Challenges as needed + } + + console.log('Protected Resource Response', await response.json()) } diff --git a/examples/jarm.diff b/examples/jarm.diff new file mode 100644 index 00000000..85457142 --- /dev/null +++ b/examples/jarm.diff @@ -0,0 +1,21 @@ +diff --git a/examples/oauth.ts b/examples/jarm.ts +index cc6d632..0f2170c 100644 +--- a/examples/oauth.ts ++++ b/examples/jarm.ts +@@ -46,6 +46,7 @@ let state: string | undefined + authorizationUrl.searchParams.set('scope', 'api:read') + authorizationUrl.searchParams.set('code_challenge', code_challenge) + authorizationUrl.searchParams.set('code_challenge_method', code_challenge_method) ++ authorizationUrl.searchParams.set('response_mode', 'jwt') // JARM + + /** + * We cannot be sure the AS supports PKCE so we're going to use state too. Use of PKCE is +@@ -65,7 +66,7 @@ let access_token: string + { + // @ts-expect-error + const currentUrl: URL = getCurrentUrl() +- const params = oauth.validateAuthResponse(as, client, currentUrl, state) ++ const params = await oauth.validateJwtAuthResponse(as, client, currentUrl) // JARM + if (oauth.isOAuth2Error(params)) { + console.error('Error Response', params) + throw new Error() // Handle OAuth 2.0 redirect error diff --git a/examples/jarm.ts b/examples/jarm.ts new file mode 100644 index 00000000..0f2170c7 --- /dev/null +++ b/examples/jarm.ts @@ -0,0 +1,118 @@ +import * as oauth from '../src/index.js' // replace with an import of oauth4webapi + +// Prerequisites + +let issuer!: URL // Authorization server's Issuer Identifier URL +let algorithm!: + | 'oauth2' /* For .well-known/oauth-authorization-server discovery */ + | 'oidc' /* For .well-known/openid-configuration discovery */ + | undefined /* Defaults to 'oidc' */ +let client_id!: string +let client_secret!: string +/** + * Value used in the authorization request as redirect_uri pre-registered at the Authorization + * Server. + */ +let redirect_uri!: string + +// End of prerequisites + +const as = await oauth + .discoveryRequest(issuer, { algorithm }) + .then((response) => oauth.processDiscoveryResponse(issuer, response)) + +const client: oauth.Client = { + client_id, + client_secret, + token_endpoint_auth_method: 'client_secret_basic', +} + +const code_challenge_method = 'S256' +/** + * The following MUST be generated for every redirect to the authorization_endpoint. You must store + * the code_verifier and nonce in the end-user session such that it can be recovered as the user + * gets redirected from the authorization server back to your application. + */ +const code_verifier = oauth.generateRandomCodeVerifier() +const code_challenge = await oauth.calculatePKCECodeChallenge(code_verifier) +let state: string | undefined + +{ + // redirect user to as.authorization_endpoint + const authorizationUrl = new URL(as.authorization_endpoint!) + authorizationUrl.searchParams.set('client_id', client.client_id) + authorizationUrl.searchParams.set('redirect_uri', redirect_uri) + authorizationUrl.searchParams.set('response_type', 'code') + authorizationUrl.searchParams.set('scope', 'api:read') + authorizationUrl.searchParams.set('code_challenge', code_challenge) + authorizationUrl.searchParams.set('code_challenge_method', code_challenge_method) + authorizationUrl.searchParams.set('response_mode', 'jwt') // JARM + + /** + * We cannot be sure the AS supports PKCE so we're going to use state too. Use of PKCE is + * backwards compatible even if the AS doesn't support it which is why we're using it regardless. + */ + if (as.code_challenge_methods_supported?.includes('S256') !== true) { + state = oauth.generateRandomState() + authorizationUrl.searchParams.set('state', state) + } + + // now redirect the user to authorizationUrl.href +} + +// one eternity later, the user lands back on the redirect_uri +// Authorization Code Grant Request & Response +let access_token: string +{ + // @ts-expect-error + const currentUrl: URL = getCurrentUrl() + const params = await oauth.validateJwtAuthResponse(as, client, currentUrl) // JARM + if (oauth.isOAuth2Error(params)) { + console.error('Error Response', params) + throw new Error() // Handle OAuth 2.0 redirect error + } + + const response = await oauth.authorizationCodeGrantRequest( + as, + client, + params, + redirect_uri, + code_verifier, + ) + + let challenges: oauth.WWWAuthenticateChallenge[] | undefined + if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { + for (const challenge of challenges) { + console.error('WWW-Authenticate Challenge', challenge) + } + throw new Error() // Handle WWW-Authenticate Challenges as needed + } + + const result = await oauth.processAuthorizationCodeOAuth2Response(as, client, response) + if (oauth.isOAuth2Error(result)) { + console.error('Error Response', result) + throw new Error() // Handle OAuth 2.0 response body error + } + + console.log('Access Token Response', result) + ;({ access_token } = result) +} + +// Protected Resource Request +{ + const response = await oauth.protectedResourceRequest( + access_token, + 'GET', + new URL('https://rs.example.com/api'), + ) + + let challenges: oauth.WWWAuthenticateChallenge[] | undefined + if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { + for (const challenge of challenges) { + console.error('WWW-Authenticate Challenge', challenge) + } + throw new Error() // Handle WWW-Authenticate Challenges as needed + } + + console.log('Protected Resource Response', await response.json()) +} diff --git a/examples/oauth.ts b/examples/oauth.ts index 96b11cbb..cc6d632c 100644 --- a/examples/oauth.ts +++ b/examples/oauth.ts @@ -3,6 +3,10 @@ import * as oauth from '../src/index.js' // replace with an import of oauth4weba // Prerequisites let issuer!: URL // Authorization server's Issuer Identifier URL +let algorithm!: + | 'oauth2' /* For .well-known/oauth-authorization-server discovery */ + | 'oidc' /* For .well-known/openid-configuration discovery */ + | undefined /* Defaults to 'oidc' */ let client_id!: string let client_secret!: string /** @@ -14,7 +18,7 @@ let redirect_uri!: string // End of prerequisites const as = await oauth - .discoveryRequest(issuer, { algorithm: 'oauth2' }) + .discoveryRequest(issuer, { algorithm }) .then((response) => oauth.processDiscoveryResponse(issuer, response)) const client: oauth.Client = { @@ -56,35 +60,58 @@ let state: string | undefined } // one eternity later, the user lands back on the redirect_uri +// Authorization Code Grant Request & Response +let access_token: string +{ + // @ts-expect-error + const currentUrl: URL = getCurrentUrl() + const params = oauth.validateAuthResponse(as, client, currentUrl, state) + if (oauth.isOAuth2Error(params)) { + console.error('Error Response', params) + throw new Error() // Handle OAuth 2.0 redirect error + } -// @ts-expect-error -const currentUrl: URL = getCurrentUrl() -const parameters = oauth.validateAuthResponse(as, client, currentUrl, state) -if (oauth.isOAuth2Error(parameters)) { - console.log('error', parameters) - throw new Error() // Handle OAuth 2.0 redirect error -} + const response = await oauth.authorizationCodeGrantRequest( + as, + client, + params, + redirect_uri, + code_verifier, + ) -const response = await oauth.authorizationCodeGrantRequest( - as, - client, - parameters, - redirect_uri, - code_verifier, -) - -let challenges: oauth.WWWAuthenticateChallenge[] | undefined -if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { - for (const challenge of challenges) { - console.log('challenge', challenge) + let challenges: oauth.WWWAuthenticateChallenge[] | undefined + if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { + for (const challenge of challenges) { + console.error('WWW-Authenticate Challenge', challenge) + } + throw new Error() // Handle WWW-Authenticate Challenges as needed + } + + const result = await oauth.processAuthorizationCodeOAuth2Response(as, client, response) + if (oauth.isOAuth2Error(result)) { + console.error('Error Response', result) + throw new Error() // Handle OAuth 2.0 response body error } - throw new Error() // Handle www-authenticate challenges as needed -} -const result = await oauth.processAuthorizationCodeOAuth2Response(as, client, response) -if (oauth.isOAuth2Error(result)) { - console.log('error', result) - throw new Error() // Handle OAuth 2.0 response body error + console.log('Access Token Response', result) + ;({ access_token } = result) } -console.log('result', result) +// Protected Resource Request +{ + const response = await oauth.protectedResourceRequest( + access_token, + 'GET', + new URL('https://rs.example.com/api'), + ) + + let challenges: oauth.WWWAuthenticateChallenge[] | undefined + if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { + for (const challenge of challenges) { + console.error('WWW-Authenticate Challenge', challenge) + } + throw new Error() // Handle WWW-Authenticate Challenges as needed + } + + console.log('Protected Resource Response', await response.json()) +} diff --git a/examples/oidc.diff b/examples/oidc.diff new file mode 100644 index 00000000..8936ab43 --- /dev/null +++ b/examples/oidc.diff @@ -0,0 +1,87 @@ +diff --git a/examples/oauth.ts b/examples/oidc.ts +index cc6d632..cac6c75 100644 +--- a/examples/oauth.ts ++++ b/examples/oidc.ts +@@ -35,7 +35,7 @@ const code_challenge_method = 'S256' + */ + const code_verifier = oauth.generateRandomCodeVerifier() + const code_challenge = await oauth.calculatePKCECodeChallenge(code_verifier) +-let state: string | undefined ++let nonce: string | undefined + + { + // redirect user to as.authorization_endpoint +@@ -43,17 +43,17 @@ let state: string | undefined + authorizationUrl.searchParams.set('client_id', client.client_id) + authorizationUrl.searchParams.set('redirect_uri', redirect_uri) + authorizationUrl.searchParams.set('response_type', 'code') +- authorizationUrl.searchParams.set('scope', 'api:read') ++ authorizationUrl.searchParams.set('scope', 'openid email') + authorizationUrl.searchParams.set('code_challenge', code_challenge) + authorizationUrl.searchParams.set('code_challenge_method', code_challenge_method) + + /** +- * We cannot be sure the AS supports PKCE so we're going to use state too. Use of PKCE is ++ * We cannot be sure the AS supports PKCE so we're going to use nonce too. Use of PKCE is + * backwards compatible even if the AS doesn't support it which is why we're using it regardless. + */ + if (as.code_challenge_methods_supported?.includes('S256') !== true) { +- state = oauth.generateRandomState() +- authorizationUrl.searchParams.set('state', state) ++ nonce = oauth.generateRandomNonce() ++ authorizationUrl.searchParams.set('nonce', nonce) + } + + // now redirect the user to authorizationUrl.href +@@ -61,11 +61,12 @@ let state: string | undefined + + // one eternity later, the user lands back on the redirect_uri + // Authorization Code Grant Request & Response ++let sub: string + let access_token: string + { + // @ts-expect-error + const currentUrl: URL = getCurrentUrl() +- const params = oauth.validateAuthResponse(as, client, currentUrl, state) ++ const params = oauth.validateAuthResponse(as, client, currentUrl) + if (oauth.isOAuth2Error(params)) { + console.error('Error Response', params) + throw new Error() // Handle OAuth 2.0 redirect error +@@ -87,7 +88,7 @@ let access_token: string + throw new Error() // Handle WWW-Authenticate Challenges as needed + } + +- const result = await oauth.processAuthorizationCodeOAuth2Response(as, client, response) ++ const result = await oauth.processAuthorizationCodeOpenIDResponse(as, client, response, nonce) + if (oauth.isOAuth2Error(result)) { + console.error('Error Response', result) + throw new Error() // Handle OAuth 2.0 response body error +@@ -95,15 +96,14 @@ let access_token: string + + console.log('Access Token Response', result) + ;({ access_token } = result) ++ const claims = oauth.getValidatedIdTokenClaims(result) ++ console.log('ID Token Claims', claims) ++ ;({ sub } = claims) + } + +-// Protected Resource Request ++// UserInfo Request + { +- const response = await oauth.protectedResourceRequest( +- access_token, +- 'GET', +- new URL('https://rs.example.com/api'), +- ) ++ const response = await oauth.userInfoRequest(as, client, access_token) + + let challenges: oauth.WWWAuthenticateChallenge[] | undefined + if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { +@@ -113,5 +113,6 @@ let access_token: string + throw new Error() // Handle WWW-Authenticate Challenges as needed + } + +- console.log('Protected Resource Response', await response.json()) ++ const result = await oauth.processUserInfoResponse(as, client, sub, response) ++ console.log('UserInfo Response', result) + } diff --git a/examples/code.ts b/examples/oidc.ts similarity index 82% rename from examples/code.ts rename to examples/oidc.ts index a7303f1e..cac6c750 100644 --- a/examples/code.ts +++ b/examples/oidc.ts @@ -3,6 +3,10 @@ import * as oauth from '../src/index.js' // replace with an import of oauth4weba // Prerequisites let issuer!: URL // Authorization server's Issuer Identifier URL +let algorithm!: + | 'oauth2' /* For .well-known/oauth-authorization-server discovery */ + | 'oidc' /* For .well-known/openid-configuration discovery */ + | undefined /* Defaults to 'oidc' */ let client_id!: string let client_secret!: string /** @@ -14,7 +18,7 @@ let redirect_uri!: string // End of prerequisites const as = await oauth - .discoveryRequest(issuer) + .discoveryRequest(issuer, { algorithm }) .then((response) => oauth.processDiscoveryResponse(issuer, response)) const client: oauth.Client = { @@ -56,6 +60,7 @@ let nonce: string | undefined } // one eternity later, the user lands back on the redirect_uri +// Authorization Code Grant Request & Response let sub: string let access_token: string { @@ -63,7 +68,7 @@ let access_token: string const currentUrl: URL = getCurrentUrl() const params = oauth.validateAuthResponse(as, client, currentUrl) if (oauth.isOAuth2Error(params)) { - console.log('error', params) + console.error('Error Response', params) throw new Error() // Handle OAuth 2.0 redirect error } @@ -78,36 +83,36 @@ let access_token: string let challenges: oauth.WWWAuthenticateChallenge[] | undefined if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { for (const challenge of challenges) { - console.log('challenge', challenge) + console.error('WWW-Authenticate Challenge', challenge) } - throw new Error() // Handle www-authenticate challenges as needed + throw new Error() // Handle WWW-Authenticate Challenges as needed } const result = await oauth.processAuthorizationCodeOpenIDResponse(as, client, response, nonce) if (oauth.isOAuth2Error(result)) { - console.log('error', result) + console.error('Error Response', result) throw new Error() // Handle OAuth 2.0 response body error } - console.log('result', result) + console.log('Access Token Response', result) ;({ access_token } = result) const claims = oauth.getValidatedIdTokenClaims(result) console.log('ID Token Claims', claims) ;({ sub } = claims) } -// fetch userinfo response +// UserInfo Request { const response = await oauth.userInfoRequest(as, client, access_token) let challenges: oauth.WWWAuthenticateChallenge[] | undefined if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { for (const challenge of challenges) { - console.log('challenge', challenge) + console.error('WWW-Authenticate Challenge', challenge) } - throw new Error() // Handle www-authenticate challenges as needed + throw new Error() // Handle WWW-Authenticate Challenges as needed } const result = await oauth.processUserInfoResponse(as, client, sub, response) - console.log('result', result) + console.log('UserInfo Response', result) } diff --git a/examples/par.diff b/examples/par.diff index 07a9f753..433a2483 100644 --- a/examples/par.diff +++ b/examples/par.diff @@ -1,11 +1,12 @@ -diff --git a/examples/code.ts b/examples/par.ts -index a7303f1..e31489e 100644 ---- a/examples/code.ts +diff --git a/examples/oauth.ts b/examples/par.ts +index cc6d632..110d820 100644 +--- a/examples/oauth.ts +++ b/examples/par.ts -@@ -33,15 +33,15 @@ const code_verifier = oauth.generateRandomCodeVerifier() +@@ -37,15 +37,16 @@ const code_verifier = oauth.generateRandomCodeVerifier() const code_challenge = await oauth.calculatePKCECodeChallenge(code_verifier) - let nonce: string | undefined + let state: string | undefined ++// Pushed Authorization Request & Response (PAR) +let request_uri: string { - // redirect user to as.authorization_endpoint @@ -13,43 +14,42 @@ index a7303f1..e31489e 100644 - authorizationUrl.searchParams.set('client_id', client.client_id) - authorizationUrl.searchParams.set('redirect_uri', redirect_uri) - authorizationUrl.searchParams.set('response_type', 'code') -- authorizationUrl.searchParams.set('scope', 'openid email') +- authorizationUrl.searchParams.set('scope', 'api:read') - authorizationUrl.searchParams.set('code_challenge', code_challenge) - authorizationUrl.searchParams.set('code_challenge_method', code_challenge_method) + const params = new URLSearchParams() + params.set('client_id', client.client_id) + params.set('redirect_uri', redirect_uri) + params.set('response_type', 'code') -+ params.set('scope', 'openid email') ++ params.set('scope', 'api:read') + params.set('code_challenge', code_challenge) + params.set('code_challenge_method', code_challenge_method) /** - * We cannot be sure the AS supports PKCE so we're going to use nonce too. Use of PKCE is -@@ -49,9 +49,34 @@ let nonce: string | undefined + * We cannot be sure the AS supports PKCE so we're going to use state too. Use of PKCE is +@@ -53,9 +54,33 @@ let state: string | undefined */ if (as.code_challenge_methods_supported?.includes('S256') !== true) { - nonce = oauth.generateRandomNonce() -- authorizationUrl.searchParams.set('nonce', nonce) -+ params.set('nonce', nonce) + state = oauth.generateRandomState() +- authorizationUrl.searchParams.set('state', state) ++ params.set('state', state) + } + + const response = await oauth.pushedAuthorizationRequest(as, client, params) + let challenges: oauth.WWWAuthenticateChallenge[] | undefined + if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { + for (const challenge of challenges) { -+ console.log('challenge', challenge) ++ console.error('WWW-Authenticate Challenge', challenge) + } -+ throw new Error() // Handle www-authenticate challenges as needed -+ } -+ ++ throw new Error() // Handle WWW-Authenticate Challenges as needed + } + + const result = await oauth.processPushedAuthorizationResponse(as, client, response) + if (oauth.isOAuth2Error(result)) { -+ console.log('error', result) ++ console.error('Error Response', result) + throw new Error() // Handle OAuth 2.0 response body error - } - -+ console.log('result', result) ++ } ++ + ;({ request_uri } = result) +} + diff --git a/examples/par.ts b/examples/par.ts index e31489ed..110d8209 100644 --- a/examples/par.ts +++ b/examples/par.ts @@ -3,6 +3,10 @@ import * as oauth from '../src/index.js' // replace with an import of oauth4weba // Prerequisites let issuer!: URL // Authorization server's Issuer Identifier URL +let algorithm!: + | 'oauth2' /* For .well-known/oauth-authorization-server discovery */ + | 'oidc' /* For .well-known/openid-configuration discovery */ + | undefined /* Defaults to 'oidc' */ let client_id!: string let client_secret!: string /** @@ -14,7 +18,7 @@ let redirect_uri!: string // End of prerequisites const as = await oauth - .discoveryRequest(issuer) + .discoveryRequest(issuer, { algorithm }) .then((response) => oauth.processDiscoveryResponse(issuer, response)) const client: oauth.Client = { @@ -31,43 +35,43 @@ const code_challenge_method = 'S256' */ const code_verifier = oauth.generateRandomCodeVerifier() const code_challenge = await oauth.calculatePKCECodeChallenge(code_verifier) -let nonce: string | undefined +let state: string | undefined +// Pushed Authorization Request & Response (PAR) let request_uri: string { const params = new URLSearchParams() params.set('client_id', client.client_id) params.set('redirect_uri', redirect_uri) params.set('response_type', 'code') - params.set('scope', 'openid email') + params.set('scope', 'api:read') params.set('code_challenge', code_challenge) params.set('code_challenge_method', code_challenge_method) /** - * We cannot be sure the AS supports PKCE so we're going to use nonce too. Use of PKCE is + * We cannot be sure the AS supports PKCE so we're going to use state too. Use of PKCE is * backwards compatible even if the AS doesn't support it which is why we're using it regardless. */ if (as.code_challenge_methods_supported?.includes('S256') !== true) { - nonce = oauth.generateRandomNonce() - params.set('nonce', nonce) + state = oauth.generateRandomState() + params.set('state', state) } const response = await oauth.pushedAuthorizationRequest(as, client, params) let challenges: oauth.WWWAuthenticateChallenge[] | undefined if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { for (const challenge of challenges) { - console.log('challenge', challenge) + console.error('WWW-Authenticate Challenge', challenge) } - throw new Error() // Handle www-authenticate challenges as needed + throw new Error() // Handle WWW-Authenticate Challenges as needed } const result = await oauth.processPushedAuthorizationResponse(as, client, response) if (oauth.isOAuth2Error(result)) { - console.log('error', result) + console.error('Error Response', result) throw new Error() // Handle OAuth 2.0 response body error } - console.log('result', result) ;({ request_uri } = result) } @@ -81,14 +85,14 @@ let request_uri: string } // one eternity later, the user lands back on the redirect_uri -let sub: string +// Authorization Code Grant Request & Response let access_token: string { // @ts-expect-error const currentUrl: URL = getCurrentUrl() - const params = oauth.validateAuthResponse(as, client, currentUrl) + const params = oauth.validateAuthResponse(as, client, currentUrl, state) if (oauth.isOAuth2Error(params)) { - console.log('error', params) + console.error('Error Response', params) throw new Error() // Handle OAuth 2.0 redirect error } @@ -103,36 +107,36 @@ let access_token: string let challenges: oauth.WWWAuthenticateChallenge[] | undefined if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { for (const challenge of challenges) { - console.log('challenge', challenge) + console.error('WWW-Authenticate Challenge', challenge) } - throw new Error() // Handle www-authenticate challenges as needed + throw new Error() // Handle WWW-Authenticate Challenges as needed } - const result = await oauth.processAuthorizationCodeOpenIDResponse(as, client, response, nonce) + const result = await oauth.processAuthorizationCodeOAuth2Response(as, client, response) if (oauth.isOAuth2Error(result)) { - console.log('error', result) + console.error('Error Response', result) throw new Error() // Handle OAuth 2.0 response body error } - console.log('result', result) + console.log('Access Token Response', result) ;({ access_token } = result) - const claims = oauth.getValidatedIdTokenClaims(result) - console.log('ID Token Claims', claims) - ;({ sub } = claims) } -// fetch userinfo response +// Protected Resource Request { - const response = await oauth.userInfoRequest(as, client, access_token) + const response = await oauth.protectedResourceRequest( + access_token, + 'GET', + new URL('https://rs.example.com/api'), + ) let challenges: oauth.WWWAuthenticateChallenge[] | undefined if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { for (const challenge of challenges) { - console.log('challenge', challenge) + console.error('WWW-Authenticate Challenge', challenge) } - throw new Error() // Handle www-authenticate challenges as needed + throw new Error() // Handle WWW-Authenticate Challenges as needed } - const result = await oauth.processUserInfoResponse(as, client, sub, response) - console.log('result', result) + console.log('Protected Resource Response', await response.json()) } diff --git a/examples/private_key_jwt.diff b/examples/private_key_jwt.diff index 48d4ee92..937c5817 100644 --- a/examples/private_key_jwt.diff +++ b/examples/private_key_jwt.diff @@ -1,10 +1,10 @@ -diff --git a/examples/code.ts b/examples/private_key_jwt.ts -index a7303f1..9a34637 100644 ---- a/examples/code.ts +diff --git a/examples/oauth.ts b/examples/private_key_jwt.ts +index cc6d632..7bf6b06 100644 +--- a/examples/oauth.ts +++ b/examples/private_key_jwt.ts -@@ -4,12 +4,16 @@ import * as oauth from '../src/index.js' // replace with an import of oauth4weba - - let issuer!: URL // Authorization server's Issuer Identifier URL +@@ -8,12 +8,16 @@ let algorithm!: + | 'oidc' /* For .well-known/openid-configuration discovery */ + | undefined /* Defaults to 'oidc' */ let client_id!: string -let client_secret!: string /** @@ -20,7 +20,7 @@ index a7303f1..9a34637 100644 // End of prerequisites -@@ -19,8 +23,7 @@ const as = await oauth +@@ -23,8 +27,7 @@ const as = await oauth const client: oauth.Client = { client_id, @@ -30,7 +30,7 @@ index a7303f1..9a34637 100644 } const code_challenge_method = 'S256' -@@ -73,6 +76,7 @@ let access_token: string +@@ -77,6 +80,7 @@ let access_token: string params, redirect_uri, code_verifier, diff --git a/examples/private_key_jwt.ts b/examples/private_key_jwt.ts index 9a34637d..7bf6b066 100644 --- a/examples/private_key_jwt.ts +++ b/examples/private_key_jwt.ts @@ -3,6 +3,10 @@ import * as oauth from '../src/index.js' // replace with an import of oauth4weba // Prerequisites let issuer!: URL // Authorization server's Issuer Identifier URL +let algorithm!: + | 'oauth2' /* For .well-known/oauth-authorization-server discovery */ + | 'oidc' /* For .well-known/openid-configuration discovery */ + | undefined /* Defaults to 'oidc' */ let client_id!: string /** * Value used in the authorization request as redirect_uri pre-registered at the Authorization @@ -18,7 +22,7 @@ let clientPrivateKey!: CryptoKey // End of prerequisites const as = await oauth - .discoveryRequest(issuer) + .discoveryRequest(issuer, { algorithm }) .then((response) => oauth.processDiscoveryResponse(issuer, response)) const client: oauth.Client = { @@ -34,7 +38,7 @@ const code_challenge_method = 'S256' */ const code_verifier = oauth.generateRandomCodeVerifier() const code_challenge = await oauth.calculatePKCECodeChallenge(code_verifier) -let nonce: string | undefined +let state: string | undefined { // redirect user to as.authorization_endpoint @@ -42,31 +46,31 @@ let nonce: string | undefined authorizationUrl.searchParams.set('client_id', client.client_id) authorizationUrl.searchParams.set('redirect_uri', redirect_uri) authorizationUrl.searchParams.set('response_type', 'code') - authorizationUrl.searchParams.set('scope', 'openid email') + authorizationUrl.searchParams.set('scope', 'api:read') authorizationUrl.searchParams.set('code_challenge', code_challenge) authorizationUrl.searchParams.set('code_challenge_method', code_challenge_method) /** - * We cannot be sure the AS supports PKCE so we're going to use nonce too. Use of PKCE is + * We cannot be sure the AS supports PKCE so we're going to use state too. Use of PKCE is * backwards compatible even if the AS doesn't support it which is why we're using it regardless. */ if (as.code_challenge_methods_supported?.includes('S256') !== true) { - nonce = oauth.generateRandomNonce() - authorizationUrl.searchParams.set('nonce', nonce) + state = oauth.generateRandomState() + authorizationUrl.searchParams.set('state', state) } // now redirect the user to authorizationUrl.href } // one eternity later, the user lands back on the redirect_uri -let sub: string +// Authorization Code Grant Request & Response let access_token: string { // @ts-expect-error const currentUrl: URL = getCurrentUrl() - const params = oauth.validateAuthResponse(as, client, currentUrl) + const params = oauth.validateAuthResponse(as, client, currentUrl, state) if (oauth.isOAuth2Error(params)) { - console.log('error', params) + console.error('Error Response', params) throw new Error() // Handle OAuth 2.0 redirect error } @@ -82,36 +86,36 @@ let access_token: string let challenges: oauth.WWWAuthenticateChallenge[] | undefined if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { for (const challenge of challenges) { - console.log('challenge', challenge) + console.error('WWW-Authenticate Challenge', challenge) } - throw new Error() // Handle www-authenticate challenges as needed + throw new Error() // Handle WWW-Authenticate Challenges as needed } - const result = await oauth.processAuthorizationCodeOpenIDResponse(as, client, response, nonce) + const result = await oauth.processAuthorizationCodeOAuth2Response(as, client, response) if (oauth.isOAuth2Error(result)) { - console.log('error', result) + console.error('Error Response', result) throw new Error() // Handle OAuth 2.0 response body error } - console.log('result', result) + console.log('Access Token Response', result) ;({ access_token } = result) - const claims = oauth.getValidatedIdTokenClaims(result) - console.log('ID Token Claims', claims) - ;({ sub } = claims) } -// fetch userinfo response +// Protected Resource Request { - const response = await oauth.userInfoRequest(as, client, access_token) + const response = await oauth.protectedResourceRequest( + access_token, + 'GET', + new URL('https://rs.example.com/api'), + ) let challenges: oauth.WWWAuthenticateChallenge[] | undefined if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { for (const challenge of challenges) { - console.log('challenge', challenge) + console.error('WWW-Authenticate Challenge', challenge) } - throw new Error() // Handle www-authenticate challenges as needed + throw new Error() // Handle WWW-Authenticate Challenges as needed } - const result = await oauth.processUserInfoResponse(as, client, sub, response) - console.log('result', result) + console.log('Protected Resource Response', await response.json()) } diff --git a/examples/public.diff b/examples/public.diff index 2c7b04a9..85c4064f 100644 --- a/examples/public.diff +++ b/examples/public.diff @@ -1,16 +1,16 @@ -diff --git a/examples/code.ts b/examples/public.ts -index a7303f1..c8b525e 100644 ---- a/examples/code.ts +diff --git a/examples/oauth.ts b/examples/public.ts +index cc6d632..25585b3 100644 +--- a/examples/oauth.ts +++ b/examples/public.ts -@@ -4,7 +4,6 @@ import * as oauth from '../src/index.js' // replace with an import of oauth4weba - - let issuer!: URL // Authorization server's Issuer Identifier URL +@@ -8,7 +8,6 @@ let algorithm!: + | 'oidc' /* For .well-known/openid-configuration discovery */ + | undefined /* Defaults to 'oidc' */ let client_id!: string -let client_secret!: string /** * Value used in the authorization request as redirect_uri pre-registered at the Authorization * Server. -@@ -19,8 +18,7 @@ const as = await oauth +@@ -23,8 +22,7 @@ const as = await oauth const client: oauth.Client = { client_id, diff --git a/examples/public.ts b/examples/public.ts index c8b525e2..25585b3c 100644 --- a/examples/public.ts +++ b/examples/public.ts @@ -3,6 +3,10 @@ import * as oauth from '../src/index.js' // replace with an import of oauth4weba // Prerequisites let issuer!: URL // Authorization server's Issuer Identifier URL +let algorithm!: + | 'oauth2' /* For .well-known/oauth-authorization-server discovery */ + | 'oidc' /* For .well-known/openid-configuration discovery */ + | undefined /* Defaults to 'oidc' */ let client_id!: string /** * Value used in the authorization request as redirect_uri pre-registered at the Authorization @@ -13,7 +17,7 @@ let redirect_uri!: string // End of prerequisites const as = await oauth - .discoveryRequest(issuer) + .discoveryRequest(issuer, { algorithm }) .then((response) => oauth.processDiscoveryResponse(issuer, response)) const client: oauth.Client = { @@ -29,7 +33,7 @@ const code_challenge_method = 'S256' */ const code_verifier = oauth.generateRandomCodeVerifier() const code_challenge = await oauth.calculatePKCECodeChallenge(code_verifier) -let nonce: string | undefined +let state: string | undefined { // redirect user to as.authorization_endpoint @@ -37,31 +41,31 @@ let nonce: string | undefined authorizationUrl.searchParams.set('client_id', client.client_id) authorizationUrl.searchParams.set('redirect_uri', redirect_uri) authorizationUrl.searchParams.set('response_type', 'code') - authorizationUrl.searchParams.set('scope', 'openid email') + authorizationUrl.searchParams.set('scope', 'api:read') authorizationUrl.searchParams.set('code_challenge', code_challenge) authorizationUrl.searchParams.set('code_challenge_method', code_challenge_method) /** - * We cannot be sure the AS supports PKCE so we're going to use nonce too. Use of PKCE is + * We cannot be sure the AS supports PKCE so we're going to use state too. Use of PKCE is * backwards compatible even if the AS doesn't support it which is why we're using it regardless. */ if (as.code_challenge_methods_supported?.includes('S256') !== true) { - nonce = oauth.generateRandomNonce() - authorizationUrl.searchParams.set('nonce', nonce) + state = oauth.generateRandomState() + authorizationUrl.searchParams.set('state', state) } // now redirect the user to authorizationUrl.href } // one eternity later, the user lands back on the redirect_uri -let sub: string +// Authorization Code Grant Request & Response let access_token: string { // @ts-expect-error const currentUrl: URL = getCurrentUrl() - const params = oauth.validateAuthResponse(as, client, currentUrl) + const params = oauth.validateAuthResponse(as, client, currentUrl, state) if (oauth.isOAuth2Error(params)) { - console.log('error', params) + console.error('Error Response', params) throw new Error() // Handle OAuth 2.0 redirect error } @@ -76,36 +80,36 @@ let access_token: string let challenges: oauth.WWWAuthenticateChallenge[] | undefined if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { for (const challenge of challenges) { - console.log('challenge', challenge) + console.error('WWW-Authenticate Challenge', challenge) } - throw new Error() // Handle www-authenticate challenges as needed + throw new Error() // Handle WWW-Authenticate Challenges as needed } - const result = await oauth.processAuthorizationCodeOpenIDResponse(as, client, response, nonce) + const result = await oauth.processAuthorizationCodeOAuth2Response(as, client, response) if (oauth.isOAuth2Error(result)) { - console.log('error', result) + console.error('Error Response', result) throw new Error() // Handle OAuth 2.0 response body error } - console.log('result', result) + console.log('Access Token Response', result) ;({ access_token } = result) - const claims = oauth.getValidatedIdTokenClaims(result) - console.log('ID Token Claims', claims) - ;({ sub } = claims) } -// fetch userinfo response +// Protected Resource Request { - const response = await oauth.userInfoRequest(as, client, access_token) + const response = await oauth.protectedResourceRequest( + access_token, + 'GET', + new URL('https://rs.example.com/api'), + ) let challenges: oauth.WWWAuthenticateChallenge[] | undefined if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { for (const challenge of challenges) { - console.log('challenge', challenge) + console.error('WWW-Authenticate Challenge', challenge) } - throw new Error() // Handle www-authenticate challenges as needed + throw new Error() // Handle WWW-Authenticate Challenges as needed } - const result = await oauth.processUserInfoResponse(as, client, sub, response) - console.log('result', result) + console.log('Protected Resource Response', await response.json()) } diff --git a/examples/refresh_token.diff b/examples/refresh_token.diff new file mode 100644 index 00000000..7db4e704 --- /dev/null +++ b/examples/refresh_token.diff @@ -0,0 +1,47 @@ +diff --git a/examples/oauth.ts b/examples/refresh_token.ts +index cc6d632..357e55d 100644 +--- a/examples/oauth.ts ++++ b/examples/refresh_token.ts +@@ -62,6 +62,7 @@ let state: string | undefined + // one eternity later, the user lands back on the redirect_uri + // Authorization Code Grant Request & Response + let access_token: string ++let refresh_token: string | undefined + { + // @ts-expect-error + const currentUrl: URL = getCurrentUrl() +@@ -94,7 +95,7 @@ let access_token: string + } + + console.log('Access Token Response', result) +- ;({ access_token } = result) ++ ;({ access_token, refresh_token } = result) + } + + // Protected Resource Request +@@ -115,3 +116,25 @@ let access_token: string + + console.log('Protected Resource Response', await response.json()) + } ++ ++// Refresh Token Grant Request & Response ++if (refresh_token) { ++ const response = await oauth.refreshTokenGrantRequest(as, client, refresh_token) ++ ++ let challenges: oauth.WWWAuthenticateChallenge[] | undefined ++ if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { ++ for (const challenge of challenges) { ++ console.error('WWW-Authenticate Challenge', challenge) ++ } ++ throw new Error() // Handle WWW-Authenticate Challenges as needed ++ } ++ ++ const result = await oauth.processRefreshTokenResponse(as, client, response) ++ if (oauth.isOAuth2Error(result)) { ++ console.error('Error Response', result) ++ throw new Error() // Handle OAuth 2.0 response body error ++ } ++ ++ console.log('Access Token Response', result) ++ ;({ access_token, refresh_token } = result) ++} diff --git a/examples/refresh_token.ts b/examples/refresh_token.ts new file mode 100644 index 00000000..357e55dc --- /dev/null +++ b/examples/refresh_token.ts @@ -0,0 +1,140 @@ +import * as oauth from '../src/index.js' // replace with an import of oauth4webapi + +// Prerequisites + +let issuer!: URL // Authorization server's Issuer Identifier URL +let algorithm!: + | 'oauth2' /* For .well-known/oauth-authorization-server discovery */ + | 'oidc' /* For .well-known/openid-configuration discovery */ + | undefined /* Defaults to 'oidc' */ +let client_id!: string +let client_secret!: string +/** + * Value used in the authorization request as redirect_uri pre-registered at the Authorization + * Server. + */ +let redirect_uri!: string + +// End of prerequisites + +const as = await oauth + .discoveryRequest(issuer, { algorithm }) + .then((response) => oauth.processDiscoveryResponse(issuer, response)) + +const client: oauth.Client = { + client_id, + client_secret, + token_endpoint_auth_method: 'client_secret_basic', +} + +const code_challenge_method = 'S256' +/** + * The following MUST be generated for every redirect to the authorization_endpoint. You must store + * the code_verifier and nonce in the end-user session such that it can be recovered as the user + * gets redirected from the authorization server back to your application. + */ +const code_verifier = oauth.generateRandomCodeVerifier() +const code_challenge = await oauth.calculatePKCECodeChallenge(code_verifier) +let state: string | undefined + +{ + // redirect user to as.authorization_endpoint + const authorizationUrl = new URL(as.authorization_endpoint!) + authorizationUrl.searchParams.set('client_id', client.client_id) + authorizationUrl.searchParams.set('redirect_uri', redirect_uri) + authorizationUrl.searchParams.set('response_type', 'code') + authorizationUrl.searchParams.set('scope', 'api:read') + authorizationUrl.searchParams.set('code_challenge', code_challenge) + authorizationUrl.searchParams.set('code_challenge_method', code_challenge_method) + + /** + * We cannot be sure the AS supports PKCE so we're going to use state too. Use of PKCE is + * backwards compatible even if the AS doesn't support it which is why we're using it regardless. + */ + if (as.code_challenge_methods_supported?.includes('S256') !== true) { + state = oauth.generateRandomState() + authorizationUrl.searchParams.set('state', state) + } + + // now redirect the user to authorizationUrl.href +} + +// one eternity later, the user lands back on the redirect_uri +// Authorization Code Grant Request & Response +let access_token: string +let refresh_token: string | undefined +{ + // @ts-expect-error + const currentUrl: URL = getCurrentUrl() + const params = oauth.validateAuthResponse(as, client, currentUrl, state) + if (oauth.isOAuth2Error(params)) { + console.error('Error Response', params) + throw new Error() // Handle OAuth 2.0 redirect error + } + + const response = await oauth.authorizationCodeGrantRequest( + as, + client, + params, + redirect_uri, + code_verifier, + ) + + let challenges: oauth.WWWAuthenticateChallenge[] | undefined + if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { + for (const challenge of challenges) { + console.error('WWW-Authenticate Challenge', challenge) + } + throw new Error() // Handle WWW-Authenticate Challenges as needed + } + + const result = await oauth.processAuthorizationCodeOAuth2Response(as, client, response) + if (oauth.isOAuth2Error(result)) { + console.error('Error Response', result) + throw new Error() // Handle OAuth 2.0 response body error + } + + console.log('Access Token Response', result) + ;({ access_token, refresh_token } = result) +} + +// Protected Resource Request +{ + const response = await oauth.protectedResourceRequest( + access_token, + 'GET', + new URL('https://rs.example.com/api'), + ) + + let challenges: oauth.WWWAuthenticateChallenge[] | undefined + if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { + for (const challenge of challenges) { + console.error('WWW-Authenticate Challenge', challenge) + } + throw new Error() // Handle WWW-Authenticate Challenges as needed + } + + console.log('Protected Resource Response', await response.json()) +} + +// Refresh Token Grant Request & Response +if (refresh_token) { + const response = await oauth.refreshTokenGrantRequest(as, client, refresh_token) + + let challenges: oauth.WWWAuthenticateChallenge[] | undefined + if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) { + for (const challenge of challenges) { + console.error('WWW-Authenticate Challenge', challenge) + } + throw new Error() // Handle WWW-Authenticate Challenges as needed + } + + const result = await oauth.processRefreshTokenResponse(as, client, response) + if (oauth.isOAuth2Error(result)) { + console.error('Error Response', result) + throw new Error() // Handle OAuth 2.0 response body error + } + + console.log('Access Token Response', result) + ;({ access_token, refresh_token } = result) +}