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
4 changes: 2 additions & 2 deletions internal/api/oauthserver/authorize.go
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ func (s *Server) buildSuccessRedirectURL(authorization *models.OAuthServerAuthor
q.Set("state", *authorization.State)
}
u.RawQuery = q.Encode()
return u.String()
return utilities.PreserveEmptyAuthority(authorization.RedirectURI, u)
}

// buildErrorRedirectURL builds an error redirect URL with the given parameters
Expand All @@ -619,7 +619,7 @@ func (s *Server) buildErrorRedirectURL(redirectURI, errorCode, errorDescription,
q.Set("state", state)
}
u.RawQuery = q.Encode()
return u.String()
return utilities.PreserveEmptyAuthority(redirectURI, u)
}

// buildAuthorizationURL safely joins a base URL with a path, handling slashes correctly
Expand Down
6 changes: 3 additions & 3 deletions internal/api/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,7 @@ func (a *API) prepErrorRedirectURL(err *HTTPError, r *http.Request, rurl string,
// Add Supabase Auth identifier to help clients distinguish Supabase Auth redirects
hq.Set("sb", "")
u.Fragment = hq.Encode()
return u.String(), nil
return utilities.PreserveEmptyAuthority(rurl, u), nil
}

func (a *API) prepRedirectURL(message string, rurl string, flowType models.FlowType) (string, error) {
Expand All @@ -528,7 +528,7 @@ func (a *API) prepRedirectURL(message string, rurl string, flowType models.FlowT
// Add Supabase Auth identifier to help clients distinguish Supabase Auth redirects
hq.Set("sb", "")
u.Fragment = hq.Encode()
return u.String(), nil
return utilities.PreserveEmptyAuthority(rurl, u), nil
}

func (a *API) prepPKCERedirectURL(rurl, code string) (string, error) {
Expand All @@ -539,7 +539,7 @@ func (a *API) prepPKCERedirectURL(rurl, code string) (string, error) {
q := u.Query()
q.Set("code", code)
u.RawQuery = q.Encode()
return u.String(), nil
return utilities.PreserveEmptyAuthority(rurl, u), nil
}

func (a *API) emailChangeVerify(r *http.Request, conn *storage.Connection, params *VerifyParams, user *models.User) (*models.User, error) {
Expand Down
18 changes: 18 additions & 0 deletions internal/utilities/url.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package utilities

import (
"net/url"
"strings"
)

// PreserveEmptyAuthority restores the `//` in URLs that use "scheme://" with
// an empty authority (e.g. custom-scheme deep links like "myapp://"). Go's
// net/url package normalizes these to "scheme:" on String(), which breaks
// iOS/Android deep-link clients that register for the "scheme://" form.
func PreserveEmptyAuthority(rurl string, u *url.URL) string {
formatted := u.String()
if u.Scheme != "" && u.Host == "" && u.Path == "" && strings.HasPrefix(rurl, u.Scheme+"://") {
return strings.Replace(formatted, u.Scheme+":", u.Scheme+"://", 1)
}
return formatted
}
44 changes: 44 additions & 0 deletions internal/utilities/url_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package utilities

import (
"net/url"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestPreserveEmptyAuthority(t *testing.T) {
cases := []struct {
name string
in string
want string
}{
{"custom scheme empty authority", "myapp://", "myapp://?code=ABC"},
{"custom scheme with host", "myapp://host", "myapp://host?code=ABC"},
{"scheme only no slashes", "myapp:", "myapp:?code=ABC"},
{"https", "https://example.com", "https://example.com?code=ABC"},
{"reverse dns empty authority", "com.example.app://", "com.example.app://?code=ABC"},
{"reverse dns with path", "com.example.app://callback", "com.example.app://callback?code=ABC"},
{"triple slash", "myapp:///callback", "myapp:///callback?code=ABC"},
{"host port path", "myapp://host:1234/callback", "myapp://host:1234/callback?code=ABC"},
{"existing query", "myapp://?x=1", "myapp://?code=ABC&x=1"},
{"existing code param overwrites", "myapp://?code=OLD", "myapp://?code=ABC"},
{"fragment", "myapp://#frag", "myapp://?code=ABC#frag"},
// net/url lowercases the scheme per RFC 3986 §3.1. iOS URL-scheme
// matching is case-sensitive in practice, so mixed-case schemes are
// normalized to lowercase by the time they reach the client.
{"mixed case scheme", "MyApp://callback", "myapp://callback?code=ABC"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
u, err := url.Parse(c.in)
require.NoError(t, err)
q := u.Query()
q.Set("code", "ABC")
u.RawQuery = q.Encode()
got := PreserveEmptyAuthority(c.in, u)
assert.Equal(t, c.want, got)
})
}
}