Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
16 changes: 14 additions & 2 deletions packages/core/auth-js/src/GoTrueClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -751,9 +751,21 @@ export default class GoTrueClient {
async exchangeCodeForSession(authCode: string): Promise<AuthTokenResponse> {
await this.initializePromise

return this._acquireLock(this.lockAcquireTimeout, async () => {
return this._exchangeCodeForSession(authCode)
let signedInSession: Session | null = null

const result = await this._acquireLock(this.lockAcquireTimeout, async () => {
const response = await this._exchangeCodeForSession(authCode)
if (!response.error && response.data?.session) {
signedInSession = response.data.session
}
return response
})

if (signedInSession) {
await this._notifyAllSubscribers('SIGNED_IN', signedInSession)
}

return result
}

/**
Expand Down
158 changes: 158 additions & 0 deletions packages/core/auth-js/test/GoTrueClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,164 @@ describe('GoTrueClient', () => {
expect(error?.message).toContain('@supabase/ssr')
expect(error?.code).toEqual('pkce_code_verifier_not_found')
})

test('exchangeCodeForSession() notifies subscribers before resolving', async () => {
const mockFetch = jest.fn().mockResolvedValue({
ok: true,
status: 200,
headers: new Headers(),
json: () =>
Promise.resolve({
access_token: 'test-access-token',
refresh_token: 'test-refresh-token',
expires_in: 3600,
token_type: 'bearer',
user: { id: 'test-user' },
}),
})

const storage = memoryLocalStorageAdapter()
const client = new GoTrueClient({
url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON,
autoRefreshToken: false,
persistSession: true,
storage,
flowType: 'pkce',
fetch: mockFetch,
})

const mockCallback = jest.fn()
const {
data: { subscription },
} = client.onAuthStateChange(mockCallback)

// @ts-expect-error 'Allow access to protected storageKey'
const storageKey = client.storageKey
await storage.setItem(`${storageKey}-code-verifier`, 'mock-verifier')

const { data, error } = await client.exchangeCodeForSession('mock_code')

expect(error).toBeNull()
expect(data.session).not.toBeNull()
expect(mockCallback).toHaveBeenCalledWith('SIGNED_IN', data.session)

subscription?.unsubscribe()
})

test('exchangeCodeForSession() does not notify SIGNED_IN on error', async () => {
const mockFetch = jest.fn().mockResolvedValue({
ok: false,
status: 400,
headers: new Headers(),
json: () =>
Promise.resolve({
error: 'invalid_grant',
error_description: 'Invalid auth code',
}),
})

const storage = memoryLocalStorageAdapter()
const client = new GoTrueClient({
url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON,
autoRefreshToken: false,
persistSession: true,
storage,
flowType: 'pkce',
fetch: mockFetch,
})

const mockCallback = jest.fn()
const {
data: { subscription },
} = client.onAuthStateChange(mockCallback)

// @ts-expect-error 'Allow access to protected storageKey'
const storageKey = client.storageKey
await storage.setItem(`${storageKey}-code-verifier`, 'mock-verifier')

const { error } = await client.exchangeCodeForSession('mock_code')

expect(error).not.toBeNull()
const signedInCalls = mockCallback.mock.calls.filter(([event]) => event === 'SIGNED_IN')
expect(signedInCalls.length).toBe(0)

subscription?.unsubscribe()
})

test('exchangeCodeForSession() notifies outside the auth lock', async () => {
const mockFetch = jest.fn().mockResolvedValue({
ok: true,
status: 200,
headers: new Headers(),
json: () =>
Promise.resolve({
access_token: 'test-access-token',
refresh_token: 'test-refresh-token',
expires_in: 3600,
token_type: 'bearer',
user: { id: 'test-user' },
}),
})

const storage = memoryLocalStorageAdapter()
const client = new GoTrueClient({
url: GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON,
autoRefreshToken: false,
persistSession: true,
storage,
flowType: 'pkce',
fetch: mockFetch,
})

let resolveInitialSession: (() => void) | null = null
const initialSessionReady = new Promise<void>((resolve) => {
resolveInitialSession = resolve
})

let resolveSignedIn: (() => void) | null = null
let rejectSignedIn: ((error: unknown) => void) | null = null
const signedInReady = new Promise<void>((resolve, reject) => {
resolveSignedIn = resolve
rejectSignedIn = reject
})

const {
data: { subscription },
} = client.onAuthStateChange(async (event) => {
if (event === 'INITIAL_SESSION') {
resolveInitialSession?.()
return
}

if (event !== 'SIGNED_IN') {
return
}

try {
// @ts-expect-error 'Allow access to protected lockAcquired'
expect(client.lockAcquired).toBe(false)
const { data, error } = await client.getSession()
expect(error).toBeNull()
expect(data.session).not.toBeNull()
resolveSignedIn?.()
} catch (error) {
rejectSignedIn?.(error)
}
})

await initialSessionReady

// @ts-expect-error 'Allow access to protected storageKey'
const storageKey = client.storageKey
await storage.setItem(`${storageKey}-code-verifier`, 'mock-verifier')

const { error } = await client.exchangeCodeForSession('mock_code')

expect(error).toBeNull()
await signedInReady

subscription?.unsubscribe()
})
})

describe('Email Auth', () => {
Expand Down
Loading