Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/core/auth-js/src/GoTrueClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2480,12 +2480,22 @@ export default class GoTrueClient {
const endpoint = `${this.url}/resend`
if ('email' in credentials) {
const { email, type, options } = credentials
let codeChallenge: string | null = null
let codeChallengeMethod: string | null = null
if (this.flowType === 'pkce') {
;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
this.storage,
this.storageKey
)
}
const { error } = await _request(this.fetch, 'POST', endpoint, {
headers: this.headers,
body: {
email,
type,
gotrue_meta_security: { captcha_token: options?.captchaToken },
code_challenge: codeChallenge,
code_challenge_method: codeChallengeMethod,
},
Comment on lines +2483 to 2499
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the non-PKCE case, code_challenge / code_challenge_method are still being included in the /resend request body as explicit null values. This changes the payload compared to prior versions and can trigger backend validation differences vs omitting the fields entirely. Consider only adding these properties to the body when this.flowType === 'pkce' (e.g., via a conditional object spread) so implicit-flow resend requests remain unchanged.

Suggested change
let codeChallenge: string | null = null
let codeChallengeMethod: string | null = null
if (this.flowType === 'pkce') {
;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
this.storage,
this.storageKey
)
}
const { error } = await _request(this.fetch, 'POST', endpoint, {
headers: this.headers,
body: {
email,
type,
gotrue_meta_security: { captcha_token: options?.captchaToken },
code_challenge: codeChallenge,
code_challenge_method: codeChallengeMethod,
},
const body: {
email: string
type: typeof type
gotrue_meta_security: { captcha_token: string | undefined }
code_challenge?: string | null
code_challenge_method?: string | null
} = {
email,
type,
gotrue_meta_security: { captcha_token: options?.captchaToken },
}
if (this.flowType === 'pkce') {
const [codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
this.storage,
this.storageKey
)
body.code_challenge = codeChallenge
body.code_challenge_method = codeChallengeMethod
}
const { error } = await _request(this.fetch, 'POST', endpoint, {
headers: this.headers,
body,

Copilot uses AI. Check for mistakes.
redirectTo: options?.emailRedirectTo,
})
Expand All @@ -2509,6 +2519,7 @@ export default class GoTrueClient {
'You must provide either an email or phone number and a type'
)
} catch (error) {
await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
if (isAuthError(error)) {
return this._returnResult({ data: { user: null, session: null }, error })
}
Expand Down
10 changes: 10 additions & 0 deletions packages/core/auth-js/test/GoTrueClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,16 @@ describe('GoTrueClient', () => {
expect(error).toBeNull()
})

test('resend with email and PKCE flowType', async () => {
const { email } = mockUserCredentials()
const { error } = await pkceClient.resend({
email,
type: 'signup',
options: { emailRedirectTo: 'http://localhost:9999/welcome' },
})
expect(error).toBeNull()
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test only asserts that pkceClient.resend() returns error === null, but it doesn’t verify the new PKCE-specific behavior (generating/storing a fresh code_verifier and sending code_challenge + code_challenge_method). As written, it could still pass even if those fields aren’t included. Consider using a client with a mocked fetch to assert the request body contains the PKCE fields (and/or assert ${storageKey}-code-verifier is set after the call).

Suggested change
const { error } = await pkceClient.resend({
email,
type: 'signup',
options: { emailRedirectTo: 'http://localhost:9999/welcome' },
})
expect(error).toBeNull()
const fetchSpy = jest
.spyOn(globalThis as any, 'fetch')
.mockImplementation(async (_input: any, init?: any) => {
if (init && typeof init.body === 'string') {
const body = JSON.parse(init.body)
expect(body.code_challenge).toEqual(expect.any(String))
expect(body.code_challenge_method).toBe('S256')
}
return Promise.resolve({
ok: true,
status: 200,
json: async () => ({}),
}) as any
})
try {
const { error } = await pkceClient.resend({
email,
type: 'signup',
options: { emailRedirectTo: 'http://localhost:9999/welcome' },
})
expect(error).toBeNull()
const storedCodeVerifier = await memoryLocalStorageAdapter.getItem(
`${STORAGE_KEY}-code-verifier`
)
expect(storedCodeVerifier).toEqual(expect.any(String))
} finally {
fetchSpy.mockRestore()
}

Copilot uses AI. Check for mistakes.
})

// Phone resend tests moved to docker-tests/phone-otp.test.ts

test('resend() fails without email or phone', async () => {
Expand Down
Loading