Skip to content

Commit 56258a7

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

File tree

1 file changed

+63
-2
lines changed

1 file changed

+63
-2
lines changed

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

Lines changed: 63 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,40 @@ 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.toString('base64')
60+
.replace(/\+/g, '-')
61+
.replace(/\//g, '_')
62+
.replace(/=/g, '')
63+
}
64+
function randomBytes(length: number) {
65+
return crypto.randomBytes(length).toString('base64')
66+
}
67+
function sha256(buffer: string) {
68+
return crypto.createHash('sha256').update(buffer).digest('base64')
69+
}
70+
4971
export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig) {
5072
return eventHandler(async (event: H3Event) => {
5173
// @ts-ignore
5274
config = defu(config, useRuntimeConfig(event).oauth?.auth0) as OAuthAuth0Config
53-
const { code } = getQuery(event)
75+
const { code, state } = getQuery(event)
5476

5577
if (!config.clientId || !config.clientSecret || !config.domain) {
5678
const error = createError({
@@ -65,6 +87,19 @@ export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig) {
6587

6688
const redirectUrl = getRequestURL(event).href
6789
if (!code) {
90+
// Initialize checks
91+
const checks: Record<string, string> = {}
92+
if (config.checks?.includes('pkce')) {
93+
const pkceVerifier = base64URLEncode(randomBytes(32))
94+
const pkceChallenge = base64URLEncode(sha256(pkceVerifier))
95+
checks['code_challenge'] = pkceChallenge
96+
checks['code_challenge_method'] = 'S256'
97+
setCookie(event, 'nuxt-auth-util-verifier', pkceVerifier, { maxAge: 60 * 15, secure: true, httpOnly: true })
98+
}
99+
if (config.checks?.includes('state')) {
100+
checks['state'] = base64URLEncode(randomBytes(32))
101+
setCookie(event, 'nuxt-auth-util-state', checks['state'], { maxAge: 60 * 15, secure: true, httpOnly: true })
102+
}
68103
config.scope = config.scope || ['openid', 'offline_access']
69104
if (config.emailRequired && !config.scope.includes('email')) {
70105
config.scope.push('email')
@@ -78,10 +113,35 @@ export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig) {
78113
redirect_uri: redirectUrl,
79114
scope: config.scope.join(' '),
80115
audience: config.audience || '',
116+
...checks
81117
})
82118
)
83119
}
84120

121+
// Verify checks
122+
const pkceVerifier = getCookie(event, 'nuxt-auth-util-verifier')
123+
setCookie(event, 'nuxt-auth-util-verifier', '', { maxAge: -1 })
124+
const stateInCookie = getCookie(event, 'nuxt-auth-util-state')
125+
setCookie(event, 'nuxt-auth-util-state', '', { maxAge: -1 })
126+
if (config.checks?.includes('state')) {
127+
if (!state || !stateInCookie) {
128+
const error = createError({
129+
statusCode: 401,
130+
message: 'Auth0 login failed: state is missing'
131+
})
132+
if (!onError) throw error
133+
return onError(event, error)
134+
}
135+
if (state !== stateInCookie) {
136+
const error = createError({
137+
statusCode: 401,
138+
message: 'Auth0 login failed: state does not match'
139+
})
140+
if (!onError) throw error
141+
return onError(event, error)
142+
}
143+
}
144+
85145
const tokens: any = await ofetch(
86146
tokenURL as string,
87147
{
@@ -95,6 +155,7 @@ export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig) {
95155
client_secret: config.clientSecret,
96156
redirect_uri: parsePath(redirectUrl).pathname,
97157
code,
158+
code_verifier: pkceVerifier
98159
}
99160
}
100161
).catch(error => {

0 commit comments

Comments
 (0)