@@ -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,40 @@ 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 . 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
+
49
71
export function auth0EventHandler ( { config, onSuccess, onError } : OAuthConfig ) {
50
72
return eventHandler ( async ( event : H3Event ) => {
51
73
// @ts -ignore
52
74
config = defu ( config , useRuntimeConfig ( event ) . oauth ?. auth0 ) as OAuthAuth0Config
53
- const { code } = getQuery ( event )
75
+ const { code, state } = getQuery ( event )
54
76
55
77
if ( ! config . clientId || ! config . clientSecret || ! config . domain ) {
56
78
const error = createError ( {
@@ -65,6 +87,19 @@ export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig) {
65
87
66
88
const redirectUrl = getRequestURL ( event ) . href
67
89
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
+ }
68
103
config . scope = config . scope || [ 'openid' , 'offline_access' ]
69
104
if ( config . emailRequired && ! config . scope . includes ( 'email' ) ) {
70
105
config . scope . push ( 'email' )
@@ -78,10 +113,35 @@ export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig) {
78
113
redirect_uri : redirectUrl ,
79
114
scope : config . scope . join ( ' ' ) ,
80
115
audience : config . audience || '' ,
116
+ ...checks
81
117
} )
82
118
)
83
119
}
84
120
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
+
85
145
const tokens : any = await ofetch (
86
146
tokenURL as string ,
87
147
{
@@ -95,6 +155,7 @@ export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig) {
95
155
client_secret : config . clientSecret ,
96
156
redirect_uri : parsePath ( redirectUrl ) . pathname ,
97
157
code,
158
+ code_verifier : pkceVerifier
98
159
}
99
160
}
100
161
) . catch ( error => {
0 commit comments