Skip to content

Commit 615fcf9

Browse files
authored
feat(zitadel) use more secure PKCE flow (#438)
1 parent 67aa321 commit 615fcf9

File tree

2 files changed

+30
-11
lines changed

2 files changed

+30
-11
lines changed

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

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import type { H3Event } from 'h3'
22
import { eventHandler, getQuery, sendRedirect } from 'h3'
33
import { withQuery } from 'ufo'
44
import { defu } from 'defu'
5-
import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from '../utils'
5+
import type { RequestAccessTokenOptions } from '../utils'
6+
import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken, handleState, handlePkceVerifier, handleInvalidState } from '../utils'
67
import { useRuntimeConfig, createError } from '#imports'
78
import type { OAuthConfig } from '#auth-utils'
89

@@ -48,7 +49,7 @@ export function defineOAuthZitadelEventHandler({ config, onSuccess, onError }: O
4849
authorizationParams: {},
4950
}) as OAuthZitadelConfig
5051

51-
const query = getQuery<{ code?: string, error?: string }>(event)
52+
const query = getQuery<{ code?: string, state?: string, error?: string }>(event)
5253

5354
if (query.error) {
5455
const error = createError({
@@ -60,14 +61,18 @@ export function defineOAuthZitadelEventHandler({ config, onSuccess, onError }: O
6061
return onError(event, error)
6162
}
6263

63-
if (!config.clientId || !config.clientSecret || !config.domain) {
64-
return handleMissingConfiguration(event, 'zitadel', ['clientId', 'clientSecret', 'issuerUrl'], onError)
64+
if (!config.clientId || !config.domain) {
65+
return handleMissingConfiguration(event, 'zitadel', ['clientId', 'domain'], onError)
6566
}
6667

6768
const authorizationURL = `https://${config.domain}/oauth/v2/authorize`
6869
const tokenURL = `https://${config.domain}/oauth/v2/token`
6970
const redirectURL = config.redirectURL || getOAuthRedirectURL(event)
7071

72+
// Create pkce verifier
73+
const verifier = await handlePkceVerifier(event)
74+
const state = await handleState(event)
75+
7176
if (!query.code) {
7277
config.scope = config.scope || ['openid']
7378
// Redirect to Zitadel OAuth page
@@ -79,23 +84,37 @@ export function defineOAuthZitadelEventHandler({ config, onSuccess, onError }: O
7984
client_id: config.clientId,
8085
redirect_uri: redirectURL,
8186
scope: config.scope.join(' '),
87+
state,
88+
code_challenge: verifier.code_challenge,
89+
code_challenge_method: verifier.code_challenge_method,
8290
...config.authorizationParams,
8391
}),
8492
)
8593
}
8694

87-
const tokens = await requestAccessToken(tokenURL, {
88-
headers: {
89-
'Authorization': `Basic ${Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64')}`,
90-
'Content-Type': 'application/x-www-form-urlencoded',
91-
},
95+
if (query.state !== state) {
96+
return handleInvalidState(event, 'zitadel', onError)
97+
}
98+
99+
const request: RequestAccessTokenOptions = {
92100
body: {
93101
grant_type: 'authorization_code',
94102
client_id: config.clientId,
95103
redirect_uri: redirectURL,
96104
code: query.code,
105+
code_verifier: verifier.code_verifier,
97106
},
98-
})
107+
}
108+
109+
if (config.clientSecret) {
110+
const basicAuthorization = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64')
111+
request.headers = {
112+
'Authorization': `Basic ${basicAuthorization}`,
113+
'Content-Type': 'application/x-www-form-urlencoded',
114+
}
115+
}
116+
117+
const tokens = await requestAccessToken(tokenURL, request)
99118

100119
if (tokens.error) {
101120
return handleAccessTokenErrorResponse(event, 'zitadel', tokens, onError)

src/runtime/server/lib/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export interface RequestAccessTokenBody {
2727
[key: string]: string | undefined
2828
}
2929

30-
interface RequestAccessTokenOptions {
30+
export interface RequestAccessTokenOptions {
3131
body?: RequestAccessTokenBody
3232
params?: Record<string, string | undefined>
3333
headers?: Record<string, string>

0 commit comments

Comments
 (0)