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
+5
View File
@@ -592,6 +592,11 @@ ENABLED = true
;; * https://github.com/git-ecosystem/git-credential-manager ;; * https://github.com/git-ecosystem/git-credential-manager
;; * https://gitea.com/gitea/tea ;; * https://gitea.com/gitea/tea
;DEFAULT_APPLICATIONS = git-credential-oauth, git-credential-manager, tea ;DEFAULT_APPLICATIONS = git-credential-oauth, git-credential-manager, tea
;;
;; By default, OAuth2 applications can only use "http" and "https" as their redirect URI schemes.
;; If you need to use other schemes (e.g. for desktop applications), you can specify them here as a comma-separated list.
;; For example: set "my-scheme, com.example.app" to support "my-scheme://..." and "com.example.app://..." redirect URIs.
;CUSTOM_SCHEMES =
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+22 -30
View File
@@ -12,19 +12,30 @@ import (
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestOAuth2AuthorizationCodeValidity(t *testing.T) { func TestOAuth2AuthorizationCode(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) require.NoError(t, unittest.PrepareTestDatabase())
t.Run("GenerateSetsValidUntil", func(t *testing.T) { t.Run("GenerateSetsValidUntil", func(t *testing.T) {
grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1}) grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1})
expectedValidUntil := timeutil.TimeStamp(time.Now().Unix() + 600) expectedValidUntil := timeutil.TimeStamp(time.Now().Unix() + 600)
code, err := grant.GenerateNewAuthorizationCode(t.Context(), "http://127.0.0.1/", "", "") code, err := grant.GenerateNewAuthorizationCode(t.Context(), "http://127.0.0.1/", "", "")
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, expectedValidUntil, code.ValidUntil) assert.Equal(t, expectedValidUntil, code.ValidUntil)
assert.False(t, code.IsExpired()) assert.False(t, code.IsExpired())
assert.Equal(t, int64(1), code.ID)
code2, err := auth_model.GetOAuth2AuthorizationByCode(t.Context(), code.Code)
require.NoError(t, err)
assert.Equal(t, code.Code, code2.Code)
assert.NoError(t, code.Invalidate(t.Context())) assert.NoError(t, code.Invalidate(t.Context()))
code, err = auth_model.GetOAuth2AuthorizationByCode(t.Context(), "does not exist")
require.NoError(t, err)
require.Nil(t, code)
}) })
t.Run("Expired", func(t *testing.T) { t.Run("Expired", func(t *testing.T) {
@@ -34,13 +45,14 @@ func TestOAuth2AuthorizationCodeValidity(t *testing.T) {
assert.True(t, code.IsExpired()) assert.True(t, code.IsExpired())
}) })
t.Run("InvalidateTwice", func(t *testing.T) { t.Run("Invalidate", func(t *testing.T) {
code, err := auth_model.GetOAuth2AuthorizationByCode(t.Context(), "authcode") grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1})
assert.NoError(t, err) code, err := grant.GenerateNewAuthorizationCode(t.Context(), "http://127.0.0.1/", "", "")
if assert.NotNil(t, code) { require.NoError(t, err)
assert.NoError(t, code.Invalidate(t.Context())) require.NotNil(t, code)
assert.ErrorIs(t, code.Invalidate(t.Context()), auth_model.ErrOAuth2AuthorizationCodeInvalidated) require.NoError(t, code.Invalidate(t.Context()))
} unittest.AssertNotExistsBean(t, &auth_model.OAuth2AuthorizationCode{Code: code.Code})
assert.ErrorIs(t, code.Invalidate(t.Context()), auth_model.ErrOAuth2AuthorizationCodeInvalidated)
}) })
} }
@@ -224,19 +236,6 @@ func TestRevokeOAuth2Grant(t *testing.T) {
//////////////////// Authorization Code //////////////////// Authorization Code
func TestGetOAuth2AuthorizationByCode(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
code, err := auth_model.GetOAuth2AuthorizationByCode(t.Context(), "authcode")
assert.NoError(t, err)
assert.NotNil(t, code)
assert.Equal(t, "authcode", code.Code)
assert.Equal(t, int64(1), code.ID)
code, err = auth_model.GetOAuth2AuthorizationByCode(t.Context(), "does not exist")
assert.NoError(t, err)
assert.Nil(t, code)
}
func TestOAuth2AuthorizationCode_ValidateCodeChallenge(t *testing.T) { func TestOAuth2AuthorizationCode_ValidateCodeChallenge(t *testing.T) {
// test plain // test plain
code := &auth_model.OAuth2AuthorizationCode{ code := &auth_model.OAuth2AuthorizationCode{
@@ -284,13 +283,6 @@ func TestOAuth2AuthorizationCode_GenerateRedirectURI(t *testing.T) {
assert.Equal(t, "https://example.com/callback?code=thecode", redirect.String()) assert.Equal(t, "https://example.com/callback?code=thecode", redirect.String())
} }
func TestOAuth2AuthorizationCode_Invalidate(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
code := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2AuthorizationCode{Code: "authcode"})
assert.NoError(t, code.Invalidate(t.Context()))
unittest.AssertNotExistsBean(t, &auth_model.OAuth2AuthorizationCode{Code: "authcode"})
}
func TestOAuth2AuthorizationCode_TableName(t *testing.T) { func TestOAuth2AuthorizationCode_TableName(t *testing.T) {
assert.Equal(t, "oauth2_authorization_code", new(auth_model.OAuth2AuthorizationCode).TableName()) assert.Equal(t, "oauth2_authorization_code", new(auth_model.OAuth2AuthorizationCode).TableName())
} }
+1 -1
View File
@@ -4,7 +4,7 @@
name: "Test" name: "Test"
client_id: "da7da3ba-9a13-4167-856f-3899de0b0138" client_id: "da7da3ba-9a13-4167-856f-3899de0b0138"
client_secret: "$2a$10$UYRgUSgekzBp6hYe8pAdc.cgB4Gn06QRKsORUnIYTYQADs.YR/uvi" # bcrypt of "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA= client_secret: "$2a$10$UYRgUSgekzBp6hYe8pAdc.cgB4Gn06QRKsORUnIYTYQADs.YR/uvi" # bcrypt of "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=
redirect_uris: '["a", "https://example.com/xyzzy"]' redirect_uris: '["https://example.com"]'
created_unix: 1546869730 created_unix: 1546869730
updated_unix: 1546869730 updated_unix: 1546869730
confidential_client: true confidential_client: true
+1 -16
View File
@@ -1,17 +1,2 @@
- id: 1 []
grant_id: 1
code: "authcode"
code_challenge: "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg" # Code Verifier: N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt
code_challenge_method: "S256"
redirect_uri: "a"
valid_until: 3546869730
- id: 2
grant_id: 4
code: "authcodepublic"
code_challenge: "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg" # Code Verifier: N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt
code_challenge_method: "S256"
redirect_uri: "http://127.0.0.1/"
valid_until: 3546869730
# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly # DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly
+1
View File
@@ -99,6 +99,7 @@ var OAuth2 = struct {
JWTClaimIssuer string `ini:"JWT_CLAIM_ISSUER"` JWTClaimIssuer string `ini:"JWT_CLAIM_ISSUER"`
MaxTokenLength int MaxTokenLength int
DefaultApplications []string DefaultApplications []string
CustomSchemes []string
}{ }{
Enabled: true, Enabled: true,
AccessTokenExpirationTime: 3600, AccessTokenExpirationTime: 3600,
-29
View File
@@ -13,7 +13,6 @@ import (
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/glob" "code.gitea.io/gitea/modules/glob"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/util"
"gitea.com/go-chi/binding" "gitea.com/go-chi/binding"
) )
@@ -51,7 +50,6 @@ func (j jsonProvider) NewEncoder(writer io.Writer) binding.JSONEncoder {
func AddBindingRules() { func AddBindingRules() {
binding.JSONProvider = jsonProvider{} binding.JSONProvider = jsonProvider{}
addGitRefNameBindingRule() addGitRefNameBindingRule()
addValidURLListBindingRule()
addValidURLBindingRule() addValidURLBindingRule()
addValidSiteURLBindingRule() addValidSiteURLBindingRule()
addGlobPatternRule() addGlobPatternRule()
@@ -80,33 +78,6 @@ func addGitRefNameBindingRule() {
}) })
} }
func addValidURLListBindingRule() {
// URL validation rule
binding.AddRule(&binding.Rule{
IsMatch: func(rule string) bool {
return rule == "ValidUrlList"
},
IsValid: func(errs binding.Errors, name string, val any) (bool, binding.Errors) {
str := fmt.Sprintf("%v", val)
if len(str) == 0 {
errs.Add([]string{name}, binding.ERR_URL, "Url")
return false, errs
}
ok := true
urls := util.SplitTrimSpace(str, "\n")
for _, u := range urls {
if !IsValidURL(u) {
ok = false
errs.Add([]string{name}, binding.ERR_URL, u)
}
}
return ok, errs
},
})
}
func addValidURLBindingRule() { func addValidURLBindingRule() {
// URL validation rule // URL validation rule
binding.AddRule(&binding.Rule{ binding.AddRule(&binding.Rule{
-1
View File
@@ -27,7 +27,6 @@ type (
TestForm struct { TestForm struct {
BranchName string `form:"BranchName" binding:"GitRefName"` BranchName string `form:"BranchName" binding:"GitRefName"`
URL string `form:"ValidUrl" binding:"ValidUrl"` URL string `form:"ValidUrl" binding:"ValidUrl"`
URLs string `form:"ValidUrls" binding:"ValidUrlList"`
GlobPattern string `form:"GlobPattern" binding:"GlobPattern"` GlobPattern string `form:"GlobPattern" binding:"GlobPattern"`
RegexPattern string `form:"RegexPattern" binding:"RegexPattern"` RegexPattern string `form:"RegexPattern" binding:"RegexPattern"`
} }
-157
View File
@@ -1,157 +0,0 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package validation
import (
"testing"
"gitea.com/go-chi/binding"
)
func Test_ValidURLListValidation(t *testing.T) {
AddBindingRules()
// This is a copy of all the URL tests cases, plus additional ones to
// account for multiple URLs
urlListValidationTestCases := []validationTestCase{
{
description: "Empty URL",
data: TestForm{
URLs: "",
},
expectedErrors: binding.Errors{},
},
{
description: "URL without port",
data: TestForm{
URLs: "http://test.lan/",
},
expectedErrors: binding.Errors{},
},
{
description: "URL with port",
data: TestForm{
URLs: "http://test.lan:3000/",
},
expectedErrors: binding.Errors{},
},
{
description: "URL with IPv6 address without port",
data: TestForm{
URLs: "http://[::1]/",
},
expectedErrors: binding.Errors{},
},
{
description: "URL with IPv6 address with port",
data: TestForm{
URLs: "http://[::1]:3000/",
},
expectedErrors: binding.Errors{},
},
{
description: "Invalid URL",
data: TestForm{
URLs: "http//test.lan/",
},
expectedErrors: binding.Errors{
binding.Error{
FieldNames: []string{"URLs"},
Classification: binding.ERR_URL,
Message: "http//test.lan/",
},
},
},
{
description: "Invalid schema",
data: TestForm{
URLs: "ftp://test.lan/",
},
expectedErrors: binding.Errors{
binding.Error{
FieldNames: []string{"URLs"},
Classification: binding.ERR_URL,
Message: "ftp://test.lan/",
},
},
},
{
description: "Invalid port",
data: TestForm{
URLs: "http://test.lan:3x4/",
},
expectedErrors: binding.Errors{
binding.Error{
FieldNames: []string{"URLs"},
Classification: binding.ERR_URL,
Message: "http://test.lan:3x4/",
},
},
},
{
description: "Invalid port with IPv6 address",
data: TestForm{
URLs: "http://[::1]:3x4/",
},
expectedErrors: binding.Errors{
binding.Error{
FieldNames: []string{"URLs"},
Classification: binding.ERR_URL,
Message: "http://[::1]:3x4/",
},
},
},
{
description: "Multi URLs",
data: TestForm{
URLs: "http://test.lan:3000/\nhttp://test.local/",
},
expectedErrors: binding.Errors{},
},
{
description: "Multi URLs with newline",
data: TestForm{
URLs: "http://test.lan:3000/\nhttp://test.local/\n",
},
expectedErrors: binding.Errors{},
},
{
description: "List with invalid entry",
data: TestForm{
URLs: "http://test.lan:3000/\nhttp://[::1]:3x4/",
},
expectedErrors: binding.Errors{
binding.Error{
FieldNames: []string{"URLs"},
Classification: binding.ERR_URL,
Message: "http://[::1]:3x4/",
},
},
},
{
description: "List with two invalid entries",
data: TestForm{
URLs: "ftp://test.lan:3000/\nhttp://[::1]:3x4/\n",
},
expectedErrors: binding.Errors{
binding.Error{
FieldNames: []string{"URLs"},
Classification: binding.ERR_URL,
Message: "ftp://test.lan:3000/",
},
binding.Error{
FieldNames: []string{"URLs"},
Classification: binding.ERR_URL,
Message: "http://[::1]:3x4/",
},
},
},
}
for _, testCase := range urlListValidationTestCases {
t.Run(testCase.description, func(t *testing.T) {
performValidationTest(t, testCase)
})
}
}
+80 -65
View File
@@ -78,82 +78,97 @@ func GetInclude(field reflect.StructField) string {
return getRuleBody(field, "Include(") return getRuleBody(field, "Include(")
} }
// Validate validate func ReportValidationError(errs binding.Errors, data map[string]any, fieldName, classification, errorMsg string) binding.Errors {
errs.Add([]string{fieldName}, classification, errorMsg)
data["HasError"] = true
data["ErrorMsg"] = fieldName + ": " + errorMsg
data["Err_"+fieldName] = true
// there is already a reported validation error, so no need to generate default error messages in Validate()
data["HasErrorFormValidation"] = true
return errs
}
func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Locale) binding.Errors { func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Locale) binding.Errors {
if errs.Len() == 0 { // try to restore the form's values as much as possible,
// especially for RenderWithErrDeprecated to re-render the form with errors
AssignForm(f, data)
if errs.Len() == 0 || data["HasErrorFormValidation"] == true {
return errs return errs
} }
// if HasError=true, then must set default error message
// because still a lot of places use `ctx.Data["ErrorMsg"].(string)` even if the error fields can't be found
data["HasError"] = true data["HasError"] = true
// If the field with name errs[0].FieldNames[0] is not found in form data["ErrorMsg"] = l.TrString("form.unknown_error")
// somehow, some code later on will panic on Data["ErrorMsg"].(string).
// So initialize it to some default.
data["ErrorMsg"] = l.Tr("form.unknown_error")
AssignForm(f, data)
typ := reflect.TypeOf(f) typ := reflect.TypeOf(f)
if typ.Kind() == reflect.Ptr { if typ.Kind() == reflect.Ptr {
typ = typ.Elem() typ = typ.Elem()
} }
if field, ok := typ.FieldByName(errs[0].FieldNames[0]); ok { field, fieldExists := typ.FieldByName(errs[0].FieldNames[0])
fieldName := field.Tag.Get("form") if !fieldExists {
if fieldName != "-" { return errs
data["Err_"+field.Name] = true
trName := field.Tag.Get("locale")
if len(trName) == 0 {
trName = l.TrString("form." + field.Name)
} else {
trName = l.TrString(trName)
}
switch errs[0].Classification {
case binding.ERR_REQUIRED:
data["ErrorMsg"] = trName + l.TrString("form.require_error")
case binding.ERR_ALPHA_DASH:
data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_error")
case binding.ERR_ALPHA_DASH_DOT:
data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_dot_error")
case validation.ErrGitRefName:
data["ErrorMsg"] = trName + l.TrString("form.git_ref_name_error")
case binding.ERR_SIZE:
data["ErrorMsg"] = trName + l.TrString("form.size_error", GetSize(field))
case binding.ERR_MIN_SIZE:
data["ErrorMsg"] = trName + l.TrString("form.min_size_error", GetMinSize(field))
case binding.ERR_MAX_SIZE:
data["ErrorMsg"] = trName + l.TrString("form.max_size_error", GetMaxSize(field))
case binding.ERR_EMAIL:
data["ErrorMsg"] = trName + l.TrString("form.email_error")
case binding.ERR_URL:
data["ErrorMsg"] = trName + l.TrString("form.url_error", errs[0].Message)
case binding.ERR_INCLUDE:
data["ErrorMsg"] = trName + l.TrString("form.include_error", GetInclude(field))
case validation.ErrGlobPattern:
data["ErrorMsg"] = trName + l.TrString("form.glob_pattern_error", errs[0].Message)
case validation.ErrRegexPattern:
data["ErrorMsg"] = trName + l.TrString("form.regex_pattern_error", errs[0].Message)
case validation.ErrUsername:
data["ErrorMsg"] = trName + l.TrString("form.username_error")
case validation.ErrInvalidGroupTeamMap:
data["ErrorMsg"] = trName + l.TrString("form.invalid_group_team_map_error", errs[0].Message)
case validation.ErrInvalidBadgeSlug:
data["ErrorMsg"] = trName + l.TrString("form.invalid_slug_error")
default:
msg := errs[0].Classification
if msg != "" && errs[0].Message != "" {
msg += ": "
}
msg += errs[0].Message
if msg == "" {
msg = l.TrString("form.unknown_error")
}
data["ErrorMsg"] = trName + ": " + msg
}
return errs
}
} }
if field.Tag.Get("form") == "-" {
return errs
}
data["Err_"+field.Name] = true
trName := field.Tag.Get("locale")
if len(trName) == 0 {
trName = l.TrString("form." + field.Name)
} else {
trName = l.TrString(trName)
}
switch errs[0].Classification {
case binding.ERR_REQUIRED:
data["ErrorMsg"] = trName + l.TrString("form.require_error")
case binding.ERR_ALPHA_DASH:
data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_error")
case binding.ERR_ALPHA_DASH_DOT:
data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_dot_error")
case validation.ErrGitRefName:
data["ErrorMsg"] = trName + l.TrString("form.git_ref_name_error")
case binding.ERR_SIZE:
data["ErrorMsg"] = trName + l.TrString("form.size_error", GetSize(field))
case binding.ERR_MIN_SIZE:
data["ErrorMsg"] = trName + l.TrString("form.min_size_error", GetMinSize(field))
case binding.ERR_MAX_SIZE:
data["ErrorMsg"] = trName + l.TrString("form.max_size_error", GetMaxSize(field))
case binding.ERR_EMAIL:
data["ErrorMsg"] = trName + l.TrString("form.email_error")
case binding.ERR_URL:
data["ErrorMsg"] = trName + l.TrString("form.url_error", errs[0].Message)
case binding.ERR_INCLUDE:
data["ErrorMsg"] = trName + l.TrString("form.include_error", GetInclude(field))
case validation.ErrGlobPattern:
data["ErrorMsg"] = trName + l.TrString("form.glob_pattern_error", errs[0].Message)
case validation.ErrRegexPattern:
data["ErrorMsg"] = trName + l.TrString("form.regex_pattern_error", errs[0].Message)
case validation.ErrUsername:
data["ErrorMsg"] = trName + l.TrString("form.username_error")
case validation.ErrInvalidGroupTeamMap:
data["ErrorMsg"] = trName + l.TrString("form.invalid_group_team_map_error", errs[0].Message)
case validation.ErrInvalidBadgeSlug:
data["ErrorMsg"] = trName + l.TrString("form.invalid_slug_error")
default:
msg := errs[0].Classification
if msg != "" && errs[0].Message != "" {
msg += ": "
}
msg += errs[0].Message
if msg == "" {
msg = l.TrString("form.unknown_error")
}
data["ErrorMsg"] = trName + ": " + msg
}
return errs return errs
} }
-14
View File
@@ -4,8 +4,6 @@
package misc package misc
import ( import (
"net/http"
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/markup/markdown"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
@@ -36,12 +34,6 @@ func Markup(ctx *context.APIContext) {
// "$ref": "#/responses/validationError" // "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.MarkupOption) form := web.GetForm(ctx).(*api.MarkupOption)
if ctx.HasAPIError() {
ctx.APIError(http.StatusUnprocessableEntity, ctx.GetErrMsg())
return
}
mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck // form.Wiki is deprecated mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck // form.Wiki is deprecated
common.RenderMarkup(ctx.Base, ctx.Repo, mode, form.Text, form.Context, form.FilePath) common.RenderMarkup(ctx.Base, ctx.Repo, mode, form.Text, form.Context, form.FilePath)
} }
@@ -67,12 +59,6 @@ func Markdown(ctx *context.APIContext) {
// "$ref": "#/responses/validationError" // "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.MarkdownOption) form := web.GetForm(ctx).(*api.MarkdownOption)
if ctx.HasAPIError() {
ctx.APIError(http.StatusUnprocessableEntity, ctx.GetErrMsg())
return
}
mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck // form.Wiki is deprecated mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck // form.Wiki is deprecated
common.RenderMarkup(ctx.Base, ctx.Repo, mode, form.Text, form.Context, "") common.RenderMarkup(ctx.Base, ctx.Repo, mode, form.Text, form.Context, "")
} }
-5
View File
@@ -79,11 +79,6 @@ func Migrate(ctx *context.APIContext) {
return return
} }
if ctx.HasAPIError() {
ctx.APIError(http.StatusUnprocessableEntity, ctx.GetErrMsg())
return
}
if !ctx.Doer.IsAdmin { if !ctx.Doer.IsAdmin {
if !repoOwner.IsOrganization() && ctx.Doer.ID != repoOwner.ID { if !repoOwner.IsOrganization() && ctx.Doer.ID != repoOwner.ID {
ctx.APIError(http.StatusForbidden, "Given user is not an organization.") ctx.APIError(http.StatusForbidden, "Given user is not an organization.")
+11 -1
View File
@@ -18,6 +18,7 @@ import (
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/forms"
) )
// ListAccessTokens list all the access tokens // ListAccessTokens list all the access tokens
@@ -228,7 +229,10 @@ func CreateOauth2Application(ctx *context.APIContext) {
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
data := web.GetForm(ctx).(*api.CreateOAuth2ApplicationOptions) data := web.GetForm(ctx).(*api.CreateOAuth2ApplicationOptions)
if invalidURI := forms.DetectInvalidOAuth2ApplicationRedirectURI(data.RedirectURIs); invalidURI != "" {
ctx.APIError(http.StatusBadRequest, "invalid redirect URI: "+invalidURI)
return
}
app, err := auth_model.CreateOAuth2Application(ctx, auth_model.CreateOAuth2ApplicationOptions{ app, err := auth_model.CreateOAuth2Application(ctx, auth_model.CreateOAuth2ApplicationOptions{
Name: data.Name, Name: data.Name,
UserID: ctx.Doer.ID, UserID: ctx.Doer.ID,
@@ -382,11 +386,17 @@ func UpdateOauth2Application(ctx *context.APIContext) {
// responses: // responses:
// "200": // "200":
// "$ref": "#/responses/OAuth2Application" // "$ref": "#/responses/OAuth2Application"
// "400":
// "$ref": "#/responses/error"
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
appID := ctx.PathParamInt64("id") appID := ctx.PathParamInt64("id")
data := web.GetForm(ctx).(*api.CreateOAuth2ApplicationOptions) data := web.GetForm(ctx).(*api.CreateOAuth2ApplicationOptions)
if invalidURI := forms.DetectInvalidOAuth2ApplicationRedirectURI(data.RedirectURIs); invalidURI != "" {
ctx.APIError(http.StatusBadRequest, "invalid redirect URI: "+invalidURI)
return
}
app, err := auth_model.UpdateOAuth2Application(ctx, auth_model.UpdateOAuth2ApplicationOptions{ app, err := auth_model.UpdateOAuth2Application(ctx, auth_model.UpdateOAuth2ApplicationOptions{
Name: data.Name, Name: data.Name,
+2 -2
View File
@@ -72,7 +72,7 @@ func CreateCodeComment(ctx *context.Context) {
} }
if ctx.HasError() { if ctx.HasError() {
ctx.Flash.Error(ctx.Data["ErrorMsg"].(string)) ctx.Flash.Error(ctx.GetErrMsg())
ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index)) ctx.Redirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
return return
} }
@@ -230,7 +230,7 @@ func SubmitReview(ctx *context.Context) {
return return
} }
if ctx.HasError() { if ctx.HasError() {
ctx.Flash.Error(ctx.Data["ErrorMsg"].(string)) ctx.Flash.Error(ctx.GetErrMsg())
ctx.JSONRedirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index)) ctx.JSONRedirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
return return
} }
+1 -1
View File
@@ -13,7 +13,7 @@ import (
func GetLabelEditForm(ctx *context.Context) *forms.CreateLabelForm { func GetLabelEditForm(ctx *context.Context) *forms.CreateLabelForm {
form := web.GetForm(ctx).(*forms.CreateLabelForm) form := web.GetForm(ctx).(*forms.CreateLabelForm)
if ctx.HasError() { if ctx.HasError() {
ctx.JSONError(ctx.Data["ErrorMsg"].(string)) ctx.JSONError(ctx.GetErrMsg())
return nil return nil
} }
var err error var err error
-18
View File
@@ -322,24 +322,6 @@ func RepoRefForAPI(next http.Handler) http.Handler {
}) })
} }
// HasAPIError returns true if error occurs in form validation.
func (ctx *APIContext) HasAPIError() bool {
hasErr, ok := ctx.Data["HasError"]
if !ok {
return false
}
return hasErr.(bool)
}
// GetErrMsg returns error message in form validation.
func (ctx *APIContext) GetErrMsg() string {
msg, _ := ctx.Data["ErrorMsg"].(string)
if msg == "" {
msg = "invalid form data"
}
return msg
}
// NotFoundOrServerError use error check function to determine if the error // NotFoundOrServerError use error check function to determine if the error
// is about not found. It responds with 404 status code for not found error, // is about not found. It responds with 404 status code for not found error,
// or error context description for logging purpose of 500 server error. // or error context description for logging purpose of 500 server error.
+20 -1
View File
@@ -7,9 +7,13 @@ package forms
import ( import (
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"strings"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
@@ -356,14 +360,29 @@ func (f *NewAccessTokenForm) Validate(req *http.Request, errs binding.Errors) bi
// EditOAuth2ApplicationForm form for editing oauth2 applications // EditOAuth2ApplicationForm form for editing oauth2 applications
type EditOAuth2ApplicationForm struct { type EditOAuth2ApplicationForm struct {
Name string `binding:"Required;MaxSize(255)" form:"application_name"` Name string `binding:"Required;MaxSize(255)" form:"application_name"`
RedirectURIs string `binding:"Required;ValidUrlList" form:"redirect_uris"` RedirectURIs string `binding:"Required" form:"redirect_uris"`
ConfidentialClient bool `form:"confidential_client"` ConfidentialClient bool `form:"confidential_client"`
SkipSecondaryAuthorization bool `form:"skip_secondary_authorization"` SkipSecondaryAuthorization bool `form:"skip_secondary_authorization"`
} }
func DetectInvalidOAuth2ApplicationRedirectURI(uris []string) (invalidURL string) {
for _, u := range uris {
scheme, _, ok := strings.Cut(u, ":")
valid := ok && (validation.IsValidURL(u) || util.SliceContainsString(setting.OAuth2.CustomSchemes, scheme))
if !valid {
return u
}
}
return ""
}
// Validate validates the fields // Validate validates the fields
func (f *EditOAuth2ApplicationForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { func (f *EditOAuth2ApplicationForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req) ctx := context.GetValidateContext(req)
invalidURI := DetectInvalidOAuth2ApplicationRedirectURI(util.SplitTrimSpace(f.RedirectURIs, "\n"))
if invalidURI != "" {
errs = middleware.ReportValidationError(errs, ctx.Data, "RedirectURIs", binding.ERR_URL, ctx.Locale.TrString("form.url_error", invalidURI))
}
return middleware.Validate(errs, ctx.Data, f, ctx.Locale) return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
} }
+28 -7
View File
@@ -14,15 +14,9 @@ import (
) )
func TestRegisterForm_IsDomainAllowed_Empty(t *testing.T) { func TestRegisterForm_IsDomainAllowed_Empty(t *testing.T) {
oldService := setting.Service defer test.MockVariableValue(&setting.Service)()
defer func() {
setting.Service = oldService
}()
setting.Service.EmailDomainAllowList = nil setting.Service.EmailDomainAllowList = nil
form := RegisterForm{} form := RegisterForm{}
assert.True(t, form.IsEmailDomainAllowed()) assert.True(t, form.IsEmailDomainAllowed())
} }
@@ -87,3 +81,30 @@ func TestRegisterForm_IsDomainAllowed_BlockedEmail(t *testing.T) {
assert.Equal(t, v.valid, form.IsEmailDomainAllowed()) assert.Equal(t, v.valid, form.IsEmailDomainAllowed())
} }
} }
func TestDetectInvalidOAuth2ApplicationRedirectURI(t *testing.T) {
defer test.MockVariableValue(&setting.OAuth2.CustomSchemes)()
setting.OAuth2.CustomSchemes = []string{"my-app"}
assertValid := func(t *testing.T, s string, valid bool) {
ret := DetectInvalidOAuth2ApplicationRedirectURI([]string{s})
if valid {
assert.Empty(t, ret)
} else {
assert.Equal(t, s, ret)
}
}
assertValid(t, "my-app:", true)
assertValid(t, "my-app:/foo", true)
assertValid(t, "http://foo", true)
assertValid(t, "https://foo", true)
assertValid(t, "my-app", false)
assertValid(t, "ftp:", false)
assertValid(t, "ftp://foo", false)
assertValid(t, "https://[invalid", false)
ret := DetectInvalidOAuth2ApplicationRedirectURI([]string{"my-app:", "http://foo", "https://foo"})
assert.Empty(t, ret)
ret = DetectInvalidOAuth2ApplicationRedirectURI([]string{"my-app:", "http://foo", "invalid", "https://foo"})
assert.Equal(t, "invalid", ret)
}
+3
View File
@@ -19354,6 +19354,9 @@
"200": { "200": {
"$ref": "#/responses/OAuth2Application" "$ref": "#/responses/OAuth2Application"
}, },
"400": {
"$ref": "#/responses/error"
},
"404": { "404": {
"$ref": "#/responses/notFound" "$ref": "#/responses/notFound"
} }
+39 -74
View File
@@ -11,10 +11,13 @@ import (
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestOAuth2Application(t *testing.T) { func TestOAuth2Application(t *testing.T) {
@@ -28,18 +31,17 @@ func TestOAuth2Application(t *testing.T) {
func testAPICreateOAuth2Application(t *testing.T) { func testAPICreateOAuth2Application(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
appBody := api.CreateOAuth2ApplicationOptions{ redirectURIs := []string{"http://www.google.com", "my-app:foo"}
Name: "test-app-1", appBody := api.CreateOAuth2ApplicationOptions{Name: "test-app-1", RedirectURIs: redirectURIs, ConfidentialClient: true}
RedirectURIs: []string{
"http://www.google.com",
},
ConfidentialClient: true,
}
req := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &appBody). // no custom scheme
AddBasicAuth(user.Name) 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) resp := MakeRequest(t, req, http.StatusCreated)
createdApp := DecodeJSON(t, resp, &api.OAuth2Application{}) createdApp := DecodeJSON(t, resp, &api.OAuth2Application{})
assert.Equal(t, appBody.Name, createdApp.Name) assert.Equal(t, appBody.Name, createdApp.Name)
@@ -47,7 +49,7 @@ func testAPICreateOAuth2Application(t *testing.T) {
assert.Len(t, createdApp.ClientID, 36) assert.Len(t, createdApp.ClientID, 36)
assert.True(t, createdApp.ConfidentialClient) assert.True(t, createdApp.ConfidentialClient)
assert.NotEmpty(t, createdApp.Created) 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}) 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) session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser)
existApp := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ existApp := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{UID: user.ID, Name: "test-app-1", ConfidentialClient: true})
UID: user.ID, require.NotEmpty(t, existApp.RedirectURIs)
Name: "test-app-1",
RedirectURIs: []string{
"http://www.google.com",
},
ConfidentialClient: true,
})
req := NewRequest(t, "GET", "/api/v1/user/applications/oauth2"). req := NewRequest(t, "GET", "/api/v1/user/applications/oauth2").AddTokenAuth(token)
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK) resp := MakeRequest(t, req, http.StatusOK)
var appList api.OAuth2ApplicationList appList := DecodeJSON(t, resp, api.OAuth2ApplicationList{})
DecodeJSON(t, resp, &appList)
expectedApp := appList[0] expectedApp := appList[0]
assert.Equal(t, expectedApp.Name, existApp.Name) assert.Equal(t, expectedApp.Name, existApp.Name)
@@ -78,7 +72,7 @@ func testAPIListOAuth2Applications(t *testing.T) {
assert.Equal(t, expectedApp.ConfidentialClient, existApp.ConfidentialClient) assert.Equal(t, expectedApp.ConfidentialClient, existApp.ConfidentialClient)
assert.Len(t, expectedApp.ClientID, 36) assert.Len(t, expectedApp.ClientID, 36)
assert.Empty(t, expectedApp.ClientSecret) 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}) 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) session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser)
oldApp := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ oldApp := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{UID: user.ID, Name: "test-app-1"})
UID: user.ID,
Name: "test-app-1",
})
urlStr := fmt.Sprintf("/api/v1/user/applications/oauth2/%d", oldApp.ID) urlStr := fmt.Sprintf("/api/v1/user/applications/oauth2/%d", oldApp.ID)
req := NewRequest(t, "DELETE", urlStr). req := NewRequest(t, "DELETE", urlStr).AddTokenAuth(token)
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent) MakeRequest(t, req, http.StatusNoContent)
unittest.AssertNotExistsBean(t, &auth_model.OAuth2Application{UID: oldApp.UID, Name: oldApp.Name}) unittest.AssertNotExistsBean(t, &auth_model.OAuth2Application{UID: oldApp.UID, Name: oldApp.Name})
// Delete again will return not found // Delete again will return not found
req = NewRequest(t, "DELETE", urlStr). req = NewRequest(t, "DELETE", urlStr).AddTokenAuth(token)
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound) MakeRequest(t, req, http.StatusNotFound)
} }
@@ -110,65 +99,41 @@ func testAPIGetOAuth2Application(t *testing.T) {
session := loginUser(t, user.Name) session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser)
existApp := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ existApp := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{UID: user.ID, Name: "test-app-1", ConfidentialClient: true})
UID: user.ID, require.NotEmpty(t, existApp.RedirectURIs)
Name: "test-app-1",
RedirectURIs: []string{
"http://www.google.com",
},
ConfidentialClient: true,
})
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/applications/oauth2/%d", existApp.ID)). req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/applications/oauth2/%d", existApp.ID)).AddTokenAuth(token)
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK) resp := MakeRequest(t, req, http.StatusOK)
expectedApp := DecodeJSON(t, resp, &api.OAuth2Application{})
var app api.OAuth2Application
DecodeJSON(t, resp, &app)
expectedApp := app
assert.Equal(t, expectedApp.Name, existApp.Name) assert.Equal(t, expectedApp.Name, existApp.Name)
assert.Equal(t, expectedApp.ClientID, existApp.ClientID) assert.Equal(t, expectedApp.ClientID, existApp.ClientID)
assert.Equal(t, expectedApp.ConfidentialClient, existApp.ConfidentialClient) assert.Equal(t, expectedApp.ConfidentialClient, existApp.ConfidentialClient)
assert.Len(t, expectedApp.ClientID, 36) assert.Len(t, expectedApp.ClientID, 36)
assert.Empty(t, expectedApp.ClientSecret) assert.Empty(t, expectedApp.ClientSecret)
assert.Len(t, expectedApp.RedirectURIs, 1) assert.Equal(t, expectedApp.RedirectURIs, existApp.RedirectURIs)
assert.Equal(t, expectedApp.RedirectURIs[0], existApp.RedirectURIs[0])
unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: expectedApp.ID, Name: expectedApp.Name}) unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: expectedApp.ID, Name: expectedApp.Name})
} }
func testAPIUpdateOAuth2Application(t *testing.T) { func testAPIUpdateOAuth2Application(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
existApp := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ existApp := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{UID: user.ID, Name: "test-app-1"})
UID: user.ID, redirectURIs := []string{"https://www.google.com", "my-app:foo"}
Name: "test-app-1", appBody := api.CreateOAuth2ApplicationOptions{Name: "test-app-1", RedirectURIs: redirectURIs, ConfidentialClient: true}
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,
}
urlStr := fmt.Sprintf("/api/v1/user/applications/oauth2/%d", existApp.ID) 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) resp := MakeRequest(t, req, http.StatusOK)
var app api.OAuth2Application expectedApp := DecodeJSON(t, resp, &api.OAuth2Application{})
DecodeJSON(t, resp, &app) assert.Equal(t, expectedApp.RedirectURIs, appBody.RedirectURIs)
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])
assert.Equal(t, expectedApp.ConfidentialClient, appBody.ConfidentialClient) assert.Equal(t, expectedApp.ConfidentialClient, appBody.ConfidentialClient)
unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: expectedApp.ID, Name: expectedApp.Name}) 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" "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth/source/oauth2" "code.gitea.io/gitea/services/auth/source/oauth2"
"code.gitea.io/gitea/services/oauth2_provider" "code.gitea.io/gitea/services/oauth2_provider"
@@ -35,17 +36,57 @@ import (
"github.com/stretchr/testify/require" "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)() defer tests.PrepareTestEnv(t)()
t.Run("AuthorizeNoClientID", testAuthorizeNoClientID) t.Run("Provider", func(t *testing.T) {
t.Run("AuthorizeUnregisteredRedirect", testAuthorizeUnregisteredRedirect) t.Run("AuthorizeNoClientID", testAuthorizeNoClientID)
t.Run("AuthorizeUnsupportedResponseType", testAuthorizeUnsupportedResponseType) t.Run("AuthorizeUnregisteredRedirect", testAuthorizeUnregisteredRedirect)
t.Run("AuthorizeUnsupportedCodeChallengeMethod", testAuthorizeUnsupportedCodeChallengeMethod) t.Run("AuthorizeUnsupportedResponseType", testAuthorizeUnsupportedResponseType)
t.Run("AuthorizeLoginRedirect", testAuthorizeLoginRedirect) t.Run("AuthorizeUnsupportedCodeChallengeMethod", testAuthorizeUnsupportedCodeChallengeMethod)
t.Run("AuthorizeLoginRedirect", testAuthorizeLoginRedirect)
t.Run("OAuth2WellKnown", testOAuth2WellKnown) t.Run("AuthorizeShow", testAuthorizeShow)
t.Run("OAuthSourceSpecialChars", testOAuthSourceSpecialChars) 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 // 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) { 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") ctx := loginUser(t, "user1")
resp := ctx.MakeRequest(t, req, http.StatusSeeOther) resp := ctx.MakeRequest(t, req, http.StatusSeeOther)
u, err := resp.Result().Location() u, err := resp.Result().Location()
@@ -74,7 +115,7 @@ func testAuthorizeUnsupportedResponseType(t *testing.T) {
} }
func testAuthorizeUnsupportedCodeChallengeMethod(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") ctx := loginUser(t, "user1")
resp := ctx.MakeRequest(t, req, http.StatusSeeOther) resp := ctx.MakeRequest(t, req, http.StatusSeeOther)
u, err := resp.Result().Location() 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") assert.Contains(t, MakeRequest(t, req, http.StatusSeeOther).Body.String(), "/user/login")
} }
func TestAuthorizeShow(t *testing.T) { 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=https://example.com&response_type=code&state=thestate")
req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=a&response_type=code&state=thestate")
ctx := loginUser(t, "user4") ctx := loginUser(t, "user4")
resp := ctx.MakeRequest(t, req, http.StatusOK) resp := ctx.MakeRequest(t, req, http.StatusOK)
@@ -98,11 +138,10 @@ func TestAuthorizeShow(t *testing.T) {
AssertHTMLElement(t, htmlDoc, "#authorize-app", true) AssertHTMLElement(t, htmlDoc, "#authorize-app", true)
} }
func TestAuthorizeGrantS256RequiresVerifier(t *testing.T) { func testAuthorizeGrantS256RequiresVerifier(t *testing.T) {
defer tests.PrepareTestEnv(t)()
ctx := loginUser(t, "user4") ctx := loginUser(t, "user4")
codeChallenge := "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg" 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) resp := ctx.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body) htmlDoc := NewHTMLParser(t, resp.Body)
@@ -113,7 +152,7 @@ func TestAuthorizeGrantS256RequiresVerifier(t *testing.T) {
"state": "thestate", "state": "thestate",
"scope": "", "scope": "",
"nonce": "", "nonce": "",
"redirect_uri": "a", "redirect_uri": "https://example.com",
"granted": "true", "granted": "true",
}) })
grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther) grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther)
@@ -126,7 +165,7 @@ func TestAuthorizeGrantS256RequiresVerifier(t *testing.T) {
"grant_type": "authorization_code", "grant_type": "authorization_code",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138", "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=", "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a", "redirect_uri": "https://example.com",
"code": code, "code": code,
}) })
accessResp := MakeRequest(t, accessReq, http.StatusBadRequest) accessResp := MakeRequest(t, accessReq, http.StatusBadRequest)
@@ -136,9 +175,8 @@ func TestAuthorizeGrantS256RequiresVerifier(t *testing.T) {
assert.Equal(t, "failed PKCE code challenge", parsedError.ErrorDescription) assert.Equal(t, "failed PKCE code challenge", parsedError.ErrorDescription)
} }
func TestAuthorizeRedirectWithExistingGrant(t *testing.T) { 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://example.com/&response_type=code&state=thestate")
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")
ctx := loginUser(t, "user1") ctx := loginUser(t, "user1")
resp := ctx.MakeRequest(t, req, http.StatusSeeOther) resp := ctx.MakeRequest(t, req, http.StatusSeeOther)
u, err := resp.Result().Location() u, err := resp.Result().Location()
@@ -146,11 +184,11 @@ func TestAuthorizeRedirectWithExistingGrant(t *testing.T) {
assert.Equal(t, "thestate", u.Query().Get("state")) 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")) assert.Greaterf(t, len(u.Query().Get("code")), 30, "authorization code '%s' should be longer then 30", u.Query().Get("code"))
u.RawQuery = "" u.RawQuery = ""
assert.Equal(t, "https://example.com/xyzzy", u.String()) assert.Equal(t, "https://example.com/", u.String())
} }
func TestAuthorizePKCERequiredForPublicClient(t *testing.T) { func testAuthorizePKCERequiredForPublicClient(t *testing.T) {
defer tests.PrepareTestEnv(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") 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") ctx := loginUser(t, "user1")
resp := ctx.MakeRequest(t, req, http.StatusSeeOther) 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")) assert.Equal(t, "PKCE is required for public clients", u.Query().Get("error_description"))
} }
func TestAccessTokenExchange(t *testing.T) { func testAccessTokenExchange(t *testing.T) {
defer tests.PrepareTestEnv(t)() testOAuth2PrepareTestCode(t)
req := 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", "grant_type": "authorization_code",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138", "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=", "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a", "redirect_uri": "https://example.com",
"code": "authcode", "code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
}) })
@@ -184,8 +222,8 @@ func TestAccessTokenExchange(t *testing.T) {
assert.Greater(t, len(parsed.RefreshToken), 10) assert.Greater(t, len(parsed.RefreshToken), 10)
} }
func TestAccessTokenExchangeWithPublicClient(t *testing.T) { func testAccessTokenExchangeWithPublicClient(t *testing.T) {
defer tests.PrepareTestEnv(t)() testOAuth2PrepareTestCode(t)
req := 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", "grant_type": "authorization_code",
"client_id": "ce5a1322-42a7-11ed-b878-0242ac120002", "client_id": "ce5a1322-42a7-11ed-b878-0242ac120002",
@@ -207,13 +245,13 @@ func TestAccessTokenExchangeWithPublicClient(t *testing.T) {
assert.Greater(t, len(parsed.RefreshToken), 10) assert.Greater(t, len(parsed.RefreshToken), 10)
} }
func TestAccessTokenExchangeJSON(t *testing.T) { func testAccessTokenExchangeJSON(t *testing.T) {
defer tests.PrepareTestEnv(t)() testOAuth2PrepareTestCode(t)
req := NewRequestWithJSON(t, "POST", "/login/oauth/access_token", map[string]string{ req := NewRequestWithJSON(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code", "grant_type": "authorization_code",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138", "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=", "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a", "redirect_uri": "https://example.com",
"code": "authcode", "code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
}) })
@@ -231,13 +269,13 @@ func TestAccessTokenExchangeJSON(t *testing.T) {
assert.Greater(t, len(parsed.RefreshToken), 10) assert.Greater(t, len(parsed.RefreshToken), 10)
} }
func TestAccessTokenExchangeWithoutPKCE(t *testing.T) { func testAccessTokenExchangeWithoutPKCE(t *testing.T) {
defer tests.PrepareTestEnv(t)() testOAuth2PrepareTestCode(t)
req := 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", "grant_type": "authorization_code",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138", "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=", "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a", "redirect_uri": "https://example.com",
"code": "authcode", "code": "authcode",
}) })
resp := MakeRequest(t, req, http.StatusBadRequest) resp := MakeRequest(t, req, http.StatusBadRequest)
@@ -247,14 +285,14 @@ func TestAccessTokenExchangeWithoutPKCE(t *testing.T) {
assert.Equal(t, "failed PKCE code challenge", parsedError.ErrorDescription) assert.Equal(t, "failed PKCE code challenge", parsedError.ErrorDescription)
} }
func TestAccessTokenExchangeWithInvalidCredentials(t *testing.T) { func testAccessTokenExchangeWithInvalidCredentials(t *testing.T) {
defer tests.PrepareTestEnv(t)() testOAuth2PrepareTestCode(t)
// invalid client id // invalid client id
req := 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", "grant_type": "authorization_code",
"client_id": "???", "client_id": "???",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=", "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a", "redirect_uri": "https://example.com",
"code": "authcode", "code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
}) })
@@ -269,7 +307,7 @@ func TestAccessTokenExchangeWithInvalidCredentials(t *testing.T) {
"grant_type": "authorization_code", "grant_type": "authorization_code",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138", "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "???", "client_secret": "???",
"redirect_uri": "a", "redirect_uri": "https://example.com",
"code": "authcode", "code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
}) })
@@ -299,7 +337,7 @@ func TestAccessTokenExchangeWithInvalidCredentials(t *testing.T) {
"grant_type": "authorization_code", "grant_type": "authorization_code",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138", "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=", "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a", "redirect_uri": "https://example.com",
"code": "???", "code": "???",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
}) })
@@ -314,7 +352,7 @@ func TestAccessTokenExchangeWithInvalidCredentials(t *testing.T) {
"grant_type": "???", "grant_type": "???",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138", "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=", "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a", "redirect_uri": "https://example.com",
"code": "authcode", "code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", "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) assert.Equal(t, "Only refresh_token or authorization_code grant type is supported", parsedError.ErrorDescription)
} }
func TestAccessTokenExchangeWithBasicAuth(t *testing.T) { func testAccessTokenExchangeWithBasicAuth(t *testing.T) {
defer tests.PrepareTestEnv(t)() testOAuth2PrepareTestCode(t)
req := 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", "grant_type": "authorization_code",
"redirect_uri": "a", "redirect_uri": "https://example.com",
"code": "authcode", "code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
}) })
@@ -350,7 +388,7 @@ func TestAccessTokenExchangeWithBasicAuth(t *testing.T) {
// use wrong client_secret // use wrong client_secret
req = 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", "grant_type": "authorization_code",
"redirect_uri": "a", "redirect_uri": "https://example.com",
"code": "authcode", "code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
}) })
@@ -364,7 +402,7 @@ func TestAccessTokenExchangeWithBasicAuth(t *testing.T) {
// missing header // missing header
req = 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", "grant_type": "authorization_code",
"redirect_uri": "a", "redirect_uri": "https://example.com",
"code": "authcode", "code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
}) })
@@ -377,7 +415,7 @@ func TestAccessTokenExchangeWithBasicAuth(t *testing.T) {
// client_id inconsistent with Authorization header // client_id inconsistent with Authorization header
req = 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", "grant_type": "authorization_code",
"redirect_uri": "a", "redirect_uri": "https://example.com",
"code": "authcode", "code": "authcode",
"client_id": "inconsistent", "client_id": "inconsistent",
}) })
@@ -391,7 +429,7 @@ func TestAccessTokenExchangeWithBasicAuth(t *testing.T) {
// client_secret inconsistent with Authorization header // client_secret inconsistent with Authorization header
req = 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", "grant_type": "authorization_code",
"redirect_uri": "a", "redirect_uri": "https://example.com",
"code": "authcode", "code": "authcode",
"client_secret": "inconsistent", "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) assert.Equal(t, "client_secret in request body inconsistent with Authorization header", parsedError.ErrorDescription)
} }
func TestRefreshTokenInvalidation(t *testing.T) { func testRefreshTokenInvalidation(t *testing.T) {
defer tests.PrepareTestEnv(t)() testOAuth2PrepareTestCode(t)
req := 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", "grant_type": "authorization_code",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138", "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=", "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a", "redirect_uri": "https://example.com",
"code": "authcode", "code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
}) })
@@ -431,7 +469,7 @@ func TestRefreshTokenInvalidation(t *testing.T) {
"grant_type": "refresh_token", "grant_type": "refresh_token",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138", "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
// omit secret // omit secret
"redirect_uri": "a", "redirect_uri": "https://example.com",
"refresh_token": parsed.RefreshToken, "refresh_token": parsed.RefreshToken,
}) })
resp = MakeRequest(t, req, http.StatusBadRequest) resp = MakeRequest(t, req, http.StatusBadRequest)
@@ -444,7 +482,7 @@ func TestRefreshTokenInvalidation(t *testing.T) {
"grant_type": "refresh_token", "grant_type": "refresh_token",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138", "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=", "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a", "redirect_uri": "https://example.com",
"refresh_token": "UNEXPECTED", "refresh_token": "UNEXPECTED",
}) })
resp = MakeRequest(t, req, http.StatusBadRequest) resp = MakeRequest(t, req, http.StatusBadRequest)
@@ -457,7 +495,7 @@ func TestRefreshTokenInvalidation(t *testing.T) {
"grant_type": "refresh_token", "grant_type": "refresh_token",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138", "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=", "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a", "redirect_uri": "https://example.com",
"refresh_token": parsed.RefreshToken, "refresh_token": parsed.RefreshToken,
}) })
@@ -484,13 +522,13 @@ func TestRefreshTokenInvalidation(t *testing.T) {
assert.Equal(t, "token was already used", parsedError.ErrorDescription) assert.Equal(t, "token was already used", parsedError.ErrorDescription)
} }
func TestOAuthIntrospection(t *testing.T) { func testOAuthIntrospection(t *testing.T) {
defer tests.PrepareTestEnv(t)() testOAuth2PrepareTestCode(t)
req := 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", "grant_type": "authorization_code",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138", "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=", "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a", "redirect_uri": "https://example.com",
"code": "authcode", "code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt", "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
}) })
@@ -542,14 +580,12 @@ func TestOAuthIntrospection(t *testing.T) {
assert.Contains(t, resp.Body.String(), "no valid authorization") assert.Contains(t, resp.Body.String(), "no valid authorization")
} }
func TestOAuth_GrantScopesReadUserFailRepos(t *testing.T) { func testOAuthGrantScopesReadUserFailRepos(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
appBody := api.CreateOAuth2ApplicationOptions{ appBody := api.CreateOAuth2ApplicationOptions{
Name: "oauth-provider-scopes-test", Name: "oauth-provider-scopes-test",
RedirectURIs: []string{ RedirectURIs: []string{
"a", "https://example.com",
}, },
ConfidentialClient: true, ConfidentialClient: true,
} }
@@ -573,7 +609,7 @@ func TestOAuth_GrantScopesReadUserFailRepos(t *testing.T) {
ctx := loginUser(t, user.Name) 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) authorizeReq := NewRequest(t, "GET", authorizeURL)
authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther) authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
@@ -583,7 +619,7 @@ func TestOAuth_GrantScopesReadUserFailRepos(t *testing.T) {
"grant_type": "authorization_code", "grant_type": "authorization_code",
"client_id": app.ClientID, "client_id": app.ClientID,
"client_secret": app.ClientSecret, "client_secret": app.ClientSecret,
"redirect_uri": "a", "redirect_uri": "https://example.com",
"code": authcode, "code": authcode,
}) })
accessTokenResp := ctx.MakeRequest(t, accessTokenReq, 200) 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]") 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) { func testOAuthGrantScopesReadRepositoryFailOrganization(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
appBody := api.CreateOAuth2ApplicationOptions{ appBody := api.CreateOAuth2ApplicationOptions{
Name: "oauth-provider-scopes-test", Name: "oauth-provider-scopes-test",
RedirectURIs: []string{ RedirectURIs: []string{
"a", "https://example.com",
}, },
ConfidentialClient: true, ConfidentialClient: true,
} }
@@ -653,7 +687,7 @@ func TestOAuth_GrantScopesReadRepositoryFailOrganization(t *testing.T) {
ctx := loginUser(t, user.Name) 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) authorizeReq := NewRequest(t, "GET", authorizeURL)
authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther) authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
@@ -662,7 +696,7 @@ func TestOAuth_GrantScopesReadRepositoryFailOrganization(t *testing.T) {
"grant_type": "authorization_code", "grant_type": "authorization_code",
"client_id": app.ClientID, "client_id": app.ClientID,
"client_secret": app.ClientSecret, "client_secret": app.ClientSecret,
"redirect_uri": "a", "redirect_uri": "https://example.com",
"code": authcode, "code": authcode,
}) })
accessTokenResp := ctx.MakeRequest(t, accessTokenReq, http.StatusOK) 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]") 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) { func testOAuthGrantScopesClaimPublicOnlyGroups(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
appBody := api.CreateOAuth2ApplicationOptions{ appBody := api.CreateOAuth2ApplicationOptions{
Name: "oauth-provider-scopes-test", Name: "oauth-provider-scopes-test",
RedirectURIs: []string{ RedirectURIs: []string{
"a", "https://example.com",
}, },
ConfidentialClient: true, ConfidentialClient: true,
} }
@@ -792,7 +824,7 @@ func TestOAuth_GrantScopesClaimPublicOnlyGroups(t *testing.T) {
ctx := loginUser(t, user.Name) 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) authorizeReq := NewRequest(t, "GET", authorizeURL)
authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther) authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
@@ -802,7 +834,7 @@ func TestOAuth_GrantScopesClaimPublicOnlyGroups(t *testing.T) {
"grant_type": "authorization_code", "grant_type": "authorization_code",
"client_id": app.ClientID, "client_id": app.ClientID,
"client_secret": app.ClientSecret, "client_secret": app.ClientSecret,
"redirect_uri": "a", "redirect_uri": "https://example.com",
"code": authcode, "code": authcode,
}) })
accessTokenResp := ctx.MakeRequest(t, accessTokenReq, http.StatusOK) accessTokenResp := ctx.MakeRequest(t, accessTokenReq, http.StatusOK)
@@ -860,15 +892,13 @@ func TestOAuth_GrantScopesClaimPublicOnlyGroups(t *testing.T) {
} }
} }
func TestOAuth_GrantScopesClaimAllGroups(t *testing.T) { func testOAuthGrantScopesClaimAllGroups(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
appBody := api.CreateOAuth2ApplicationOptions{ appBody := api.CreateOAuth2ApplicationOptions{
Name: "oauth-provider-scopes-test", Name: "oauth-provider-scopes-test",
RedirectURIs: []string{ RedirectURIs: []string{
"a", "https://example.com",
}, },
ConfidentialClient: true, ConfidentialClient: true,
} }
@@ -892,7 +922,7 @@ func TestOAuth_GrantScopesClaimAllGroups(t *testing.T) {
ctx := loginUser(t, user.Name) 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) authorizeReq := NewRequest(t, "GET", authorizeURL)
authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther) authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
@@ -902,7 +932,7 @@ func TestOAuth_GrantScopesClaimAllGroups(t *testing.T) {
"grant_type": "authorization_code", "grant_type": "authorization_code",
"client_id": app.ClientID, "client_id": app.ClientID,
"client_secret": app.ClientSecret, "client_secret": app.ClientSecret,
"redirect_uri": "a", "redirect_uri": "https://example.com",
"code": authcode, "code": authcode,
}) })
accessTokenResp := ctx.MakeRequest(t, accessTokenReq, http.StatusOK) 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) require.NoError(t, err)
} }
func createMockServer() *httptest.Server { func createOAuth2MockProvider() *httptest.Server {
var mockServer *httptest.Server var mockServer *httptest.Server
mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path { switch r.URL.Path {
@@ -1017,10 +1047,8 @@ func createMockServer() *httptest.Server {
return mockServer return mockServer
} }
func TestSignInOauthCallbackSyncSSHKeys(t *testing.T) { func testSignInOauthCallbackSyncSSHKeys(t *testing.T) {
defer tests.PrepareTestEnv(t)() mockServer := createOAuth2MockProvider()
mockServer := createMockServer()
defer mockServer.Close() defer mockServer.Close()
ctx := t.Context() ctx := t.Context()
@@ -1100,7 +1128,7 @@ func TestSignInOauthCallbackSyncSSHKeys(t *testing.T) {
// Checks if an OAuth provider with spaces within the name does work, // Checks if an OAuth provider with spaces within the name does work,
// with the encoding of its names in the URL (PR#37327) // with the encoding of its names in the URL (PR#37327)
func testOAuthSourceSpecialChars(t *testing.T) { func testOAuthSourceSpecialChars(t *testing.T) {
mockServer := createMockServer() mockServer := createOAuth2MockProvider()
defer mockServer.Close() defer mockServer.Close()
addOAuth2Source(t, "test space", oauth2.Source{ 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/container"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -311,18 +312,25 @@ func TestUserSettingsApplications(t *testing.T) {
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body) doc := NewHTMLParser(t, resp.Body)
msg := strings.TrimSpace(doc.Find(".ui.message.flash-message").Text()) 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) { t.Run("OK", func(t *testing.T) {
defer tests.PrintCurrentTest(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{ req := NewRequestWithValues(t, "POST", "/user/settings/applications/oauth2/2", map[string]string{
"application_name": "Test native app", "application_name": "Test native app",
"redirect_uris": "http://127.0.0.1", "redirect_uris": "http://127.0.0.1",
"confidential_client": "false", "confidential_client": "false",
}) })
session.MakeRequest(t, req, http.StatusSeeOther) 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)
}) })
}) })
}) })