Skip to content

Commit 2d3dbc6

Browse files
authored
feat: add Supabase Auth identifier to OAuth redirect URLs (#2299)
## Summary Adds a Supabase Auth identifier (`sb`) to URL fragments in all OAuth redirect responses to help clients distinguish Supabase Auth redirects from third-party OAuth flows. ## Problem auth-js GoTrueClient currently intercepts all URL fragments containing `access_token`, including those from non-Supabase OAuth providers. This causes unintended logouts and authentication issues when users have other OAuth flows in their applications. Related issue: supabase/supabase-js#1697 ## Solution Added an empty `sb` parameter to the URL fragment in all redirect responses: - Success redirects with tokens (via `AsRedirectURL`) - Error redirects in OAuth callbacks ([supabase-js has](https://github.com/supabase/supabase-js/blob/a66387e9923255160031a1c55545cf7ab27b3aaf/packages/core/auth-js/src/lib/errors.ts#L14-L38) a `__isAuthError`, but adding it for error to be fault-tolerant, and non-supabase-sdk cases) - Error redirects in verification flows - Message redirects in verification flows Example redirect URL: `https://example.com/callback#access_token=xxx&refresh_token=yyy&expires_in=3600&sb` Clients can now check for the presence of `sb` in the fragment to confirm the redirect originated from Supabase Auth.
1 parent c43eacf commit 2d3dbc6

File tree

6 files changed

+57
-8
lines changed

6 files changed

+57
-8
lines changed

internal/api/external.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,8 @@ func redirectErrors(handler apiHandler, w http.ResponseWriter, r *http.Request,
779779
if q.Get("error_code") != "" {
780780
hq.Set("error_code", q.Get("error_code"))
781781
}
782+
// Add Supabase Auth identifier to help clients distinguish Supabase Auth redirects
783+
hq.Set("sb", "")
782784
u.Fragment = hq.Encode()
783785
http.Redirect(w, r, u.String(), http.StatusFound)
784786
}

internal/api/external_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,8 @@ func assertAuthorizationSuccess(ts *ExternalTestSuite, u *url.URL, tokenCount in
207207
ts.NotEmpty(v.Get("refresh_token"))
208208
ts.NotEmpty(v.Get("expires_in"))
209209
ts.Equal("bearer", v.Get("token_type"))
210+
// Verify Supabase Auth identifier is present
211+
ts.Contains(v, "sb", "Fragment should contain Supabase Auth identifier 'sb'")
210212

211213
ts.Equal(1, tokenCount)
212214
if userCount > -1 {
@@ -248,6 +250,8 @@ func assertAuthorizationFailure(ts *ExternalTestSuite, u *url.URL, errorDescript
248250
ts.Empty(v.Get("refresh_token"))
249251
ts.Empty(v.Get("expires_in"))
250252
ts.Empty(v.Get("token_type"))
253+
// Verify Supabase Auth identifier is present even in error responses
254+
ts.Contains(v, "sb", "Fragment should contain Supabase Auth identifier 'sb' even in errors")
251255

252256
// ensure user is nil
253257
user, err := models.FindUserByEmailAndAudience(ts.API.db, email, ts.Config.JWT.Aud)

internal/api/verify.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,8 @@ func (a *API) prepErrorRedirectURL(err *HTTPError, r *http.Request, rurl string,
507507
u.RawQuery = q.Encode()
508508
}
509509
// Left as hash fragment to comply with spec.
510+
// Add Supabase Auth identifier to help clients distinguish Supabase Auth redirects
511+
hq.Set("sb", "")
510512
u.Fragment = hq.Encode()
511513
return u.String(), nil
512514
}
@@ -523,6 +525,8 @@ func (a *API) prepRedirectURL(message string, rurl string, flowType models.FlowT
523525
q.Set("message", message)
524526
}
525527
u.RawQuery = q.Encode()
528+
// Add Supabase Auth identifier to help clients distinguish Supabase Auth redirects
529+
hq.Set("sb", "")
526530
u.Fragment = hq.Encode()
527531
return u.String(), nil
528532
}

internal/api/verify_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,28 +1155,28 @@ func (ts *VerifyTestSuite) TestPrepRedirectURL() {
11551155
message: singleConfirmationAccepted,
11561156
rurl: "https://example.com/?first=another&second=other",
11571157
flowType: models.PKCEFlow,
1158-
expected: fmt.Sprintf("https://example.com/?first=another&message=%s&second=other#message=%s", escapedMessage, escapedMessage),
1158+
expected: fmt.Sprintf("https://example.com/?first=another&message=%s&second=other#message=%s&sb=", escapedMessage, escapedMessage),
11591159
},
11601160
{
11611161
desc: "(PKCE): Query params in redirect url are overriden",
11621162
message: singleConfirmationAccepted,
11631163
rurl: "https://example.com/?message=Valid+redirect+URL",
11641164
flowType: models.PKCEFlow,
1165-
expected: fmt.Sprintf("https://example.com/?message=%s#message=%s", escapedMessage, escapedMessage),
1165+
expected: fmt.Sprintf("https://example.com/?message=%s#message=%s&sb=", escapedMessage, escapedMessage),
11661166
},
11671167
{
11681168
desc: "(Implicit): plain redirect url",
11691169
message: singleConfirmationAccepted,
11701170
rurl: "https://example.com/",
11711171
flowType: models.ImplicitFlow,
1172-
expected: fmt.Sprintf("https://example.com/#message=%s", escapedMessage),
1172+
expected: fmt.Sprintf("https://example.com/#message=%s&sb=", escapedMessage),
11731173
},
11741174
{
11751175
desc: "(Implicit): query params retained",
11761176
message: singleConfirmationAccepted,
11771177
rurl: "https://example.com/?first=another",
11781178
flowType: models.ImplicitFlow,
1179-
expected: fmt.Sprintf("https://example.com/?first=another#message=%s", escapedMessage),
1179+
expected: fmt.Sprintf("https://example.com/?first=another#message=%s&sb=", escapedMessage),
11801180
},
11811181
}
11821182
for _, c := range cases {
@@ -1204,28 +1204,28 @@ func (ts *VerifyTestSuite) TestPrepErrorRedirectURL() {
12041204
message: "Valid redirect URL",
12051205
rurl: "https://example.com/",
12061206
flowType: models.PKCEFlow,
1207-
expected: fmt.Sprintf("https://example.com/?%s#%s", redirectError, redirectError),
1207+
expected: fmt.Sprintf("https://example.com/?%s#%s&sb=", redirectError, redirectError),
12081208
},
12091209
{
12101210
desc: "(PKCE): Error with conflicting query params in redirect url",
12111211
message: DefaultError,
12121212
rurl: "https://example.com/?error=Error+to+be+overriden",
12131213
flowType: models.PKCEFlow,
1214-
expected: fmt.Sprintf("https://example.com/?%s#%s", redirectError, redirectError),
1214+
expected: fmt.Sprintf("https://example.com/?%s#%s&sb=", redirectError, redirectError),
12151215
},
12161216
{
12171217
desc: "(Implicit): plain redirect url",
12181218
message: DefaultError,
12191219
rurl: "https://example.com/",
12201220
flowType: models.ImplicitFlow,
1221-
expected: fmt.Sprintf("https://example.com/#%s", redirectError),
1221+
expected: fmt.Sprintf("https://example.com/#%s&sb=", redirectError),
12221222
},
12231223
{
12241224
desc: "(Implicit): query params preserved",
12251225
message: DefaultError,
12261226
rurl: "https://example.com/?test=param",
12271227
flowType: models.ImplicitFlow,
1228-
expected: fmt.Sprintf("https://example.com/?test=param#%s", redirectError),
1228+
expected: fmt.Sprintf("https://example.com/?test=param#%s&sb=", redirectError),
12291229
},
12301230
}
12311231
for _, c := range cases {

internal/tokens/service.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ func (r *AccessTokenResponse) AsRedirectURL(redirectURL string, extraParams url.
145145
extraParams.Set("expires_in", strconv.Itoa(r.ExpiresIn))
146146
extraParams.Set("expires_at", strconv.FormatInt(r.ExpiresAt, 10))
147147
extraParams.Set("refresh_token", r.RefreshToken)
148+
// Add Supabase Auth identifier to help clients distinguish Supabase Auth redirects
149+
extraParams.Set("sb", "")
148150

149151
return redirectURL + "#" + extraParams.Encode()
150152
}

internal/tokens/service_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/base64"
77
"encoding/json"
88
"net/http"
9+
"net/url"
910
"strconv"
1011
"strings"
1112
"sync"
@@ -1089,3 +1090,39 @@ func TestAMRClaimUnmarshal(t *testing.T) {
10891090
require.Equal(t, "webauthn", claim[1].Provider, "provider should be preserved")
10901091
})
10911092
}
1093+
1094+
// TestAsRedirectURL tests that AsRedirectURL includes the Supabase Auth identifier
1095+
func TestAsRedirectURL(t *testing.T) {
1096+
response := &AccessTokenResponse{
1097+
Token: "test_access_token",
1098+
TokenType: "bearer",
1099+
ExpiresIn: 3600,
1100+
ExpiresAt: 1234567890,
1101+
RefreshToken: "test_refresh_token",
1102+
}
1103+
1104+
extraParams := url.Values{}
1105+
extraParams.Set("provider_token", "provider_access_token")
1106+
1107+
redirectURL := response.AsRedirectURL("https://example.com/callback", extraParams)
1108+
1109+
// Parse the URL
1110+
u, err := url.Parse(redirectURL)
1111+
require.NoError(t, err)
1112+
1113+
// Parse the fragment
1114+
fragment, err := url.ParseQuery(u.Fragment)
1115+
require.NoError(t, err)
1116+
1117+
// Verify all expected parameters are present
1118+
require.Equal(t, "test_access_token", fragment.Get("access_token"))
1119+
require.Equal(t, "bearer", fragment.Get("token_type"))
1120+
require.Equal(t, "3600", fragment.Get("expires_in"))
1121+
require.Equal(t, "1234567890", fragment.Get("expires_at"))
1122+
require.Equal(t, "test_refresh_token", fragment.Get("refresh_token"))
1123+
require.Equal(t, "provider_access_token", fragment.Get("provider_token"))
1124+
1125+
// Verify Supabase Auth identifier is present
1126+
require.Contains(t, fragment, "sb", "Fragment should contain Supabase Auth identifier 'sb'")
1127+
require.Equal(t, "", fragment.Get("sb"), "Supabase Auth identifier should have empty value")
1128+
}

0 commit comments

Comments
 (0)