diff --git a/examples/oauth.ts b/examples/fapi2.ts index d55e62d..bba1f24 100644 --- a/examples/oauth.ts +++ b/examples/fapi2.ts @@ -9,12 +9,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 DPoPKeys!: oauth.CryptoKeyPair +/** + * A key that the client has pre-registered at the Authorization Server for use with Private Key JWT + * client authentication method. + */ +let clientPrivateKey!: oauth.CryptoKey // End of prerequisites @@ -23,36 +33,54 @@ const as = await oauth .then((response) => oauth.processDiscoveryResponse(issuer, response)) const client: oauth.Client = { client_id } -const clientAuth = oauth.ClientSecretPost(client_secret) +const clientAuth = oauth.PrivateKeyJwt(clientPrivateKey) +const DPoP = oauth.DPoP(client, DPoPKeys) 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, clientAuth, params, { + DPoP, + }) + let response = await pushedAuthorizationRequest() + + const processPushedAuthorizationResponse = () => + oauth.processPushedAuthorizationResponse(as, client, response) + let result = await processPushedAuthorizationResponse().catch(async (err) => { + if (oauth.isDPoPNonceError(err)) { + // the AS-signalled nonce is now cached, retrying + response = await pushedAuthorizationRequest() + return processPushedAuthorizationResponse() + } + throw err + }) + + ;({ 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 } @@ -62,18 +90,32 @@ let state: string | undefined let access_token: string { const currentUrl: URL = getCurrentUrl() - const params = oauth.validateAuthResponse(as, client, currentUrl, state) + const params = oauth.validateAuthResponse(as, client, currentUrl) + + const authorizationCodeGrantRequest = () => + oauth.authorizationCodeGrantRequest( + as, + client, + clientAuth, + params, + redirect_uri, + code_verifier, + { DPoP }, + ) + + let response = await authorizationCodeGrantRequest() - const response = await oauth.authorizationCodeGrantRequest( - as, - client, - clientAuth, - params, - redirect_uri, - code_verifier, - ) + const processAuthorizationCodeResponse = () => + oauth.processAuthorizationCodeResponse(as, client, response) - const result = await oauth.processAuthorizationCodeResponse(as, client, response) + let result = await processAuthorizationCodeResponse().catch(async (err) => { + if (oauth.isDPoPNonceError(err)) { + // the AS-signalled nonce is now cached, retrying + response = await authorizationCodeGrantRequest() + return processAuthorizationCodeResponse() + } + throw err + }) console.log('Access Token Response', result) ;({ access_token } = result) @@ -81,11 +123,22 @@ 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().catch((err) => { + if (oauth.isDPoPNonceError(err)) { + // the RS-signalled nonce is now cached, retrying + return protectedResourceRequest() + } + throw err + }) console.log('Protected Resource Response', await response.json()) }