diff --git a/examples/oauth.ts b/examples/fapi1-advanced.ts index cc6d632..fbc52ff 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,12 +129,15 @@ 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 } + // Check ID Token signature for non-repudiation purposes + await oauth.validateIdTokenSignature(as, result) + console.log('Access Token Response', result) ;({ access_token } = result) } @@ -103,6 +148,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