From 4aea03d8d5180a65f161ff37cac21424557ca04c Mon Sep 17 00:00:00 2001 From: Lassier Antoine Date: Fri, 10 Nov 2023 22:21:11 +0100 Subject: [PATCH] feat: added simple pkce and state checks for auth0 --- src/runtime/server/lib/oauth/auth0.ts | 62 ++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/src/runtime/server/lib/oauth/auth0.ts b/src/runtime/server/lib/oauth/auth0.ts index abe7b499..1ffff02a 100644 --- a/src/runtime/server/lib/oauth/auth0.ts +++ b/src/runtime/server/lib/oauth/auth0.ts @@ -4,6 +4,7 @@ import { withQuery, parsePath } from 'ufo' import { ofetch } from 'ofetch' import { defu } from 'defu' import { useRuntimeConfig } from '#imports' +import crypto from 'crypto' export interface OAuthAuth0Config { /** @@ -23,7 +24,7 @@ export interface OAuthAuth0Config { domain?: string /** * Auth0 OAuth Audience - * @default process.env.NUXT_OAUTH_AUTH0_AUDIENCE + * @default '' */ audience?: string /** @@ -38,19 +39,37 @@ export interface OAuthAuth0Config { * @default false */ emailRequired?: boolean + /** + * checks + * @default [] + * @see https://auth0.com/docs/flows/authorization-code-flow-with-proof-key-for-code-exchange-pkce + * @see https://auth0.com/docs/protocols/oauth2/oauth-state + */ + checks?: OAuthChecks[] } +type OAuthChecks = 'pkce' | 'state' interface OAuthConfig { config?: OAuthAuth0Config onSuccess: (event: H3Event, result: { user: any, tokens: any }) => Promise | void onError?: (event: H3Event, error: H3Error) => Promise | void } +function base64URLEncode(str: string) { + return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') +} +function randomBytes(length: number) { + return crypto.randomBytes(length).toString('base64') +} +function sha256(buffer: string) { + return crypto.createHash('sha256').update(buffer).digest('base64') +} + export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig) { return eventHandler(async (event: H3Event) => { // @ts-ignore config = defu(config, useRuntimeConfig(event).oauth?.auth0) as OAuthAuth0Config - const { code } = getQuery(event) + const { code, state } = getQuery(event) if (!config.clientId || !config.clientSecret || !config.domain) { const error = createError({ @@ -65,6 +84,19 @@ export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig) { const redirectUrl = getRequestURL(event).href if (!code) { + // Initialize checks + const checks: Record = {} + if (config.checks?.includes('pkce')) { + const pkceVerifier = base64URLEncode(randomBytes(32)) + const pkceChallenge = base64URLEncode(sha256(pkceVerifier)) + checks['code_challenge'] = pkceChallenge + checks['code_challenge_method'] = 'S256' + setCookie(event, 'nuxt-auth-util-verifier', pkceVerifier, { maxAge: 60 * 15, secure: true, httpOnly: true }) + } + if (config.checks?.includes('state')) { + checks['state'] = base64URLEncode(randomBytes(32)) + setCookie(event, 'nuxt-auth-util-state', checks['state'], { maxAge: 60 * 15, secure: true, httpOnly: true }) + } config.scope = config.scope || ['openid', 'offline_access'] if (config.emailRequired && !config.scope.includes('email')) { config.scope.push('email') @@ -78,10 +110,35 @@ export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig) { redirect_uri: redirectUrl, scope: config.scope.join(' '), audience: config.audience || '', + ...checks }) ) } + // Verify checks + const pkceVerifier = getCookie(event, 'nuxt-auth-util-verifier') + setCookie(event, 'nuxt-auth-util-verifier', '', { maxAge: -1 }) + const stateInCookie = getCookie(event, 'nuxt-auth-util-state') + setCookie(event, 'nuxt-auth-util-state', '', { maxAge: -1 }) + if (config.checks?.includes('state')) { + if (!state || !stateInCookie) { + const error = createError({ + statusCode: 401, + message: 'Auth0 login failed: state is missing' + }) + if (!onError) throw error + return onError(event, error) + } + if (state !== stateInCookie) { + const error = createError({ + statusCode: 401, + message: 'Auth0 login failed: state does not match' + }) + if (!onError) throw error + return onError(event, error) + } + } + const tokens: any = await ofetch( tokenURL as string, { @@ -95,6 +152,7 @@ export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig) { client_secret: config.clientSecret, redirect_uri: parsePath(redirectUrl).pathname, code, + code_verifier: pkceVerifier } } ).catch(error => {