@@ -4,6 +4,7 @@ import { withQuery, parsePath } from 'ufo'
4
4
import { ofetch } from 'ofetch'
5
5
import { defu } from 'defu'
6
6
import { useRuntimeConfig } from '#imports'
7
+ import crypto from 'crypto'
7
8
8
9
export interface OAuthAuth0Config {
9
10
/**
@@ -23,7 +24,7 @@ export interface OAuthAuth0Config {
23
24
domain ?: string
24
25
/**
25
26
* Auth0 OAuth Audience
26
- * @default process.env.NUXT_OAUTH_AUTH0_AUDIENCE
27
+ * @default ''
27
28
*/
28
29
audience ?: string
29
30
/**
@@ -38,19 +39,37 @@ export interface OAuthAuth0Config {
38
39
* @default false
39
40
*/
40
41
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 [ ]
41
49
}
42
50
51
+ type OAuthChecks = 'pkce' | 'state'
43
52
interface OAuthConfig {
44
53
config ?: OAuthAuth0Config
45
54
onSuccess : ( event : H3Event , result : { user : any , tokens : any } ) => Promise < void > | void
46
55
onError ?: ( event : H3Event , error : H3Error ) => Promise < void > | void
47
56
}
48
57
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
+
49
68
export function auth0EventHandler ( { config, onSuccess, onError } : OAuthConfig ) {
50
69
return eventHandler ( async ( event : H3Event ) => {
51
70
// @ts -ignore
52
71
config = defu ( config , useRuntimeConfig ( event ) . oauth ?. auth0 ) as OAuthAuth0Config
53
- const { code } = getQuery ( event )
72
+ const { code, state } = getQuery ( event )
54
73
55
74
if ( ! config . clientId || ! config . clientSecret || ! config . domain ) {
56
75
const error = createError ( {
@@ -65,6 +84,19 @@ export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig) {
65
84
66
85
const redirectUrl = getRequestURL ( event ) . href
67
86
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
+ }
68
100
config . scope = config . scope || [ 'openid' , 'offline_access' ]
69
101
if ( config . emailRequired && ! config . scope . includes ( 'email' ) ) {
70
102
config . scope . push ( 'email' )
@@ -78,10 +110,35 @@ export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig) {
78
110
redirect_uri : redirectUrl ,
79
111
scope : config . scope . join ( ' ' ) ,
80
112
audience : config . audience || '' ,
113
+ ...checks
81
114
} )
82
115
)
83
116
}
84
117
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
+
85
142
const tokens : any = await ofetch (
86
143
tokenURL as string ,
87
144
{
@@ -95,6 +152,7 @@ export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig) {
95
152
client_secret : config . clientSecret ,
96
153
redirect_uri : parsePath ( redirectUrl ) . pathname ,
97
154
code,
155
+ code_verifier : pkceVerifier
98
156
}
99
157
}
100
158
) . catch ( error => {
0 commit comments