Skip to content

Commit

Permalink
feat: added simple pkce and state checks for auth0
Browse files Browse the repository at this point in the history
  • Loading branch information
Azurency committed Nov 10, 2023
1 parent 86226ad commit 4aea03d
Showing 1 changed file with 60 additions and 2 deletions.
62 changes: 60 additions & 2 deletions src/runtime/server/lib/oauth/auth0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand All @@ -23,7 +24,7 @@ export interface OAuthAuth0Config {
domain?: string
/**
* Auth0 OAuth Audience
* @default process.env.NUXT_OAUTH_AUTH0_AUDIENCE
* @default ''
*/
audience?: string
/**
Expand All @@ -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> | void
onError?: (event: H3Event, error: H3Error) => Promise<void> | 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({
Expand All @@ -65,6 +84,19 @@ export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig) {

const redirectUrl = getRequestURL(event).href
if (!code) {
// Initialize checks
const checks: Record<string, string> = {}
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')
Expand All @@ -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,
{
Expand All @@ -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 => {
Expand Down

0 comments on commit 4aea03d

Please sign in to comment.