diff --git a/README.md b/README.md index 7081d496..6e1dba76 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,8 @@ import * as oauth2 from 'https://deno.land/x/oauth4webapi@v2.5.0/mod.ts' - 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 2.0 (Private Key JWT, PAR, DPoP) - [source](examples/fapi2.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](examples/fapi2-message-signing.diff) ## Supported Runtimes diff --git a/examples/README.md b/examples/README.md index ba80c6a4..a7c33c7d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,5 +7,6 @@ A collection of examples for the most common use cases. - 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 2.0 (Private Key JWT, PAR, DPoP) - [source](fapi2.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](fapi2-message-signing.diff) diff --git a/examples/client_credentials.ts b/examples/client_credentials.ts index 39435c6f..1daf4814 100644 --- a/examples/client_credentials.ts +++ b/examples/client_credentials.ts @@ -1,14 +1,20 @@ -import * as oauth from '../src/index.js' +import * as oauth from '../src/index.js' // replace with an import of oauth4webapi + +// Prerequisites + +let issuer!: URL // Authorization server's Issuer Identifier URL +let client_id!: string +let client_secret!: string + +// End of prerequisites -const issuer = new URL('https://example.as.com') const as = await oauth .discoveryRequest(issuer) .then((response) => oauth.processDiscoveryResponse(issuer, response)) const client: oauth.Client = { - client_id: 'abc4ba37-4ab8-49b5-99d4-9441ba35d428', - client_secret: - 'ddce41c3d7618bb30e8a5e5e423fce223427426265ebc96fd9dd5713a6d4fc58bc523c45af42274c210ab18d4a93b5b7169edf6236ed2657f6be64ec41b72f87', + client_id, + client_secret, token_endpoint_auth_method: 'client_secret_basic', } diff --git a/examples/code.ts b/examples/code.ts index 98576c12..895d96ec 100644 --- a/examples/code.ts +++ b/examples/code.ts @@ -1,22 +1,33 @@ -import * as oauth from '../src/index.js' +import * as oauth from '../src/index.js' // replace with an import of oauth4webapi + +// Prerequisites + +let issuer!: URL // Authorization server's Issuer Identifier URL +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 issuer = new URL('https://example.as.com') const as = await oauth .discoveryRequest(issuer) .then((response) => oauth.processDiscoveryResponse(issuer, response)) const client: oauth.Client = { - client_id: 'abc4ba37-4ab8-49b5-99d4-9441ba35d428', - client_secret: - 'ddce41c3d7618bb30e8a5e5e423fce223427426265ebc96fd9dd5713a6d4fc58bc523c45af42274c210ab18d4a93b5b7169edf6236ed2657f6be64ec41b72f87', + client_id, + client_secret, token_endpoint_auth_method: 'client_secret_basic', } -const redirect_uri = 'https://example.rp.com/cb' - if (as.code_challenge_methods_supported?.includes('S256') !== true) { - // This example assumes S256 PKCE support is signalled - // If it isn't supported, random `nonce` must be used for CSRF protection. + /** + * This example assumes S256 PKCE support is signalled. If it isn't supported, a unique random + * `nonce` for each authorization request must be used for CSRF protection. + */ throw new Error() } @@ -26,7 +37,6 @@ const code_challenge_method = 'S256' { // redirect user to as.authorization_endpoint - const authorizationUrl = new URL(as.authorization_endpoint!) authorizationUrl.searchParams.set('client_id', client.client_id) authorizationUrl.searchParams.set('code_challenge', code_challenge) diff --git a/examples/device_authorization_grant.ts b/examples/device_authorization_grant.ts index 44c398a0..a2aebfc2 100644 --- a/examples/device_authorization_grant.ts +++ b/examples/device_authorization_grant.ts @@ -1,12 +1,18 @@ -import * as oauth from '../src/index.js' +import * as oauth from '../src/index.js' // replace with an import of oauth4webapi + +// Prerequisites + +let issuer!: URL // Authorization server's Issuer Identifier URL +let client_id!: string + +// End of prerequisites -const issuer = new URL('https://example.as.com') const as = await oauth .discoveryRequest(issuer) .then((response) => oauth.processDiscoveryResponse(issuer, response)) const client: oauth.Client = { - client_id: 'abc4ba37-4ab8-49b5-99d4-9441ba35d428', + client_id, token_endpoint_auth_method: 'none', } diff --git a/examples/dpop.diff b/examples/dpop.diff index 12ddc58f..1223f0ab 100644 --- a/examples/dpop.diff +++ b/examples/dpop.diff @@ -1,18 +1,21 @@ diff --git a/examples/code.ts b/examples/dpop.ts -index 98576c1..aa2cb90 100644 +index 895d96e..5ff36e6 100644 --- a/examples/code.ts +++ b/examples/dpop.ts -@@ -1,5 +1,9 @@ - import * as oauth from '../src/index.js' +@@ -10,6 +10,12 @@ let client_secret!: string + * 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 -+// 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 -+const DPoP = await oauth.generateKeyPair('ES256') -+ - const issuer = new URL('https://example.as.com') - const as = await oauth - .discoveryRequest(issuer) -@@ -54,6 +58,7 @@ let access_token: string + // End of prerequisites + +@@ -64,6 +70,7 @@ let access_token: string params, redirect_uri, code_verifier, @@ -20,7 +23,7 @@ index 98576c1..aa2cb90 100644 ) let challenges: oauth.WWWAuthenticateChallenge[] | undefined -@@ -67,6 +72,9 @@ let access_token: string +@@ -77,6 +84,9 @@ let access_token: string const result = await oauth.processAuthorizationCodeOpenIDResponse(as, client, response) if (oauth.isOAuth2Error(result)) { console.log('error', result) @@ -30,7 +33,7 @@ index 98576c1..aa2cb90 100644 throw new Error() // Handle OAuth 2.0 response body error } -@@ -79,12 +87,15 @@ let access_token: string +@@ -89,12 +99,15 @@ let access_token: string // fetch userinfo response { diff --git a/examples/dpop.ts b/examples/dpop.ts index aa2cb901..5ff36e6e 100644 --- a/examples/dpop.ts +++ b/examples/dpop.ts @@ -1,26 +1,39 @@ -import * as oauth from '../src/index.js' +import * as oauth from '../src/index.js' // replace with an import of oauth4webapi + +// Prerequisites + +let issuer!: URL // Authorization server's Issuer Identifier URL +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 + +// End of prerequisites -// 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 -const DPoP = await oauth.generateKeyPair('ES256') - -const issuer = new URL('https://example.as.com') const as = await oauth .discoveryRequest(issuer) .then((response) => oauth.processDiscoveryResponse(issuer, response)) const client: oauth.Client = { - client_id: 'abc4ba37-4ab8-49b5-99d4-9441ba35d428', - client_secret: - 'ddce41c3d7618bb30e8a5e5e423fce223427426265ebc96fd9dd5713a6d4fc58bc523c45af42274c210ab18d4a93b5b7169edf6236ed2657f6be64ec41b72f87', + client_id, + client_secret, token_endpoint_auth_method: 'client_secret_basic', } -const redirect_uri = 'https://example.rp.com/cb' - if (as.code_challenge_methods_supported?.includes('S256') !== true) { - // This example assumes S256 PKCE support is signalled - // If it isn't supported, random `nonce` must be used for CSRF protection. + /** + * This example assumes S256 PKCE support is signalled. If it isn't supported, a unique random + * `nonce` for each authorization request must be used for CSRF protection. + */ throw new Error() } @@ -30,7 +43,6 @@ const code_challenge_method = 'S256' { // redirect user to as.authorization_endpoint - const authorizationUrl = new URL(as.authorization_endpoint!) authorizationUrl.searchParams.set('client_id', client.client_id) authorizationUrl.searchParams.set('code_challenge', code_challenge) diff --git a/examples/fapi1-advanced.ts b/examples/fapi1-advanced.ts new file mode 100644 index 00000000..3b9ddc9b --- /dev/null +++ b/examples/fapi1-advanced.ts @@ -0,0 +1,125 @@ +import * as undici from 'undici' +import * as oauth from '../src/index.js' // replace with an import of oauth4webapi + +// Prerequisites + +let issuer!: URL // Authorization server's Issuer Identifier URL +let client_id!: 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 + +const as = await oauth + .discoveryRequest(issuer) + .then((response) => oauth.processDiscoveryResponse(issuer, response)) + +const client: oauth.Client = { + client_id, + token_endpoint_auth_method: 'private_key_jwt', +} + +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 +{ + 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 email') + 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('request', request) +} + +// one eternity later, the user lands back on the redirect_uri +{ + // @ts-expect-error + const authorizationResponse: URLSearchParams = getAuthorizationResponse() + const params = await oauth.experimental_validateDetachedSignatureResponse( + as, + client, + authorizationResponse, + nonce, + ) + if (oauth.isOAuth2Error(params)) { + console.log('error', params) + throw new Error() // Handle OAuth 2.0 redirect error + } + + const response = await oauth.authorizationCodeGrantRequest( + as, + client, + params, + redirect_uri, + code_verifier, + { + clientPrivateKey, + [oauth.experimental_useMtlsAlias]: true, + // @ts-expect-error + [oauth.experimental_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.log('challenge', challenge) + } + 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) + 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) +} diff --git a/examples/fapi2-message-signing.diff b/examples/fapi2-message-signing.diff index 4368e1d7..8cba5e35 100644 --- a/examples/fapi2-message-signing.diff +++ b/examples/fapi2-message-signing.diff @@ -1,8 +1,20 @@ diff --git a/examples/fapi2.ts b/examples/fapi2-message-signing.ts -index 3717f77..8897055 100644 +index 2f73b7a..f49f057 100644 --- a/examples/fapi2.ts +++ b/examples/fapi2-message-signing.ts -@@ -26,7 +26,7 @@ const code_verifier = oauth.generateRandomCodeVerifier() +@@ -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' @@ -11,14 +23,14 @@ index 3717f77..8897055 100644 { const params = new URLSearchParams() params.set('client_id', client.client_id) -@@ -34,8 +34,18 @@ let request_uri: string +@@ -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, clientPrivateKey) ++ request = await oauth.issueRequestObject(as, client, params, jarPrivateKey) +} + +let request_uri: string @@ -30,7 +42,7 @@ index 3717f77..8897055 100644 const response = await oauth.pushedAuthorizationRequest(as, client, params, { DPoP, clientPrivateKey, -@@ -73,7 +83,7 @@ let request_uri: string +@@ -82,7 +97,7 @@ let request_uri: string { // @ts-expect-error const currentUrl: URL = getCurrentUrl() diff --git a/examples/fapi2-message-signing.ts b/examples/fapi2-message-signing.ts index 8897055c..f49f0573 100644 --- a/examples/fapi2-message-signing.ts +++ b/examples/fapi2-message-signing.ts @@ -1,27 +1,42 @@ -import * as oauth from '../src/index.js' +import * as oauth from '../src/index.js' // replace with an import of oauth4webapi + +// Prerequisites + +let issuer!: URL // Authorization server's Issuer Identifier URL +let client_id!: 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. + */ +let jarPrivateKey!: CryptoKey + +// End of prerequisites -// in order to take full advantage of DPoP you shall generate a random private key for every session -const DPoP = await oauth.generateKeyPair('ES256') - -// a random client private key is generated here for the example's sake, you would however -// use crypto.subtle.importKey to import a private key that is pre-registered on the AS -const clientPrivateKey = { - key: (await oauth.generateKeyPair('ES256')).privateKey, - kid: 'a52faab2-f688-44b6-bde8-f493aeb526fb', // the `kid` the authorization server expects, or undefined if not applicable -} - -const issuer = new URL('https://example.as.com') const as = await oauth .discoveryRequest(issuer) .then((response) => oauth.processDiscoveryResponse(issuer, response)) const client: oauth.Client = { - client_id: 'abc4ba37-4ab8-49b5-99d4-9441ba35d428', + client_id, token_endpoint_auth_method: 'private_key_jwt', } -const redirect_uri = 'https://example.rp.com/cb' - const code_verifier = oauth.generateRandomCodeVerifier() const code_challenge = await oauth.calculatePKCECodeChallenge(code_verifier) const code_challenge_method = 'S256' @@ -37,7 +52,7 @@ let request: string params.set('response_mode', 'jwt') params.set('scope', 'openid email') - request = await oauth.issueRequestObject(as, client, params, clientPrivateKey) + request = await oauth.issueRequestObject(as, client, params, jarPrivateKey) } let request_uri: string @@ -73,7 +88,6 @@ let request_uri: 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('request_uri', request_uri) diff --git a/examples/fapi2.ts b/examples/fapi2.ts index 3717f778..2f73b7a7 100644 --- a/examples/fapi2.ts +++ b/examples/fapi2.ts @@ -1,27 +1,37 @@ -import * as oauth from '../src/index.js' +import * as oauth from '../src/index.js' // replace with an import of oauth4webapi + +// Prerequisites + +let issuer!: URL // Authorization server's Issuer Identifier URL +let client_id!: 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 -// in order to take full advantage of DPoP you shall generate a random private key for every session -const DPoP = await oauth.generateKeyPair('ES256') - -// a random client private key is generated here for the example's sake, you would however -// use crypto.subtle.importKey to import a private key that is pre-registered on the AS -const clientPrivateKey = { - key: (await oauth.generateKeyPair('ES256')).privateKey, - kid: 'a52faab2-f688-44b6-bde8-f493aeb526fb', // the `kid` the authorization server expects, or undefined if not applicable -} - -const issuer = new URL('https://example.as.com') const as = await oauth .discoveryRequest(issuer) .then((response) => oauth.processDiscoveryResponse(issuer, response)) const client: oauth.Client = { - client_id: 'abc4ba37-4ab8-49b5-99d4-9441ba35d428', + client_id, token_endpoint_auth_method: 'private_key_jwt', } -const redirect_uri = 'https://example.rp.com/cb' - const code_verifier = oauth.generateRandomCodeVerifier() const code_challenge = await oauth.calculatePKCECodeChallenge(code_verifier) const code_challenge_method = 'S256' @@ -63,7 +73,6 @@ let request_uri: 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('request_uri', request_uri) diff --git a/examples/oauth.ts b/examples/oauth.ts index 49070cae..f4083e5f 100644 --- a/examples/oauth.ts +++ b/examples/oauth.ts @@ -1,22 +1,33 @@ -import * as oauth from '../src/index.js' +import * as oauth from '../src/index.js' // replace with an import of oauth4webapi + +// Prerequisites + +let issuer!: URL // Authorization server's Issuer Identifier URL +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 issuer = new URL('https://example.as.com') const as = await oauth .discoveryRequest(issuer, { algorithm: 'oauth2' }) .then((response) => oauth.processDiscoveryResponse(issuer, response)) const client: oauth.Client = { - client_id: 'abc4ba37-4ab8-49b5-99d4-9441ba35d428', - client_secret: - 'ddce41c3d7618bb30e8a5e5e423fce223427426265ebc96fd9dd5713a6d4fc58bc523c45af42274c210ab18d4a93b5b7169edf6236ed2657f6be64ec41b72f87', + client_id, + client_secret, token_endpoint_auth_method: 'client_secret_basic', } -const redirect_uri = 'https://example.rp.com/cb' - if (as.code_challenge_methods_supported?.includes('S256') !== true) { - // This example assumes S256 PKCE support is signalled - // If it isn't supported, random `state` must be used for CSRF protection. + /** + * This example assumes S256 PKCE support is signalled. If it isn't supported, a unique random + * `state` for each authorization request must be used for CSRF protection. + */ throw new Error() } @@ -26,7 +37,6 @@ const code_challenge_method = 'S256' { // redirect user to as.authorization_endpoint - const authorizationUrl = new URL(as.authorization_endpoint!) authorizationUrl.searchParams.set('client_id', client.client_id) authorizationUrl.searchParams.set('code_challenge', code_challenge) diff --git a/examples/par.diff b/examples/par.diff index cffd0c8f..0d63b920 100644 --- a/examples/par.diff +++ b/examples/par.diff @@ -1,8 +1,8 @@ diff --git a/examples/code.ts b/examples/par.ts -index 98576c1..1cbaba2 100644 +index 895d96e..d77092b 100644 --- a/examples/code.ts +++ b/examples/par.ts -@@ -24,16 +24,41 @@ const code_verifier = oauth.generateRandomCodeVerifier() +@@ -35,15 +35,40 @@ const code_verifier = oauth.generateRandomCodeVerifier() const code_challenge = await oauth.calculatePKCECodeChallenge(code_verifier) const code_challenge_method = 'S256' @@ -37,7 +37,6 @@ index 98576c1..1cbaba2 100644 + { // redirect user to as.authorization_endpoint - const authorizationUrl = new URL(as.authorization_endpoint!) authorizationUrl.searchParams.set('client_id', client.client_id) - authorizationUrl.searchParams.set('code_challenge', code_challenge) diff --git a/examples/par.ts b/examples/par.ts index 1cbaba28..d77092b1 100644 --- a/examples/par.ts +++ b/examples/par.ts @@ -1,22 +1,33 @@ -import * as oauth from '../src/index.js' +import * as oauth from '../src/index.js' // replace with an import of oauth4webapi + +// Prerequisites + +let issuer!: URL // Authorization server's Issuer Identifier URL +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 issuer = new URL('https://example.as.com') const as = await oauth .discoveryRequest(issuer) .then((response) => oauth.processDiscoveryResponse(issuer, response)) const client: oauth.Client = { - client_id: 'abc4ba37-4ab8-49b5-99d4-9441ba35d428', - client_secret: - 'ddce41c3d7618bb30e8a5e5e423fce223427426265ebc96fd9dd5713a6d4fc58bc523c45af42274c210ab18d4a93b5b7169edf6236ed2657f6be64ec41b72f87', + client_id, + client_secret, token_endpoint_auth_method: 'client_secret_basic', } -const redirect_uri = 'https://example.rp.com/cb' - if (as.code_challenge_methods_supported?.includes('S256') !== true) { - // This example assumes S256 PKCE support is signalled - // If it isn't supported, random `nonce` must be used for CSRF protection. + /** + * This example assumes S256 PKCE support is signalled. If it isn't supported, a unique random + * `nonce` for each authorization request must be used for CSRF protection. + */ throw new Error() } @@ -55,7 +66,6 @@ let request_uri: 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('request_uri', request_uri) diff --git a/examples/private_key_jwt.diff b/examples/private_key_jwt.diff index 3ef70990..01a0af21 100644 --- a/examples/private_key_jwt.diff +++ b/examples/private_key_jwt.diff @@ -1,39 +1,40 @@ diff --git a/examples/code.ts b/examples/private_key_jwt.ts -index 98576c1..1a69495 100644 +index 895d96e..6163d34 100644 --- a/examples/code.ts +++ b/examples/private_key_jwt.ts -@@ -1,5 +1,10 @@ - import * as oauth from '../src/index.js' +@@ -4,12 +4,16 @@ import * as oauth from '../src/index.js' // replace with an import of oauth4weba -+// a random client authentication key is generated here for the example's sake, you would however -+// use crypto.subtle.importKey to import a private key that is pre-registered on the AS -+const { privateKey } = await oauth.generateKeyPair('ES256') -+const keyID = 'a52faab2-f688-44b6-bde8-f493aeb526fb' // the `kid` the authorization server expects, or undefined if not applicable -+ - const issuer = new URL('https://example.as.com') - const as = await oauth - .discoveryRequest(issuer) -@@ -7,9 +12,7 @@ const as = await oauth + let issuer!: URL // Authorization server's Issuer Identifier URL + 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 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 + +@@ -19,8 +23,7 @@ const as = await oauth const client: oauth.Client = { - client_id: 'abc4ba37-4ab8-49b5-99d4-9441ba35d428', -- client_secret: -- 'ddce41c3d7618bb30e8a5e5e423fce223427426265ebc96fd9dd5713a6d4fc58bc523c45af42274c210ab18d4a93b5b7169edf6236ed2657f6be64ec41b72f87', + client_id, +- client_secret, - token_endpoint_auth_method: 'client_secret_basic', + token_endpoint_auth_method: 'private_key_jwt', } - const redirect_uri = 'https://example.rp.com/cb' -@@ -54,6 +57,12 @@ let access_token: string + if (as.code_challenge_methods_supported?.includes('S256') !== true) { +@@ -64,6 +67,7 @@ let access_token: string params, redirect_uri, code_verifier, -+ { -+ clientPrivateKey: { -+ key: privateKey, -+ kid: keyID, -+ }, -+ }, ++ { clientPrivateKey }, ) let challenges: oauth.WWWAuthenticateChallenge[] | undefined diff --git a/examples/private_key_jwt.ts b/examples/private_key_jwt.ts index 1a69495f..6163d346 100644 --- a/examples/private_key_jwt.ts +++ b/examples/private_key_jwt.ts @@ -1,25 +1,36 @@ -import * as oauth from '../src/index.js' +import * as oauth from '../src/index.js' // replace with an import of oauth4webapi -// a random client authentication key is generated here for the example's sake, you would however -// use crypto.subtle.importKey to import a private key that is pre-registered on the AS -const { privateKey } = await oauth.generateKeyPair('ES256') -const keyID = 'a52faab2-f688-44b6-bde8-f493aeb526fb' // the `kid` the authorization server expects, or undefined if not applicable +// Prerequisites + +let issuer!: URL // Authorization server's Issuer Identifier URL +let client_id!: string +/** + * Value used in the authorization request as redirect_uri pre-registered at the Authorization + * Server. + */ +let redirect_uri!: string +/** + * 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 -const issuer = new URL('https://example.as.com') const as = await oauth .discoveryRequest(issuer) .then((response) => oauth.processDiscoveryResponse(issuer, response)) const client: oauth.Client = { - client_id: 'abc4ba37-4ab8-49b5-99d4-9441ba35d428', + client_id, token_endpoint_auth_method: 'private_key_jwt', } -const redirect_uri = 'https://example.rp.com/cb' - if (as.code_challenge_methods_supported?.includes('S256') !== true) { - // This example assumes S256 PKCE support is signalled - // If it isn't supported, random `nonce` must be used for CSRF protection. + /** + * This example assumes S256 PKCE support is signalled. If it isn't supported, a unique random + * `nonce` for each authorization request must be used for CSRF protection. + */ throw new Error() } @@ -29,7 +40,6 @@ const code_challenge_method = 'S256' { // redirect user to as.authorization_endpoint - const authorizationUrl = new URL(as.authorization_endpoint!) authorizationUrl.searchParams.set('client_id', client.client_id) authorizationUrl.searchParams.set('code_challenge', code_challenge) @@ -57,12 +67,7 @@ let access_token: string params, redirect_uri, code_verifier, - { - clientPrivateKey: { - key: privateKey, - kid: keyID, - }, - }, + { clientPrivateKey }, ) let challenges: oauth.WWWAuthenticateChallenge[] | undefined diff --git a/examples/public.diff b/examples/public.diff index 2536eb94..5e96d728 100644 --- a/examples/public.diff +++ b/examples/public.diff @@ -1,15 +1,22 @@ diff --git a/examples/code.ts b/examples/public.ts -index 98576c1..97791b2 100644 +index 895d96e..ba360f4 100644 --- a/examples/code.ts +++ b/examples/public.ts -@@ -7,9 +7,7 @@ const as = await oauth +@@ -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 + 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 const client: oauth.Client = { - client_id: 'abc4ba37-4ab8-49b5-99d4-9441ba35d428', -- client_secret: -- 'ddce41c3d7618bb30e8a5e5e423fce223427426265ebc96fd9dd5713a6d4fc58bc523c45af42274c210ab18d4a93b5b7169edf6236ed2657f6be64ec41b72f87', + client_id, +- client_secret, - token_endpoint_auth_method: 'client_secret_basic', + token_endpoint_auth_method: 'none', } - const redirect_uri = 'https://example.rp.com/cb' + if (as.code_challenge_methods_supported?.includes('S256') !== true) { diff --git a/examples/public.ts b/examples/public.ts index 97791b23..ba360f4f 100644 --- a/examples/public.ts +++ b/examples/public.ts @@ -1,20 +1,31 @@ -import * as oauth from '../src/index.js' +import * as oauth from '../src/index.js' // replace with an import of oauth4webapi + +// Prerequisites + +let issuer!: URL // Authorization server's Issuer Identifier URL +let client_id!: string +/** + * Value used in the authorization request as redirect_uri pre-registered at the Authorization + * Server. + */ +let redirect_uri!: string + +// End of prerequisites -const issuer = new URL('https://example.as.com') const as = await oauth .discoveryRequest(issuer) .then((response) => oauth.processDiscoveryResponse(issuer, response)) const client: oauth.Client = { - client_id: 'abc4ba37-4ab8-49b5-99d4-9441ba35d428', + client_id, token_endpoint_auth_method: 'none', } -const redirect_uri = 'https://example.rp.com/cb' - if (as.code_challenge_methods_supported?.includes('S256') !== true) { - // This example assumes S256 PKCE support is signalled - // If it isn't supported, random `nonce` must be used for CSRF protection. + /** + * This example assumes S256 PKCE support is signalled. If it isn't supported, a unique random + * `nonce` for each authorization request must be used for CSRF protection. + */ throw new Error() } @@ -24,7 +35,6 @@ const code_challenge_method = 'S256' { // redirect user to as.authorization_endpoint - const authorizationUrl = new URL(as.authorization_endpoint!) authorizationUrl.searchParams.set('client_id', client.client_id) authorizationUrl.searchParams.set('code_challenge', code_challenge)