feat: vendor gitea 1.16.2

This commit is contained in:
2026-06-02 08:36:01 +00:00
parent 247cd60f69
commit 54798b4615
2145 changed files with 162965 additions and 126447 deletions
@@ -5,11 +5,15 @@ package integration
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"fmt"
"image"
"image/png"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
@@ -22,17 +26,64 @@ import (
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth/source/oauth2"
"code.gitea.io/gitea/services/oauth2_provider"
"code.gitea.io/gitea/tests"
"github.com/PuerkitoBio/goquery"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func createOAuthTestApplication(t *testing.T, userName, name string, redirectURIs []string) *api.OAuth2Application {
t.Helper()
req := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &api.CreateOAuth2ApplicationOptions{
Name: name,
RedirectURIs: redirectURIs,
ConfidentialClient: true,
}).AddBasicAuth(userName)
resp := MakeRequest(t, req, http.StatusCreated)
created := DecodeJSON(t, resp, &api.OAuth2Application{})
require.NotEmpty(t, created.ClientID)
require.NotEmpty(t, created.ClientSecret)
return created
}
func issueOAuthAuthorizationCode(t *testing.T, user *user_model.User, app *api.OAuth2Application, redirectURI, scope string) (string, string) {
t.Helper()
grant := &auth_model.OAuth2Grant{
ApplicationID: app.ID,
UserID: user.ID,
Scope: scope,
}
require.NoError(t, db.Insert(t.Context(), grant))
r1, err := util.CryptoRandomBytes(12)
require.NoError(t, err)
verifier := "phase3-verifier-" + base64.RawURLEncoding.EncodeToString(r1)
challengeBytes := sha256.Sum256([]byte(verifier))
r2, err := util.CryptoRandomBytes(10)
require.NoError(t, err)
code := "phase3-code-" + base64.RawURLEncoding.EncodeToString(r2)
require.NoError(t, db.Insert(t.Context(), &auth_model.OAuth2AuthorizationCode{
GrantID: grant.ID,
Code: code,
CodeChallenge: base64.RawURLEncoding.EncodeToString(challengeBytes[:]),
CodeChallengeMethod: "S256",
RedirectURI: redirectURI,
ValidUntil: timeutil.TimeStampNow() + 86400,
}))
return code, verifier
}
func TestOAuth2Provider(t *testing.T) {
defer tests.PrepareTestEnv(t)()
@@ -42,7 +93,12 @@ func TestOAuth2Provider(t *testing.T) {
t.Run("AuthorizeUnsupportedCodeChallengeMethod", testAuthorizeUnsupportedCodeChallengeMethod)
t.Run("AuthorizeLoginRedirect", testAuthorizeLoginRedirect)
t.Run("AccessTokenExchangeRedirectURIMismatch", testAccessTokenExchangeRedirectURIMismatch)
t.Run("RefreshTokenCrossClientUsage", testRefreshTokenCrossClientUsage)
t.Run("OAuth2WellKnown", testOAuth2WellKnown)
t.Run("OAuthSourceSpecialChars", testOAuthSourceSpecialChars)
// TODO: move more tests as sub-tests here, avoid unnecessary PrepareTestEnv
}
func testAuthorizeNoClientID(t *testing.T) {
@@ -92,7 +148,44 @@ func TestAuthorizeShow(t *testing.T) {
htmlDoc := NewHTMLParser(t, resp.Body)
AssertHTMLElement(t, htmlDoc, "#authorize-app", true)
htmlDoc.GetCSRF()
}
func TestAuthorizeGrantS256RequiresVerifier(t *testing.T) {
defer tests.PrepareTestEnv(t)()
ctx := loginUser(t, "user4")
codeChallenge := "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg"
req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=a&response_type=code&state=thestate&code_challenge_method=S256&code_challenge="+url.QueryEscape(codeChallenge))
resp := ctx.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
AssertHTMLElement(t, htmlDoc, "#authorize-app", true)
grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"state": "thestate",
"scope": "",
"nonce": "",
"redirect_uri": "a",
"granted": "true",
})
grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther)
u, err := grantResp.Result().Location()
assert.NoError(t, err)
code := u.Query().Get("code")
assert.NotEmpty(t, code)
accessReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a",
"code": code,
})
accessResp := MakeRequest(t, accessReq, http.StatusBadRequest)
parsedError := new(oauth2_provider.AccessTokenError)
assert.NoError(t, json.Unmarshal(accessResp.Body.Bytes(), parsedError))
assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
assert.Equal(t, "failed PKCE code challenge", parsedError.ErrorDescription)
}
func TestAuthorizeRedirectWithExistingGrant(t *testing.T) {
@@ -143,12 +236,43 @@ func TestAccessTokenExchange(t *testing.T) {
assert.Greater(t, len(parsed.RefreshToken), 10)
}
func testAccessTokenExchangeRedirectURIMismatch(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
redirectURIs := []string{"https://phase3.example/callback", "https://phase3.example/callback-alt"}
app := createOAuthTestApplication(t, user.Name, "phase3-redirect-uri-guard", redirectURIs)
code, verifier := issueOAuthAuthorizationCode(t, user, app, redirectURIs[0], "openid profile")
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"client_id": app.ClientID,
"client_secret": app.ClientSecret,
"redirect_uri": redirectURIs[1],
"code": code,
"code_verifier": verifier,
})
resp := MakeRequest(t, req, http.StatusBadRequest)
parsedError := new(oauth2_provider.AccessTokenError)
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "invalid_grant", string(parsedError.ErrorCode))
assert.Equal(t, "redirect_uri differs from the original authorization request", parsedError.ErrorDescription)
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"client_id": app.ClientID,
"client_secret": app.ClientSecret,
"redirect_uri": redirectURIs[0],
"code": code,
"code_verifier": verifier,
})
MakeRequest(t, req, http.StatusOK)
}
func TestAccessTokenExchangeWithPublicClient(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"client_id": "ce5a1322-42a7-11ed-b878-0242ac120002",
"redirect_uri": "http://127.0.0.1",
"redirect_uri": "http://127.0.0.1/",
"code": "authcodepublic",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
})
@@ -443,6 +567,54 @@ func TestRefreshTokenInvalidation(t *testing.T) {
assert.Equal(t, "token was already used", parsedError.ErrorDescription)
}
func testRefreshTokenCrossClientUsage(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
primaryApp := createOAuthTestApplication(t, user.Name, "phase3-refresh-token-primary", []string{"https://phase3.example/refresh-primary"})
secondaryApp := createOAuthTestApplication(t, user.Name, "refresh-token-client-guard", []string{"https://alt-client.example/oauth/callback"})
code, verifier := issueOAuthAuthorizationCode(t, user, primaryApp, primaryApp.RedirectURIs[0], "openid profile")
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"client_id": primaryApp.ClientID,
"client_secret": primaryApp.ClientSecret,
"redirect_uri": primaryApp.RedirectURIs[0],
"code": code,
"code_verifier": verifier,
})
resp := MakeRequest(t, req, http.StatusOK)
type response struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
}
parsed := new(response)
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
assert.NotEmpty(t, parsed.RefreshToken)
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "refresh_token",
"client_id": secondaryApp.ClientID,
"client_secret": secondaryApp.ClientSecret,
"redirect_uri": secondaryApp.RedirectURIs[0],
"refresh_token": parsed.RefreshToken,
})
resp = MakeRequest(t, req, http.StatusBadRequest)
parsedError := new(oauth2_provider.AccessTokenError)
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "invalid_grant", string(parsedError.ErrorCode))
assert.Equal(t, "refresh token belongs to a different client", parsedError.ErrorDescription)
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "refresh_token",
"client_id": primaryApp.ClientID,
"client_secret": primaryApp.ClientSecret,
"redirect_uri": primaryApp.RedirectURIs[0],
"refresh_token": parsed.RefreshToken,
})
MakeRequest(t, req, http.StatusOK)
}
func TestOAuthIntrospection(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
@@ -961,12 +1133,17 @@ func addOAuth2Source(t *testing.T, authName string, cfg oauth2.Source) {
require.NoError(t, err)
}
func TestSignInOauthCallbackSyncSSHKeys(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func createOAuth2MockProvider() *httptest.Server {
var mockServer *httptest.Server
mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/avatar.png":
if !strings.HasPrefix(r.Header.Get("User-Agent"), "Gitea ") {
http.Error(w, "user agent doesn't match", http.StatusForbidden)
return
}
w.Header().Set("Content-Type", "image/png")
_ = png.Encode(w, image.NewRGBA(image.Rect(0, 0, 8, 8)))
case "/.well-known/openid-configuration":
_, _ = w.Write([]byte(`{
"issuer": "` + mockServer.URL + `",
@@ -978,6 +1155,14 @@ func TestSignInOauthCallbackSyncSSHKeys(t *testing.T) {
http.NotFound(w, r)
}
}))
return mockServer
}
func TestSignInOauthCallbackSyncSSHKeys(t *testing.T) {
defer tests.PrepareTestEnv(t)()
mockServer := createOAuth2MockProvider()
defer mockServer.Close()
ctx := t.Context()
@@ -1053,3 +1238,47 @@ func TestSignInOauthCallbackSyncSSHKeys(t *testing.T) {
})
}
}
// Checks if an OAuth provider with spaces within the name does work,
// with the encoding of its names in the URL (PR#37327)
func testOAuthSourceSpecialChars(t *testing.T) {
mockServer := createOAuth2MockProvider()
defer mockServer.Close()
addOAuth2Source(t, "test space", oauth2.Source{
Provider: "openidConnect",
OpenIDConnectAutoDiscoveryURL: mockServer.URL + "/.well-known/openid-configuration",
})
addOAuth2Source(t, "test+plus", oauth2.Source{
Provider: "openidConnect",
OpenIDConnectAutoDiscoveryURL: mockServer.URL + "/.well-known/openid-configuration",
})
testOAuth2 := func(t *testing.T, uri string, statusCode int) {
req := NewRequest(t, "GET", uri)
resp := MakeRequest(t, req, statusCode)
if statusCode == http.StatusTemporaryRedirect {
assert.NotEmpty(t, resp.Header().Get("Location"))
} else {
assert.Empty(t, resp.Header().Get("Location"))
}
}
req := MakeRequest(t, NewRequest(t, "GET", "/user/login"), http.StatusOK)
doc := NewHTMLParser(t, req.Body)
var oauth2Links []string
doc.Find(".external-login-link").Each(func(i int, s *goquery.Selection) {
oauth2Links = append(oauth2Links, s.AttrOr("href", ""))
})
assert.Equal(t, []string{
"/user/oauth2/test%20space",
"/user/oauth2/test+plus",
}, oauth2Links)
testOAuth2(t, "/user/oauth2/test%20space", http.StatusTemporaryRedirect)
testOAuth2(t, "/user/oauth2/test+space", http.StatusNotFound)
testOAuth2(t, "/user/oauth2/test+plus", http.StatusTemporaryRedirect)
testOAuth2(t, "/user/oauth2/test%2Bplus", http.StatusTemporaryRedirect)
testOAuth2(t, "/user/oauth2/test%20plus", http.StatusNotFound)
}