Fix URL related escaping for oauth2 (#37334)

Follow up #37327. See the comments.

* Root problem: the design of OAuth2 providers is a mess, the display
name is used as provider's name and used in the URL directly
* The regressions:
* When trying to fix https://github.com/go-gitea/gitea/issues/36409 , it
introduced inconsistent URL escaping for the "path" part.
* This fix: always use "path escaping" for the path part, add more tests
to cover all escaping cases.

Now, frontend "pathEscape" and "pathEscapeSegments" generate exactly the
same result as backend.
This commit is contained in:
wxiaoguang
2026-04-21 23:58:32 +08:00
committed by GitHub
parent 5495b5d126
commit aee6628bf5
16 changed files with 135 additions and 60 deletions

View File

@@ -230,7 +230,7 @@ func performAutoLoginOAuth2(ctx *context.Context, data *preparedSignInData) bool
return false
}
skipToOAuthURL := setting.AppSubURL + "/user/oauth2/" + url.QueryEscape(data.oauth2Providers[0].DisplayName())
skipToOAuthURL := setting.AppSubURL + "/user/oauth2/" + url.PathEscape(data.oauth2Providers[0].DisplayName())
if redirectTo := ctx.FormString("redirect_to"); redirectTo != "" {
skipToOAuthURL += "?redirect_to=" + url.QueryEscape(redirectTo)
}

View File

@@ -4,6 +4,7 @@
package auth
import (
"html/template"
"net/http"
"net/http/httptest"
"net/url"
@@ -67,15 +68,15 @@ func TestWebAuthOAuth2(t *testing.T) {
defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)()
_ = oauth2.Init(t.Context())
addOAuth2Source(t, "dummy-auth-source", oauth2.Source{})
addOAuth2Source(t, "dummy+auth's source", oauth2.Source{})
t.Run("OAuth2MissingField", func(t *testing.T) {
defer test.MockVariableValue(&gothic.CompleteUserAuth, func(res http.ResponseWriter, req *http.Request) (goth.User, error) {
return goth.User{Provider: "dummy-auth-source", UserID: "dummy-user"}, nil
return goth.User{Provider: "dummy+auth's source", UserID: "dummy-user"}, nil
})()
mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockMemStore("dummy-sid")}
ctx, resp := contexttest.MockContext(t, "/user/oauth2/dummy-auth-source/callback?code=dummy-code", mockOpt)
ctx.SetPathParam("provider", "dummy-auth-source")
ctx, resp := contexttest.MockContext(t, "/user/oauth2/..../callback?code=dummy-code", mockOpt)
ctx.SetPathParamRaw("provider", "dummy+auth%27s%20source")
SignInOAuthCallback(ctx)
assert.Equal(t, http.StatusSeeOther, resp.Code)
assert.Equal(t, "/user/link_account", test.RedirectURL(resp))
@@ -83,13 +84,13 @@ func TestWebAuthOAuth2(t *testing.T) {
// then the user will be redirected to the link account page, and see a message about the missing fields
ctx, _ = contexttest.MockContext(t, "/user/link_account", mockOpt)
LinkAccount(ctx)
assert.EqualValues(t, "auth.oauth_callback_unable_auto_reg:dummy-auth-source,email", ctx.Data["AutoRegistrationFailedPrompt"])
assert.Equal(t, template.HTML("auth.oauth_callback_unable_auto_reg:dummy+auth's source,email"), ctx.Data["AutoRegistrationFailedPrompt"])
})
t.Run("OAuth2CallbackError", func(t *testing.T) {
mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockMemStore("dummy-sid")}
ctx, resp := contexttest.MockContext(t, "/user/oauth2/dummy-auth-source/callback", mockOpt)
ctx.SetPathParam("provider", "dummy-auth-source")
ctx, resp := contexttest.MockContext(t, "/user/oauth2/...../callback", mockOpt)
ctx.SetPathParamRaw("provider", "dummy+auth%27s%20source")
SignInOAuthCallback(ctx)
assert.Equal(t, http.StatusSeeOther, resp.Code)
assert.Equal(t, "/user/login", test.RedirectURL(resp))
@@ -112,8 +113,8 @@ func TestWebAuthOAuth2(t *testing.T) {
assert.Equal(t, expectedRedirect, test.RedirectURL(resp))
}
}
testSignIn(t, "/user/login", http.StatusSeeOther, "/user/oauth2/dummy-auth-source")
testSignIn(t, "/user/login?redirect_to=/", http.StatusSeeOther, "/user/oauth2/dummy-auth-source?redirect_to=%2F")
testSignIn(t, "/user/login", http.StatusSeeOther, "/user/oauth2/dummy+auth%27s%20source")
testSignIn(t, "/user/login?redirect_to=/", http.StatusSeeOther, "/user/oauth2/dummy+auth%27s%20source?redirect_to=%2F")
*enablePassword, *enableOpenID, *enablePasskey = true, false, false
testSignIn(t, "/user/login", http.StatusOK, "")

View File

@@ -36,9 +36,7 @@ import (
// SignInOAuth handles the OAuth2 login buttons
func SignInOAuth(ctx *context.Context) {
// the provider is escaped by backend QueryEscape and frontend urlQueryEscape
// so always use QueryUnescape to decode it
authName, _ := url.QueryUnescape(ctx.PathParamRaw("provider"))
authName := ctx.PathParam("provider")
authSource, err := auth.GetActiveOAuth2SourceByAuthName(ctx, authName)
if err != nil {
ctx.ServerError("SignIn", err)