Support for Custom URI Schemes in OAuth2 Redirect URIs (#37356)

Fix #34349

By the way, remove `(ctx *APIContext) HasAPIError() ` and `(ctx
*APIContext) GetErrMsg()` because they do nothing, the error handling
has been done in API's middeware

The existing OAuth2 tests were not quite right, refactored them together
This commit is contained in:
wxiaoguang
2026-04-23 05:33:27 +08:00
committed by GitHub
parent 8cfcef32c6
commit 83bdfc2a57
21 changed files with 340 additions and 512 deletions
+39 -74
View File
@@ -11,10 +11,13 @@ import (
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestOAuth2Application(t *testing.T) {
@@ -28,18 +31,17 @@ func TestOAuth2Application(t *testing.T) {
func testAPICreateOAuth2Application(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
appBody := api.CreateOAuth2ApplicationOptions{
Name: "test-app-1",
RedirectURIs: []string{
"http://www.google.com",
},
ConfidentialClient: true,
}
redirectURIs := []string{"http://www.google.com", "my-app:foo"}
appBody := api.CreateOAuth2ApplicationOptions{Name: "test-app-1", RedirectURIs: redirectURIs, ConfidentialClient: true}
req := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &appBody).
AddBasicAuth(user.Name)
// no custom scheme
req := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &appBody).AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusBadRequest)
// with custom scheme
defer test.MockVariableValue(&setting.OAuth2.CustomSchemes, []string{"my-app"})()
req = NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &appBody).AddBasicAuth(user.Name)
resp := MakeRequest(t, req, http.StatusCreated)
createdApp := DecodeJSON(t, resp, &api.OAuth2Application{})
assert.Equal(t, appBody.Name, createdApp.Name)
@@ -47,7 +49,7 @@ func testAPICreateOAuth2Application(t *testing.T) {
assert.Len(t, createdApp.ClientID, 36)
assert.True(t, createdApp.ConfidentialClient)
assert.NotEmpty(t, createdApp.Created)
assert.Equal(t, appBody.RedirectURIs[0], createdApp.RedirectURIs[0])
assert.Equal(t, redirectURIs, createdApp.RedirectURIs)
unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{UID: user.ID, Name: createdApp.Name})
}
@@ -56,21 +58,13 @@ func testAPIListOAuth2Applications(t *testing.T) {
session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser)
existApp := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{
UID: user.ID,
Name: "test-app-1",
RedirectURIs: []string{
"http://www.google.com",
},
ConfidentialClient: true,
})
existApp := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{UID: user.ID, Name: "test-app-1", ConfidentialClient: true})
require.NotEmpty(t, existApp.RedirectURIs)
req := NewRequest(t, "GET", "/api/v1/user/applications/oauth2").
AddTokenAuth(token)
req := NewRequest(t, "GET", "/api/v1/user/applications/oauth2").AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var appList api.OAuth2ApplicationList
DecodeJSON(t, resp, &appList)
appList := DecodeJSON(t, resp, api.OAuth2ApplicationList{})
expectedApp := appList[0]
assert.Equal(t, expectedApp.Name, existApp.Name)
@@ -78,7 +72,7 @@ func testAPIListOAuth2Applications(t *testing.T) {
assert.Equal(t, expectedApp.ConfidentialClient, existApp.ConfidentialClient)
assert.Len(t, expectedApp.ClientID, 36)
assert.Empty(t, expectedApp.ClientSecret)
assert.Equal(t, existApp.RedirectURIs[0], expectedApp.RedirectURIs[0])
assert.Equal(t, expectedApp.RedirectURIs, existApp.RedirectURIs)
unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: expectedApp.ID, Name: expectedApp.Name})
}
@@ -87,21 +81,16 @@ func testAPIDeleteOAuth2Application(t *testing.T) {
session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser)
oldApp := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{
UID: user.ID,
Name: "test-app-1",
})
oldApp := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{UID: user.ID, Name: "test-app-1"})
urlStr := fmt.Sprintf("/api/v1/user/applications/oauth2/%d", oldApp.ID)
req := NewRequest(t, "DELETE", urlStr).
AddTokenAuth(token)
req := NewRequest(t, "DELETE", urlStr).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
unittest.AssertNotExistsBean(t, &auth_model.OAuth2Application{UID: oldApp.UID, Name: oldApp.Name})
// Delete again will return not found
req = NewRequest(t, "DELETE", urlStr).
AddTokenAuth(token)
req = NewRequest(t, "DELETE", urlStr).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
}
@@ -110,65 +99,41 @@ func testAPIGetOAuth2Application(t *testing.T) {
session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser)
existApp := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{
UID: user.ID,
Name: "test-app-1",
RedirectURIs: []string{
"http://www.google.com",
},
ConfidentialClient: true,
})
existApp := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{UID: user.ID, Name: "test-app-1", ConfidentialClient: true})
require.NotEmpty(t, existApp.RedirectURIs)
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/applications/oauth2/%d", existApp.ID)).
AddTokenAuth(token)
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/applications/oauth2/%d", existApp.ID)).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var app api.OAuth2Application
DecodeJSON(t, resp, &app)
expectedApp := app
expectedApp := DecodeJSON(t, resp, &api.OAuth2Application{})
assert.Equal(t, expectedApp.Name, existApp.Name)
assert.Equal(t, expectedApp.ClientID, existApp.ClientID)
assert.Equal(t, expectedApp.ConfidentialClient, existApp.ConfidentialClient)
assert.Len(t, expectedApp.ClientID, 36)
assert.Empty(t, expectedApp.ClientSecret)
assert.Len(t, expectedApp.RedirectURIs, 1)
assert.Equal(t, expectedApp.RedirectURIs[0], existApp.RedirectURIs[0])
assert.Equal(t, expectedApp.RedirectURIs, existApp.RedirectURIs)
unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: expectedApp.ID, Name: expectedApp.Name})
}
func testAPIUpdateOAuth2Application(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
existApp := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{
UID: user.ID,
Name: "test-app-1",
RedirectURIs: []string{
"http://www.google.com",
},
})
appBody := api.CreateOAuth2ApplicationOptions{
Name: "test-app-1",
RedirectURIs: []string{
"http://www.google.com/",
"http://www.github.com/",
},
ConfidentialClient: true,
}
existApp := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{UID: user.ID, Name: "test-app-1"})
redirectURIs := []string{"https://www.google.com", "my-app:foo"}
appBody := api.CreateOAuth2ApplicationOptions{Name: "test-app-1", RedirectURIs: redirectURIs, ConfidentialClient: true}
urlStr := fmt.Sprintf("/api/v1/user/applications/oauth2/%d", existApp.ID)
req := NewRequestWithJSON(t, "PATCH", urlStr, &appBody).
AddBasicAuth(user.Name)
// no custom scheme
req := NewRequestWithJSON(t, "PATCH", urlStr, &appBody).AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusBadRequest)
// with custom scheme
defer test.MockVariableValue(&setting.OAuth2.CustomSchemes, []string{"my-app"})()
req = NewRequestWithJSON(t, "PATCH", urlStr, &appBody).AddBasicAuth(user.Name)
resp := MakeRequest(t, req, http.StatusOK)
var app api.OAuth2Application
DecodeJSON(t, resp, &app)
expectedApp := app
assert.Len(t, expectedApp.RedirectURIs, 2)
assert.Equal(t, expectedApp.RedirectURIs[0], appBody.RedirectURIs[0])
assert.Equal(t, expectedApp.RedirectURIs[1], appBody.RedirectURIs[1])
expectedApp := DecodeJSON(t, resp, &api.OAuth2Application{})
assert.Equal(t, expectedApp.RedirectURIs, appBody.RedirectURIs)
assert.Equal(t, expectedApp.ConfidentialClient, appBody.ConfidentialClient)
unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: expectedApp.ID, Name: expectedApp.Name})
}
+116 -88
View File
@@ -23,6 +23,7 @@ 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"
@@ -35,17 +36,57 @@ import (
"github.com/stretchr/testify/require"
)
func TestOAuth2Provider(t *testing.T) {
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 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("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("AccessTokenExchangeWithPublicClient", testAccessTokenExchangeWithPublicClient)
t.Run("AccessTokenExchangeJSON", testAccessTokenExchangeJSON)
t.Run("AccessTokenExchangeWithoutPKCE", testAccessTokenExchangeWithoutPKCE)
t.Run("AccessTokenExchangeWithInvalidCredentials", testAccessTokenExchangeWithInvalidCredentials)
t.Run("AccessTokenExchangeWithBasicAuth", testAccessTokenExchangeWithBasicAuth)
t.Run("RefreshTokenInvalidation", testRefreshTokenInvalidation)
t.Run("OAuthIntrospection", testOAuthIntrospection)
t.Run("OAuthGrantScopesReadUserFailRepos", testOAuthGrantScopesReadUserFailRepos)
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
}
@@ -64,7 +105,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()
@@ -74,7 +115,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()
@@ -88,9 +129,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)
@@ -98,11 +138,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)
@@ -113,7 +152,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)
@@ -126,7 +165,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)
@@ -136,9 +175,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()
@@ -146,11 +184,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)
@@ -160,13 +198,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",
})
@@ -184,8 +222,8 @@ func TestAccessTokenExchange(t *testing.T) {
assert.Greater(t, len(parsed.RefreshToken), 10)
}
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",
@@ -207,13 +245,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",
})
@@ -231,13 +269,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)
@@ -247,14 +285,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",
})
@@ -269,7 +307,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",
})
@@ -299,7 +337,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",
})
@@ -314,7 +352,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",
})
@@ -325,11 +363,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",
})
@@ -350,7 +388,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",
})
@@ -364,7 +402,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",
})
@@ -377,7 +415,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",
})
@@ -391,7 +429,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",
})
@@ -403,13 +441,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",
})
@@ -431,7 +469,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)
@@ -444,7 +482,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)
@@ -457,7 +495,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,
})
@@ -484,13 +522,13 @@ func TestRefreshTokenInvalidation(t *testing.T) {
assert.Equal(t, "token was already used", parsedError.ErrorDescription)
}
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",
})
@@ -542,14 +580,12 @@ 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 testOAuthGrantScopesReadUserFailRepos(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
appBody := api.CreateOAuth2ApplicationOptions{
Name: "oauth-provider-scopes-test",
RedirectURIs: []string{
"a",
"https://example.com",
},
ConfidentialClient: true,
}
@@ -573,7 +609,7 @@ func TestOAuth_GrantScopesReadUserFailRepos(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)
@@ -583,7 +619,7 @@ func TestOAuth_GrantScopesReadUserFailRepos(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, 200)
@@ -622,14 +658,12 @@ 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 testOAuthGrantScopesReadRepositoryFailOrganization(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
appBody := api.CreateOAuth2ApplicationOptions{
Name: "oauth-provider-scopes-test",
RedirectURIs: []string{
"a",
"https://example.com",
},
ConfidentialClient: true,
}
@@ -653,7 +687,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)
@@ -662,7 +696,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)
@@ -760,15 +794,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,
}
@@ -792,7 +824,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)
@@ -802,7 +834,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)
@@ -860,15 +892,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,
}
@@ -892,7 +922,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)
@@ -902,7 +932,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)
@@ -998,7 +1028,7 @@ func addOAuth2Source(t *testing.T, authName string, cfg oauth2.Source) {
require.NoError(t, err)
}
func createMockServer() *httptest.Server {
func createOAuth2MockProvider() *httptest.Server {
var mockServer *httptest.Server
mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
@@ -1017,10 +1047,8 @@ func createMockServer() *httptest.Server {
return mockServer
}
func TestSignInOauthCallbackSyncSSHKeys(t *testing.T) {
defer tests.PrepareTestEnv(t)()
mockServer := createMockServer()
func testSignInOauthCallbackSyncSSHKeys(t *testing.T) {
mockServer := createOAuth2MockProvider()
defer mockServer.Close()
ctx := t.Context()
@@ -1100,7 +1128,7 @@ 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 := createMockServer()
mockServer := createOAuth2MockProvider()
defer mockServer.Close()
addOAuth2Source(t, "test space", oauth2.Source{
+10 -2
View File
@@ -10,6 +10,7 @@ import (
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
@@ -311,18 +312,25 @@ func TestUserSettingsApplications(t *testing.T) {
resp := session.MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body)
msg := strings.TrimSpace(doc.Find(".ui.message.flash-message").Text())
assert.Equal(t, `form.RedirectURIs"ftp://127.0.0.1" is not a valid URL.`, msg)
assert.Equal(t, `RedirectURIs: "ftp://127.0.0.1" is not a valid URL.`, msg)
})
t.Run("OK", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
defer test.MockVariableValue(&setting.OAuth2.CustomSchemes, []string{"my-app"})()
req := NewRequestWithValues(t, "POST", "/user/settings/applications/oauth2/2", map[string]string{
"application_name": "Test native app",
"redirect_uris": "http://127.0.0.1",
"confidential_client": "false",
})
session.MakeRequest(t, req, http.StatusSeeOther)
req = NewRequestWithValues(t, "POST", "/user/settings/applications/oauth2/2", map[string]string{
"application_name": "Test native app",
"redirect_uris": "my-app://127.0.0.1",
"confidential_client": "false",
})
session.MakeRequest(t, req, http.StatusSeeOther)
})
})
})