feat: update gitea vendor
runner nix smoke / nix label and flake smoke (push) Failing after 1m28s

This commit is contained in:
2026-09-26 21:18:24 +00:00
parent c439c1b948
commit d9b2a4e787
3538 changed files with 116131 additions and 44340 deletions
@@ -14,31 +14,53 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"testing"
asymkey_model "code.gitea.io/gitea/models/asymkey"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/json"
"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"
asymkey_model "gitea.dev/models/asymkey"
auth_model "gitea.dev/models/auth"
"gitea.dev/models/db"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/json"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/test"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"gitea.dev/services/auth/source/oauth2"
"gitea.dev/services/oauth2_provider"
"gitea.dev/tests"
"github.com/PuerkitoBio/goquery"
jwt "github.com/golang-jwt/jwt/v5"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func testOAuth2PrepareTestCode(t *testing.T) {
require.NoError(t, db.TruncateBeans(t.Context(), &auth_model.OAuth2AuthorizationCode{}))
err := db.Insert(t.Context(), &auth_model.OAuth2AuthorizationCode{
GrantID: 1,
Code: "authcode",
CodeChallenge: "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg", // Code Verifier: N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt
CodeChallengeMethod: "S256",
RedirectURI: "https://example.com",
ValidUntil: timeutil.TimeStampNow() + 86400,
}, &auth_model.OAuth2AuthorizationCode{
GrantID: 4,
Code: "authcodepublic",
CodeChallenge: "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg", //# Code Verifier: N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt
CodeChallengeMethod: "S256",
RedirectURI: "http://127.0.0.1/",
ValidUntil: timeutil.TimeStampNow() + 86400,
})
require.NoError(t, err)
}
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{
@@ -63,14 +85,9 @@ func issueOAuthAuthorizationCode(t *testing.T, user *user_model.User, app *api.O
}
require.NoError(t, db.Insert(t.Context(), grant))
r1, err := util.CryptoRandomBytes(12)
require.NoError(t, err)
verifier := "phase3-verifier-" + base64.RawURLEncoding.EncodeToString(r1)
verifier := "phase3-verifier-" + util.FastCryptoRandomHex(12)
challengeBytes := sha256.Sum256([]byte(verifier))
r2, err := util.CryptoRandomBytes(10)
require.NoError(t, err)
code := "phase3-code-" + base64.RawURLEncoding.EncodeToString(r2)
code := "phase3-code-" + util.FastCryptoRandomHex(10)
require.NoError(t, db.Insert(t.Context(), &auth_model.OAuth2AuthorizationCode{
GrantID: grant.ID,
@@ -84,20 +101,41 @@ func issueOAuthAuthorizationCode(t *testing.T, user *user_model.User, app *api.O
return code, verifier
}
func TestOAuth2Provider(t *testing.T) {
func TestOAuth2(t *testing.T) {
defer tests.PrepareTestEnv(t)()
t.Run("AuthorizeNoClientID", testAuthorizeNoClientID)
t.Run("AuthorizeUnregisteredRedirect", testAuthorizeUnregisteredRedirect)
t.Run("AuthorizeUnsupportedResponseType", testAuthorizeUnsupportedResponseType)
t.Run("AuthorizeUnsupportedCodeChallengeMethod", testAuthorizeUnsupportedCodeChallengeMethod)
t.Run("AuthorizeLoginRedirect", testAuthorizeLoginRedirect)
t.Run("AccessTokenExchangeRedirectURIMismatch", testAccessTokenExchangeRedirectURIMismatch)
t.Run("RefreshTokenCrossClientUsage", testRefreshTokenCrossClientUsage)
t.Run("OAuth2WellKnown", testOAuth2WellKnown)
t.Run("OAuthSourceSpecialChars", testOAuthSourceSpecialChars)
t.Run("Provider", func(t *testing.T) {
t.Run("AuthorizeNoClientID", testAuthorizeNoClientID)
t.Run("AuthorizeUnregisteredRedirect", testAuthorizeUnregisteredRedirect)
t.Run("AuthorizeUnsupportedResponseType", testAuthorizeUnsupportedResponseType)
t.Run("AuthorizeUnsupportedCodeChallengeMethod", testAuthorizeUnsupportedCodeChallengeMethod)
t.Run("AuthorizeLoginRedirect", testAuthorizeLoginRedirect)
t.Run("AuthorizeShow", testAuthorizeShow)
t.Run("AuthorizeGrantS256RequiresVerifier", testAuthorizeGrantS256RequiresVerifier)
t.Run("AuthorizeRedirectWithExistingGrant", testAuthorizeRedirectWithExistingGrant)
t.Run("AuthorizePKCERequiredForPublicClient", testAuthorizePKCERequiredForPublicClient)
t.Run("AccessTokenExchange", testAccessTokenExchange)
t.Run("AccessTokenExchangeRedirectURIMismatch", testAccessTokenExchangeRedirectURIMismatch)
t.Run("AccessTokenExchangeWithPublicClient", testAccessTokenExchangeWithPublicClient)
t.Run("AccessTokenExchangeJSON", testAccessTokenExchangeJSON)
t.Run("AccessTokenExchangeWithoutPKCE", testAccessTokenExchangeWithoutPKCE)
t.Run("AccessTokenExchangeWithInvalidCredentials", testAccessTokenExchangeWithInvalidCredentials)
t.Run("AccessTokenExchangeWithBasicAuth", testAccessTokenExchangeWithBasicAuth)
t.Run("RefreshTokenInvalidation", testRefreshTokenInvalidation)
t.Run("RefreshTokenCrossClientUsage", testRefreshTokenCrossClientUsage)
t.Run("OAuthIntrospection", testOAuthIntrospection)
t.Run("OAuthIntrospectionCrossClientIsolation", testOAuthIntrospectionCrossClientIsolation)
t.Run("OAuthGrantScopesReadUserFailRepos", testOAuthGrantScopesReadUserFailRepos)
t.Run("OAuthGrantScopesBasicRespectsWriteUser", testOAuthGrantScopesBasicRespectsWriteUser)
t.Run("OAuthGrantScopesReadRepositoryFailOrganization", testOAuthGrantScopesReadRepositoryFailOrganization)
t.Run("OAuthGrantScopesClaimPublicOnlyGroups", testOAuthGrantScopesClaimPublicOnlyGroups)
t.Run("OAuthGrantScopesClaimAllGroups", testOAuthGrantScopesClaimAllGroups)
t.Run("OAuth2WellKnown", testOAuth2WellKnown)
})
t.Run("Client", func(t *testing.T) {
t.Run("OAuthSourceSpecialChars", testOAuthSourceSpecialChars)
t.Run("SignInOauthCallbackSyncSSHKeys", testSignInOauthCallbackSyncSSHKeys)
})
// TODO: move more tests as sub-tests here, avoid unnecessary PrepareTestEnv
}
@@ -116,7 +154,7 @@ func testAuthorizeUnregisteredRedirect(t *testing.T) {
}
func testAuthorizeUnsupportedResponseType(t *testing.T) {
req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=a&response_type=UNEXPECTED&state=thestate")
req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=https://example.com&response_type=UNEXPECTED&state=thestate")
ctx := loginUser(t, "user1")
resp := ctx.MakeRequest(t, req, http.StatusSeeOther)
u, err := resp.Result().Location()
@@ -126,7 +164,7 @@ func testAuthorizeUnsupportedResponseType(t *testing.T) {
}
func testAuthorizeUnsupportedCodeChallengeMethod(t *testing.T) {
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=UNEXPECTED")
req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=https://example.com&response_type=code&state=thestate&code_challenge_method=UNEXPECTED")
ctx := loginUser(t, "user1")
resp := ctx.MakeRequest(t, req, http.StatusSeeOther)
u, err := resp.Result().Location()
@@ -140,9 +178,8 @@ func testAuthorizeLoginRedirect(t *testing.T) {
assert.Contains(t, MakeRequest(t, req, http.StatusSeeOther).Body.String(), "/user/login")
}
func TestAuthorizeShow(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=a&response_type=code&state=thestate")
func testAuthorizeShow(t *testing.T) {
req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=https://example.com&response_type=code&state=thestate")
ctx := loginUser(t, "user4")
resp := ctx.MakeRequest(t, req, http.StatusOK)
@@ -150,11 +187,10 @@ func TestAuthorizeShow(t *testing.T) {
AssertHTMLElement(t, htmlDoc, "#authorize-app", true)
}
func TestAuthorizeGrantS256RequiresVerifier(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAuthorizeGrantS256RequiresVerifier(t *testing.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))
req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=https://example.com&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)
@@ -165,7 +201,7 @@ func TestAuthorizeGrantS256RequiresVerifier(t *testing.T) {
"state": "thestate",
"scope": "",
"nonce": "",
"redirect_uri": "a",
"redirect_uri": "https://example.com",
"granted": "true",
})
grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther)
@@ -178,7 +214,7 @@ func TestAuthorizeGrantS256RequiresVerifier(t *testing.T) {
"grant_type": "authorization_code",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a",
"redirect_uri": "https://example.com",
"code": code,
})
accessResp := MakeRequest(t, accessReq, http.StatusBadRequest)
@@ -188,9 +224,8 @@ func TestAuthorizeGrantS256RequiresVerifier(t *testing.T) {
assert.Equal(t, "failed PKCE code challenge", parsedError.ErrorDescription)
}
func TestAuthorizeRedirectWithExistingGrant(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=https%3A%2F%2Fexample.com%2Fxyzzy&response_type=code&state=thestate")
func testAuthorizeRedirectWithExistingGrant(t *testing.T) {
req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=https://example.com/&response_type=code&state=thestate")
ctx := loginUser(t, "user1")
resp := ctx.MakeRequest(t, req, http.StatusSeeOther)
u, err := resp.Result().Location()
@@ -198,11 +233,11 @@ func TestAuthorizeRedirectWithExistingGrant(t *testing.T) {
assert.Equal(t, "thestate", u.Query().Get("state"))
assert.Greaterf(t, len(u.Query().Get("code")), 30, "authorization code '%s' should be longer then 30", u.Query().Get("code"))
u.RawQuery = ""
assert.Equal(t, "https://example.com/xyzzy", u.String())
assert.Equal(t, "https://example.com/", u.String())
}
func TestAuthorizePKCERequiredForPublicClient(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAuthorizePKCERequiredForPublicClient(t *testing.T) {
testOAuth2PrepareTestCode(t)
req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=ce5a1322-42a7-11ed-b878-0242ac120002&redirect_uri=http%3A%2F%2F127.0.0.1&response_type=code&state=thestate")
ctx := loginUser(t, "user1")
resp := ctx.MakeRequest(t, req, http.StatusSeeOther)
@@ -212,13 +247,13 @@ func TestAuthorizePKCERequiredForPublicClient(t *testing.T) {
assert.Equal(t, "PKCE is required for public clients", u.Query().Get("error_description"))
}
func TestAccessTokenExchange(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAccessTokenExchange(t *testing.T) {
testOAuth2PrepareTestCode(t)
req := 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",
"redirect_uri": "https://example.com",
"code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
})
@@ -267,8 +302,8 @@ func testAccessTokenExchangeRedirectURIMismatch(t *testing.T) {
MakeRequest(t, req, http.StatusOK)
}
func TestAccessTokenExchangeWithPublicClient(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAccessTokenExchangeWithPublicClient(t *testing.T) {
testOAuth2PrepareTestCode(t)
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"client_id": "ce5a1322-42a7-11ed-b878-0242ac120002",
@@ -290,13 +325,13 @@ func TestAccessTokenExchangeWithPublicClient(t *testing.T) {
assert.Greater(t, len(parsed.RefreshToken), 10)
}
func TestAccessTokenExchangeJSON(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAccessTokenExchangeJSON(t *testing.T) {
testOAuth2PrepareTestCode(t)
req := NewRequestWithJSON(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",
"redirect_uri": "https://example.com",
"code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
})
@@ -314,13 +349,13 @@ func TestAccessTokenExchangeJSON(t *testing.T) {
assert.Greater(t, len(parsed.RefreshToken), 10)
}
func TestAccessTokenExchangeWithoutPKCE(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAccessTokenExchangeWithoutPKCE(t *testing.T) {
testOAuth2PrepareTestCode(t)
req := 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",
"redirect_uri": "https://example.com",
"code": "authcode",
})
resp := MakeRequest(t, req, http.StatusBadRequest)
@@ -330,14 +365,14 @@ func TestAccessTokenExchangeWithoutPKCE(t *testing.T) {
assert.Equal(t, "failed PKCE code challenge", parsedError.ErrorDescription)
}
func TestAccessTokenExchangeWithInvalidCredentials(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAccessTokenExchangeWithInvalidCredentials(t *testing.T) {
testOAuth2PrepareTestCode(t)
// invalid client id
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"client_id": "???",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a",
"redirect_uri": "https://example.com",
"code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
})
@@ -352,7 +387,7 @@ func TestAccessTokenExchangeWithInvalidCredentials(t *testing.T) {
"grant_type": "authorization_code",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "???",
"redirect_uri": "a",
"redirect_uri": "https://example.com",
"code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
})
@@ -382,7 +417,7 @@ func TestAccessTokenExchangeWithInvalidCredentials(t *testing.T) {
"grant_type": "authorization_code",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a",
"redirect_uri": "https://example.com",
"code": "???",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
})
@@ -397,7 +432,7 @@ func TestAccessTokenExchangeWithInvalidCredentials(t *testing.T) {
"grant_type": "???",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a",
"redirect_uri": "https://example.com",
"code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
})
@@ -408,11 +443,11 @@ func TestAccessTokenExchangeWithInvalidCredentials(t *testing.T) {
assert.Equal(t, "Only refresh_token or authorization_code grant type is supported", parsedError.ErrorDescription)
}
func TestAccessTokenExchangeWithBasicAuth(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAccessTokenExchangeWithBasicAuth(t *testing.T) {
testOAuth2PrepareTestCode(t)
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"redirect_uri": "a",
"redirect_uri": "https://example.com",
"code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
})
@@ -433,7 +468,7 @@ func TestAccessTokenExchangeWithBasicAuth(t *testing.T) {
// use wrong client_secret
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"redirect_uri": "a",
"redirect_uri": "https://example.com",
"code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
})
@@ -447,7 +482,7 @@ func TestAccessTokenExchangeWithBasicAuth(t *testing.T) {
// missing header
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"redirect_uri": "a",
"redirect_uri": "https://example.com",
"code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
})
@@ -460,7 +495,7 @@ func TestAccessTokenExchangeWithBasicAuth(t *testing.T) {
// client_id inconsistent with Authorization header
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"redirect_uri": "a",
"redirect_uri": "https://example.com",
"code": "authcode",
"client_id": "inconsistent",
})
@@ -474,7 +509,7 @@ func TestAccessTokenExchangeWithBasicAuth(t *testing.T) {
// client_secret inconsistent with Authorization header
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"redirect_uri": "a",
"redirect_uri": "https://example.com",
"code": "authcode",
"client_secret": "inconsistent",
})
@@ -486,13 +521,13 @@ func TestAccessTokenExchangeWithBasicAuth(t *testing.T) {
assert.Equal(t, "client_secret in request body inconsistent with Authorization header", parsedError.ErrorDescription)
}
func TestRefreshTokenInvalidation(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testRefreshTokenInvalidation(t *testing.T) {
testOAuth2PrepareTestCode(t)
req := 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",
"redirect_uri": "https://example.com",
"code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
})
@@ -514,7 +549,7 @@ func TestRefreshTokenInvalidation(t *testing.T) {
"grant_type": "refresh_token",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
// omit secret
"redirect_uri": "a",
"redirect_uri": "https://example.com",
"refresh_token": parsed.RefreshToken,
})
resp = MakeRequest(t, req, http.StatusBadRequest)
@@ -527,7 +562,7 @@ func TestRefreshTokenInvalidation(t *testing.T) {
"grant_type": "refresh_token",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a",
"redirect_uri": "https://example.com",
"refresh_token": "UNEXPECTED",
})
resp = MakeRequest(t, req, http.StatusBadRequest)
@@ -540,7 +575,7 @@ func TestRefreshTokenInvalidation(t *testing.T) {
"grant_type": "refresh_token",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a",
"redirect_uri": "https://example.com",
"refresh_token": parsed.RefreshToken,
})
@@ -615,13 +650,13 @@ func testRefreshTokenCrossClientUsage(t *testing.T) {
MakeRequest(t, req, http.StatusOK)
}
func TestOAuthIntrospection(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testOAuthIntrospection(t *testing.T) {
testOAuth2PrepareTestCode(t)
req := 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",
"redirect_uri": "https://example.com",
"code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
})
@@ -673,63 +708,84 @@ func TestOAuthIntrospection(t *testing.T) {
assert.Contains(t, resp.Body.String(), "no valid authorization")
}
func TestOAuth_GrantScopesReadUserFailRepos(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testOAuthIntrospectionCrossClientIsolation(t *testing.T) {
resourceOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
clientA := createOAuthTestApplication(t, "user1", "introspection-primary-client", []string{"https://primary.example/oauth/callback"})
clientB := createOAuthTestApplication(t, "user2", "introspection-secondary-client", []string{"https://secondary.example/oauth/callback"})
code, verifier := issueOAuthAuthorizationCode(t, resourceOwner, clientA, clientA.RedirectURIs[0], "openid profile")
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
appBody := api.CreateOAuth2ApplicationOptions{
Name: "oauth-provider-scopes-test",
RedirectURIs: []string{
"a",
},
ConfidentialClient: true,
}
req := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &appBody).
AddBasicAuth(user.Name)
resp := MakeRequest(t, req, http.StatusCreated)
var app *api.OAuth2Application
DecodeJSON(t, resp, &app)
grant := &auth_model.OAuth2Grant{
ApplicationID: app.ID,
UserID: user.ID,
Scope: "openid read:user",
}
err := db.Insert(t.Context(), grant)
require.NoError(t, err)
assert.Contains(t, grant.Scope, "openid read:user")
ctx := loginUser(t, user.Name)
authorizeURL := fmt.Sprintf("/login/oauth/authorize?client_id=%s&redirect_uri=a&response_type=code&state=thestate", app.ClientID)
authorizeReq := NewRequest(t, "GET", authorizeURL)
authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&amp")[0]
accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
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": "a",
"code": authcode,
"client_id": clientA.ClientID,
"client_secret": clientA.ClientSecret,
"redirect_uri": clientA.RedirectURIs[0],
"code": code,
"code_verifier": verifier,
})
accessTokenResp := ctx.MakeRequest(t, accessTokenReq, 200)
type response struct {
resp := MakeRequest(t, req, http.StatusOK)
type tokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
}
parsed := new(response)
tokenParsed := new(tokenResponse)
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), tokenParsed))
require.NotEmpty(t, tokenParsed.AccessToken)
require.NotEmpty(t, tokenParsed.RefreshToken)
require.NoError(t, json.Unmarshal(accessTokenResp.Body.Bytes(), parsed))
type introspectResponse struct {
Active bool `json:"active"`
Scope string `json:"scope,omitempty"`
Username string `json:"username,omitempty"`
jwt.RegisteredClaims
}
assertBlockedIntrospection := func(token string) {
t.Helper()
req = NewRequestWithValues(t, "POST", "/login/oauth/introspect", map[string]string{
"token": token,
})
req.SetBasicAuth(clientB.ClientID, clientB.ClientSecret)
resp = MakeRequest(t, req, http.StatusOK)
blocked := new(introspectResponse)
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), blocked))
assert.False(t, blocked.Active)
assert.Empty(t, blocked.Scope)
assert.Empty(t, blocked.Username)
assert.Empty(t, blocked.Subject)
assert.Empty(t, blocked.Audience)
}
assertAllowedIntrospection := func(token string) {
t.Helper()
req = NewRequestWithValues(t, "POST", "/login/oauth/introspect", map[string]string{
"token": token,
})
req.SetBasicAuth(clientA.ClientID, clientA.ClientSecret)
resp = MakeRequest(t, req, http.StatusOK)
allowed := new(introspectResponse)
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), allowed))
assert.True(t, allowed.Active)
assert.Equal(t, "openid profile", allowed.Scope)
assert.Equal(t, resourceOwner.Name, allowed.Username)
assert.Equal(t, strconv.FormatInt(resourceOwner.ID, 10), allowed.Subject)
assert.Equal(t, jwt.ClaimStrings{clientA.ClientID}, allowed.Audience)
}
assertBlockedIntrospection(tokenParsed.AccessToken)
assertAllowedIntrospection(tokenParsed.AccessToken)
assertBlockedIntrospection(tokenParsed.RefreshToken)
assertAllowedIntrospection(tokenParsed.RefreshToken)
}
func testOAuthGrantScopesReadUserFailRepos(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
accessToken := issueOAuthAccessTokenForScope(t, user, "openid read:user")
userReq := NewRequest(t, "GET", "/api/v1/user")
userReq.SetHeader("Authorization", "Bearer "+parsed.AccessToken)
userReq.SetHeader("Authorization", "Bearer "+accessToken)
userResp := MakeRequest(t, userReq, http.StatusOK)
type userResponse struct {
@@ -742,7 +798,7 @@ func TestOAuth_GrantScopesReadUserFailRepos(t *testing.T) {
assert.Contains(t, userParsed.Email, "user2@example.com")
errorReq := NewRequest(t, "GET", "/api/v1/users/user2/repos")
errorReq.SetHeader("Authorization", "Bearer "+parsed.AccessToken)
errorReq.SetHeader("Authorization", "Bearer "+accessToken)
errorResp := MakeRequest(t, errorReq, http.StatusForbidden)
type errorResponse struct {
@@ -754,14 +810,44 @@ func TestOAuth_GrantScopesReadUserFailRepos(t *testing.T) {
assert.Contains(t, errorParsed.Message, "token does not have at least one of required scope(s), required=[read:repository]")
}
func TestOAuth_GrantScopesReadRepositoryFailOrganization(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testOAuthGrantScopesBasicRespectsWriteUser(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
accessToken := issueOAuthAccessTokenForScope(t, user, "openid read:user")
fullName := "oauth2-basic-scope-test"
updateBody := &api.UserSettingsOptions{
FullName: &fullName,
}
bearerReq := NewRequestWithJSON(t, "PATCH", "/api/v1/user/settings", updateBody)
bearerReq.SetHeader("Authorization", "Bearer "+accessToken)
bearerResp := MakeRequest(t, bearerReq, http.StatusForbidden)
type errorResponse struct {
Message string `json:"message"`
}
bearerError := new(errorResponse)
require.NoError(t, json.Unmarshal(bearerResp.Body.Bytes(), bearerError))
assert.Contains(t, bearerError.Message, "required=[write:user]")
basicReq := NewRequestWithJSON(t, "PATCH", "/api/v1/user/settings", updateBody)
basicAuth := base64.StdEncoding.EncodeToString([]byte(accessToken + ":x-oauth-basic"))
basicReq.SetHeader("Authorization", "Basic "+basicAuth)
basicResp := MakeRequest(t, basicReq, http.StatusForbidden)
basicError := new(errorResponse)
require.NoError(t, json.Unmarshal(basicResp.Body.Bytes(), basicError))
assert.Contains(t, basicError.Message, "required=[write:user]")
}
func issueOAuthAccessTokenForScope(t *testing.T, user *user_model.User, scope string) string {
t.Helper()
appBody := api.CreateOAuth2ApplicationOptions{
Name: "oauth-provider-scopes-test",
RedirectURIs: []string{
"a",
"https://example.com",
},
ConfidentialClient: true,
}
@@ -770,8 +856,55 @@ func TestOAuth_GrantScopesReadRepositoryFailOrganization(t *testing.T) {
AddBasicAuth(user.Name)
resp := MakeRequest(t, req, http.StatusCreated)
var app *api.OAuth2Application
DecodeJSON(t, resp, &app)
app := DecodeJSON(t, resp, &api.OAuth2Application{})
grant := &auth_model.OAuth2Grant{
ApplicationID: app.ID,
UserID: user.ID,
Scope: scope,
}
require.NoError(t, db.Insert(t.Context(), grant))
ctx := loginUser(t, user.Name)
authorizeURL := fmt.Sprintf("/login/oauth/authorize?client_id=%s&redirect_uri=https://example.com&response_type=code&state=thestate", app.ClientID)
authorizeReq := NewRequest(t, "GET", authorizeURL)
authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&amp")[0]
accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"client_id": app.ClientID,
"client_secret": app.ClientSecret,
"redirect_uri": "https://example.com",
"code": authcode,
})
accessTokenResp := ctx.MakeRequest(t, accessTokenReq, 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)
require.NoError(t, json.Unmarshal(accessTokenResp.Body.Bytes(), parsed))
return parsed.AccessToken
}
func testOAuthGrantScopesReadRepositoryFailOrganization(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
appBody := api.CreateOAuth2ApplicationOptions{
Name: "oauth-provider-scopes-test",
RedirectURIs: []string{
"https://example.com",
},
ConfidentialClient: true,
}
req := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &appBody).
AddBasicAuth(user.Name)
resp := MakeRequest(t, req, http.StatusCreated)
app := DecodeJSON(t, resp, &api.OAuth2Application{})
grant := &auth_model.OAuth2Grant{
ApplicationID: app.ID,
@@ -786,7 +919,7 @@ func TestOAuth_GrantScopesReadRepositoryFailOrganization(t *testing.T) {
ctx := loginUser(t, user.Name)
authorizeURL := fmt.Sprintf("/login/oauth/authorize?client_id=%s&redirect_uri=a&response_type=code&state=thestate", app.ClientID)
authorizeURL := fmt.Sprintf("/login/oauth/authorize?client_id=%s&redirect_uri=https://example.com&response_type=code&state=thestate", app.ClientID)
authorizeReq := NewRequest(t, "GET", authorizeURL)
authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
@@ -795,7 +928,7 @@ func TestOAuth_GrantScopesReadRepositoryFailOrganization(t *testing.T) {
"grant_type": "authorization_code",
"client_id": app.ClientID,
"client_secret": app.ClientSecret,
"redirect_uri": "a",
"redirect_uri": "https://example.com",
"code": authcode,
})
accessTokenResp := ctx.MakeRequest(t, accessTokenReq, http.StatusOK)
@@ -893,15 +1026,13 @@ func TestOAuth_GrantScopesReadRepositoryFailOrganization(t *testing.T) {
assert.Contains(t, errorParsed.Message, "token does not have at least one of required scope(s), required=[read:user read:organization]")
}
func TestOAuth_GrantScopesClaimPublicOnlyGroups(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testOAuthGrantScopesClaimPublicOnlyGroups(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
appBody := api.CreateOAuth2ApplicationOptions{
Name: "oauth-provider-scopes-test",
RedirectURIs: []string{
"a",
"https://example.com",
},
ConfidentialClient: true,
}
@@ -910,8 +1041,7 @@ func TestOAuth_GrantScopesClaimPublicOnlyGroups(t *testing.T) {
AddBasicAuth(user.Name)
appResp := MakeRequest(t, appReq, http.StatusCreated)
var app *api.OAuth2Application
DecodeJSON(t, appResp, &app)
app := DecodeJSON(t, appResp, &api.OAuth2Application{})
grant := &auth_model.OAuth2Grant{
ApplicationID: app.ID,
@@ -926,7 +1056,7 @@ func TestOAuth_GrantScopesClaimPublicOnlyGroups(t *testing.T) {
ctx := loginUser(t, user.Name)
authorizeURL := fmt.Sprintf("/login/oauth/authorize?client_id=%s&redirect_uri=a&response_type=code&state=thestate", app.ClientID)
authorizeURL := fmt.Sprintf("/login/oauth/authorize?client_id=%s&redirect_uri=https://example.com&response_type=code&state=thestate", app.ClientID)
authorizeReq := NewRequest(t, "GET", authorizeURL)
authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
@@ -936,7 +1066,7 @@ func TestOAuth_GrantScopesClaimPublicOnlyGroups(t *testing.T) {
"grant_type": "authorization_code",
"client_id": app.ClientID,
"client_secret": app.ClientSecret,
"redirect_uri": "a",
"redirect_uri": "https://example.com",
"code": authcode,
})
accessTokenResp := ctx.MakeRequest(t, accessTokenReq, http.StatusOK)
@@ -994,15 +1124,13 @@ func TestOAuth_GrantScopesClaimPublicOnlyGroups(t *testing.T) {
}
}
func TestOAuth_GrantScopesClaimAllGroups(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testOAuthGrantScopesClaimAllGroups(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
appBody := api.CreateOAuth2ApplicationOptions{
Name: "oauth-provider-scopes-test",
RedirectURIs: []string{
"a",
"https://example.com",
},
ConfidentialClient: true,
}
@@ -1011,8 +1139,7 @@ func TestOAuth_GrantScopesClaimAllGroups(t *testing.T) {
AddBasicAuth(user.Name)
appResp := MakeRequest(t, appReq, http.StatusCreated)
var app *api.OAuth2Application
DecodeJSON(t, appResp, &app)
app := DecodeJSON(t, appResp, &api.OAuth2Application{})
grant := &auth_model.OAuth2Grant{
ApplicationID: app.ID,
@@ -1027,7 +1154,7 @@ func TestOAuth_GrantScopesClaimAllGroups(t *testing.T) {
ctx := loginUser(t, user.Name)
authorizeURL := fmt.Sprintf("/login/oauth/authorize?client_id=%s&redirect_uri=a&response_type=code&state=thestate", app.ClientID)
authorizeURL := fmt.Sprintf("/login/oauth/authorize?client_id=%s&redirect_uri=https://example.com&response_type=code&state=thestate", app.ClientID)
authorizeReq := NewRequest(t, "GET", authorizeURL)
authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
@@ -1037,7 +1164,7 @@ func TestOAuth_GrantScopesClaimAllGroups(t *testing.T) {
"grant_type": "authorization_code",
"client_id": app.ClientID,
"client_secret": app.ClientSecret,
"redirect_uri": "a",
"redirect_uri": "https://example.com",
"code": authcode,
})
accessTokenResp := ctx.MakeRequest(t, accessTokenReq, http.StatusOK)
@@ -1097,8 +1224,7 @@ func testOAuth2WellKnown(t *testing.T) {
t.Run("WellKnown", func(t *testing.T) {
req := NewRequest(t, "GET", urlOpenidConfiguration)
resp := MakeRequest(t, req, http.StatusOK)
var respMap map[string]any
DecodeJSON(t, resp, &respMap)
respMap := DecodeJSON(t, resp, map[string]any{})
assert.Equal(t, "https://try.gitea.io", respMap["issuer"])
assert.Equal(t, "https://try.gitea.io/login/oauth/authorize", respMap["authorization_endpoint"])
assert.Equal(t, "https://try.gitea.io/login/oauth/access_token", respMap["token_endpoint"])
@@ -1112,8 +1238,7 @@ func testOAuth2WellKnown(t *testing.T) {
defer test.MockVariableValue(&setting.OAuth2.JWTClaimIssuer, "https://try.gitea.io/")()
req := NewRequest(t, "GET", urlOpenidConfiguration)
resp := MakeRequest(t, req, http.StatusOK)
var respMap map[string]any
DecodeJSON(t, resp, &respMap)
respMap := DecodeJSON(t, resp, map[string]any{})
assert.Equal(t, "https://try.gitea.io/", respMap["issuer"]) // has trailing by JWTClaimIssuer
assert.Equal(t, "https://try.gitea.io/login/oauth/authorize", respMap["authorization_endpoint"])
})
@@ -1159,9 +1284,7 @@ func createOAuth2MockProvider() *httptest.Server {
return mockServer
}
func TestSignInOauthCallbackSyncSSHKeys(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testSignInOauthCallbackSyncSSHKeys(t *testing.T) {
mockServer := createOAuth2MockProvider()
defer mockServer.Close()
@@ -1282,3 +1405,22 @@ func testOAuthSourceSpecialChars(t *testing.T) {
testOAuth2(t, "/user/oauth2/test%2Bplus", http.StatusTemporaryRedirect)
testOAuth2(t, "/user/oauth2/test%20plus", http.StatusNotFound)
}
// TestOAuthUserInfoTokenScope verifies the OIDC userinfo endpoint enforces the
// read:user token scope, so a restrictively-scoped token cannot read identity claims.
func TestOAuthUserInfoTokenScope(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// a token without the user scope must be rejected
miscToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadMisc)
req := NewRequest(t, "GET", "/login/oauth/userinfo")
req.SetHeader("Authorization", "Bearer "+miscToken)
MakeRequest(t, req, http.StatusForbidden)
// a token with read:user is allowed and returns the identity claims
userToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser)
req = NewRequest(t, "GET", "/login/oauth/userinfo")
req.SetHeader("Authorization", "Bearer "+userToken)
resp := MakeRequest(t, req, http.StatusOK)
assert.Contains(t, resp.Body.String(), "user2@example.com")
}