Skip to content

Commit 4aea03d

Browse files
committed
feat: added simple pkce and state checks for auth0
1 parent 86226ad commit 4aea03d

File tree

1 file changed

+60
-2
lines changed

1 file changed

+60
-2
lines changed

src/runtime/server/lib/oauth/auth0.ts

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { withQuery, parsePath } from 'ufo'
44
import { ofetch } from 'ofetch'
55
import { defu } from 'defu'
66
import { useRuntimeConfig } from '#imports'
7+
import crypto from 'crypto'
78

89
export interface OAuthAuth0Config {
910
/**
@@ -23,7 +24,7 @@ export interface OAuthAuth0Config {
2324
domain?: string
2425
/**
2526
* Auth0 OAuth Audience
26-
* @default process.env.NUXT_OAUTH_AUTH0_AUDIENCE
27+
* @default ''
2728
*/
2829
audience?: string
2930
/**
@@ -38,19 +39,37 @@ export interface OAuthAuth0Config {
3839
* @default false
3940
*/
4041
emailRequired?: boolean
42+
/**
43+
* checks
44+
* @default []
45+
* @see https://auth0.com/docs/flows/authorization-code-flow-with-proof-key-for-code-exchange-pkce
46+
* @see https://auth0.com/docs/protocols/oauth2/oauth-state
47+
*/
48+
checks?: OAuthChecks[]
4149
}
4250

51+
type OAuthChecks = 'pkce' | 'state'
4352
interface OAuthConfig {
4453
config?: OAuthAuth0Config
4554
onSuccess: (event: H3Event, result: { user: any, tokens: any }) => Promise<void> | void
4655
onError?: (event: H3Event, error: H3Error) => Promise<void> | void
4756
}
4857

58+
function base64URLEncode(str: string) {
59+
return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
60+
}
61+
function randomBytes(length: number) {
62+
return crypto.randomBytes(length).toString('base64')
63+
}
64+
function sha256(buffer: string) {
65+
return crypto.createHash('sha256').update(buffer).digest('base64')
66+
}
67+
4968
export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig) {
5069
return eventHandler(async (event: H3Event) => {
5170
// @ts-ignore
5271
config = defu(config, useRuntimeConfig(event).oauth?.auth0) as OAuthAuth0Config
53-
const { code } = getQuery(event)
72+
const { code, state } = getQuery(event)
5473

5574
if (!config.clientId || !config.clientSecret || !config.domain) {
5675
const error = createError({
@@ -65,6 +84,19 @@ export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig) {
6584

6685
const redirectUrl = getRequestURL(event).href
6786
if (!code) {
87+
// Initialize checks
88+
const checks: Record<string, string> = {}
89+
if (config.checks?.includes('pkce')) {
90+
const pkceVerifier = base64URLEncode(randomBytes(32))
91+
const pkceChallenge = base64URLEncode(sha256(pkceVerifier))
92+
checks['code_challenge'] = pkceChallenge
93+
checks['code_challenge_method'] = 'S256'
94+
setCookie(event, 'nuxt-auth-util-verifier', pkceVerifier, { maxAge: 60 * 15, secure: true, httpOnly: true })
95+
}
96+
if (config.checks?.includes('state')) {
97+
checks['state'] = base64URLEncode(randomBytes(32))
98+
setCookie(event, 'nuxt-auth-util-state', checks['state'], { maxAge: 60 * 15, secure: true, httpOnly: true })
99+
}
68100
config.scope = config.scope || ['openid', 'offline_access']
69101
if (config.emailRequired && !config.scope.includes('email')) {
70102
config.scope.push('email')
@@ -78,10 +110,35 @@ export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig) {
78110
redirect_uri: redirectUrl,
79111
scope: config.scope.join(' '),
80112
audience: config.audience || '',
113+
...checks
81114
})
82115
)
83116
}
84117

118+
// Verify checks
119+
const pkceVerifier = getCookie(event, 'nuxt-auth-util-verifier')
120+
setCookie(event, 'nuxt-auth-util-verifier', '', { maxAge: -1 })
121+
const stateInCookie = getCookie(event, 'nuxt-auth-util-state')
122+
setCookie(event, 'nuxt-auth-util-state', '', { maxAge: -1 })
123+
if (config.checks?.includes('state')) {
124+
if (!state || !stateInCookie) {
125+
const error = createError({
126+
statusCode: 401,
127+
message: 'Auth0 login failed: state is missing'
128+
})
129+
if (!onError) throw error
130+
return onError(event, error)
131+
}
132+
if (state !== stateInCookie) {
133+
const error = createError({
134+
statusCode: 401,
135+
message: 'Auth0 login failed: state does not match'
136+
})
137+
if (!onError) throw error
138+
return onError(event, error)
139+
}
140+
}
141+
85142
const tokens: any = await ofetch(
86143
tokenURL as string,
87144
{
@@ -95,6 +152,7 @@ export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig) {
95152
client_secret: config.clientSecret,
96153
redirect_uri: parsePath(redirectUrl).pathname,
97154
code,
155+
code_verifier: pkceVerifier
98156
}
99157
}
100158
).catch(error => {

0 commit comments

Comments
 (0)