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
29 changes: 28 additions & 1 deletion packages/core/auth-js/src/GoTrueClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2052,6 +2052,28 @@ export default class GoTrueClient {
} = params

if (!access_token || !expires_in || !refresh_token || !token_type) {
if (provider_token || provider_refresh_token) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 Severity: HIGH

CSRF/Token Injection Vulnerability: This code allows arbitrary provider_token and provider_refresh_token values from URL fragments to be injected into existing sessions without validation. An attacker can craft a malicious URL and trick users into visiting it, causing attacker-controlled provider tokens to be merged into the victim's session. There's no CSRF protection (state parameter), origin validation, or server-side verification that these tokens are legitimate.
Helpful? Add 👍 / 👎

💡 Fix Suggestion

Suggestion: This vulnerability requires server-side validation and CSRF protection. Implement the following multi-step fix: (1) Add a 'state' parameter to the OAuth flow that's generated server-side and validated on callback. Store the state parameter securely (e.g., in session storage with CSRF token) before initiating the OAuth flow. (2) Before merging provider_token and provider_refresh_token into the session, validate the state parameter from the URL matches the stored value. (3) Send the provider tokens to the auth server endpoint (e.g., POST to /token/verify) to verify they were issued for the current authenticated user before accepting them. (4) Only merge the tokens if both state validation and server-side token verification succeed. (5) Consider rejecting provider-token-only callbacks entirely if they're not expected in the linkIdentity flow, or require them to come with additional proof of authenticity from the auth server.

Copy link
Copy Markdown
Author

@samarth212 samarth212 Jan 28, 2026

Choose a reason for hiding this comment

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

Updated to use _useSession in _getSessionFromURL for provider‑token‑only callbacks to honor the documented locking contract.

We only merge provider tokens when a valid session already exists (user must be signed in), and the callback is handled by the Supabase auth redirect flow. Still, if maintainers want stricter validation or a type=link gate, I can add that in a follow‑up.

const { data: sessionData, error: sessionError } = await this._useSession(
async (result) => result
)
if (sessionError) throw sessionError
if (!sessionData.session) {
throw new AuthImplicitGrantRedirectError('No session defined in URL')
}

const session: Session = {
...sessionData.session,
provider_token,
provider_refresh_token,
}

// Remove tokens from URL
window.location.hash = ''
this._debug('#_getSessionFromURL()', 'clearing window.location.hash')

return this._returnResult({ data: { session, redirectType: params.type }, error: null })
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

throw new AuthImplicitGrantRedirectError('No session defined in URL')
}

Expand Down Expand Up @@ -2126,7 +2148,12 @@ export default class GoTrueClient {
if (typeof this.detectSessionInUrl === 'function') {
return this.detectSessionInUrl(new URL(window.location.href), params)
}
return Boolean(params.access_token || params.error_description)
return Boolean(
params.access_token ||
params.error_description ||
params.provider_token ||
params.provider_refresh_token
)
}

/**
Expand Down
26 changes: 26 additions & 0 deletions packages/core/auth-js/test/GoTrueClient.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,32 @@ describe('Callback URL handling', () => {
expect(session?.refresh_token).toBe('test-refresh-token')
})

it('should attach provider token when callback only includes provider tokens', async () => {
window.location.href =
'http://localhost:9999/callback#provider_token=provider-token&provider_refresh_token=provider-refresh&type=link'

const expiresAt = Math.floor(Date.now() / 1000) + 3600
storedSession = JSON.stringify({
access_token: 'existing-access-token',
refresh_token: 'existing-refresh-token',
expires_in: 3600,
expires_at: expiresAt,
token_type: 'bearer',
user: { id: 'test-user' },
})

const client = getClientWithSpecificStorage(mockStorage)
await client.initialize()

const {
data: { session },
} = await client.getSession()
expect(session).toBeDefined()
expect(session?.access_token).toBe('existing-access-token')
expect(session?.provider_token).toBe('provider-token')
expect(session?.provider_refresh_token).toBe('provider-refresh')
})

it('should handle error in callback URL', async () => {
// Set up URL with error parameters
window.location.href =
Expand Down
Loading