Skip to content

Commit 29c7b6f

Browse files
authored
feat: validate redirect URIs and safely append parameters (#4559)
Signed-off-by: maksim.nabokikh <max.nabokih@gmail.com>
1 parent 69f9b7e commit 29c7b6f

3 files changed

Lines changed: 137 additions & 8 deletions

File tree

examples/example-app/main.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,11 +274,21 @@ func (a *app) handleLogin(w http.ResponseWriter, r *http.Request) {
274274
scopes = append(scopes, "offline_access")
275275
}
276276
authCodeURL = a.oauth2Config(scopes).AuthCodeURL(exampleAppState, authCodeOptions...)
277+
278+
// Parse the auth code URL and safely add connector_id parameter if provided
279+
u, err := url.Parse(authCodeURL)
280+
if err != nil {
281+
http.Error(w, "Failed to parse auth URL", http.StatusInternalServerError)
282+
return
283+
}
284+
277285
if connectorID != "" {
278-
authCodeURL = authCodeURL + "&connector_id=" + connectorID
286+
query := u.Query()
287+
query.Set("connector_id", connectorID)
288+
u.RawQuery = query.Encode()
279289
}
280290

281-
http.Redirect(w, r, authCodeURL, http.StatusSeeOther)
291+
http.Redirect(w, r, u.String(), http.StatusSeeOther)
282292
}
283293

284294
func (a *app) handleCallback(w http.ResponseWriter, r *http.Request) {

server/oauth2.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,25 @@ func (err *redirectedAuthErr) Handler() http.Handler {
6464
if err.Description != "" {
6565
v.Add("error_description", err.Description)
6666
}
67-
var redirectURI string
68-
if strings.Contains(err.RedirectURI, "?") {
69-
redirectURI = err.RedirectURI + "&" + v.Encode()
70-
} else {
71-
redirectURI = err.RedirectURI + "?" + v.Encode()
67+
68+
// Parse the redirect URI to ensure it's valid before redirecting
69+
u, parseErr := url.Parse(err.RedirectURI)
70+
if parseErr != nil {
71+
// If URI parsing fails, respond with an error instead of redirecting
72+
http.Error(w, "Invalid redirect URI", http.StatusBadRequest)
73+
return
7274
}
73-
http.Redirect(w, r, redirectURI, http.StatusSeeOther)
75+
76+
// Add error parameters to the URL
77+
query := u.Query()
78+
for key, values := range v {
79+
for _, value := range values {
80+
query.Add(key, value)
81+
}
82+
}
83+
u.RawQuery = query.Encode()
84+
85+
http.Redirect(w, r, u.String(), http.StatusSeeOther)
7486
}
7587
return http.HandlerFunc(hf)
7688
}

server/oauth2_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,3 +686,110 @@ func TestSignerKeySet(t *testing.T) {
686686
})
687687
}
688688
}
689+
690+
func TestRedirectedAuthErrHandler(t *testing.T) {
691+
tests := []struct {
692+
name string
693+
redirectURI string
694+
state string
695+
errType string
696+
description string
697+
wantStatus int
698+
wantErr bool
699+
}{
700+
{
701+
name: "valid redirect uri with error parameters",
702+
redirectURI: "https://example.com/callback",
703+
state: "state123",
704+
errType: errInvalidRequest,
705+
description: "Invalid request parameter",
706+
wantStatus: http.StatusSeeOther,
707+
wantErr: false,
708+
},
709+
{
710+
name: "valid redirect uri with query params",
711+
redirectURI: "https://example.com/callback?existing=param&another=value",
712+
state: "state456",
713+
errType: errAccessDenied,
714+
description: "User denied access",
715+
wantStatus: http.StatusSeeOther,
716+
wantErr: false,
717+
},
718+
{
719+
name: "valid redirect uri without description",
720+
redirectURI: "https://example.com/callback",
721+
state: "state789",
722+
errType: errServerError,
723+
description: "",
724+
wantStatus: http.StatusSeeOther,
725+
wantErr: false,
726+
},
727+
{
728+
name: "invalid redirect uri",
729+
redirectURI: "not a valid url ://",
730+
state: "state",
731+
errType: errInvalidRequest,
732+
description: "Test error",
733+
wantStatus: http.StatusBadRequest,
734+
wantErr: true,
735+
},
736+
}
737+
738+
for _, tc := range tests {
739+
tc := tc
740+
t.Run(tc.name, func(t *testing.T) {
741+
err := &redirectedAuthErr{
742+
State: tc.state,
743+
RedirectURI: tc.redirectURI,
744+
Type: tc.errType,
745+
Description: tc.description,
746+
}
747+
748+
handler := err.Handler()
749+
w := httptest.NewRecorder()
750+
r := httptest.NewRequest("GET", "/", nil)
751+
752+
handler.ServeHTTP(w, r)
753+
754+
if w.Code != tc.wantStatus {
755+
t.Errorf("expected status %d, got %d", tc.wantStatus, w.Code)
756+
}
757+
758+
if tc.wantStatus == http.StatusSeeOther {
759+
// Verify the redirect location is a valid URL
760+
location := w.Header().Get("Location")
761+
if location == "" {
762+
t.Fatalf("expected Location header, got empty string")
763+
}
764+
765+
// Parse the redirect URL to verify it's valid
766+
redirectURL, parseErr := url.Parse(location)
767+
if parseErr != nil {
768+
t.Fatalf("invalid redirect URL: %v", parseErr)
769+
}
770+
771+
// Verify error parameters are present in the query string
772+
query := redirectURL.Query()
773+
if query.Get("state") != tc.state {
774+
t.Errorf("expected state %q, got %q", tc.state, query.Get("state"))
775+
}
776+
if query.Get("error") != tc.errType {
777+
t.Errorf("expected error type %q, got %q", tc.errType, query.Get("error"))
778+
}
779+
if tc.description != "" && query.Get("error_description") != tc.description {
780+
t.Errorf("expected error_description %q, got %q", tc.description, query.Get("error_description"))
781+
}
782+
783+
// Verify that existing query parameters are preserved
784+
if tc.name == "valid redirect uri with query params" {
785+
if query.Get("existing") != "param" {
786+
t.Errorf("expected existing parameter 'param', got %q", query.Get("existing"))
787+
}
788+
if query.Get("another") != "value" {
789+
t.Errorf("expected another parameter 'value', got %q", query.Get("another"))
790+
}
791+
}
792+
}
793+
})
794+
}
795+
}

0 commit comments

Comments
 (0)