This commit is contained in:
@@ -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], "&")[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], "&")[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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user