Skip to content

Commit b38e25b

Browse files
committed
fix
1 parent 6fe5c4c commit b38e25b

File tree

9 files changed

+136
-81
lines changed

9 files changed

+136
-81
lines changed

modules/auth/httpauth/httpauth.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package httpauth
5+
6+
import (
7+
"encoding/base64"
8+
"strings"
9+
10+
"code.gitea.io/gitea/modules/util"
11+
)
12+
13+
func ParseAuthorizationHeaderBasic(header string) (string, string, bool) {
14+
parts := strings.Fields(header)
15+
if len(parts) != 2 {
16+
return "", "", false
17+
}
18+
if !util.AsciiEqualFold(parts[0], "basic") {
19+
return "", "", false
20+
}
21+
s, err := base64.StdEncoding.DecodeString(parts[1])
22+
if err != nil {
23+
return "", "", false
24+
}
25+
if u, p, ok := strings.Cut(string(s), ":"); ok {
26+
return u, p, true
27+
}
28+
return "", "", false
29+
}
30+
31+
func ParseAuthorizationHeaderBearerToken(header string) (string, bool) {
32+
parts := strings.Fields(header)
33+
if len(parts) != 2 {
34+
return "", false
35+
}
36+
if util.AsciiEqualFold(parts[0], "token") || util.AsciiEqualFold(parts[0], "bearer") {
37+
return parts[1], true
38+
}
39+
return "", false
40+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package httpauth
5+
6+
import (
7+
"encoding/base64"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestParseAuthorizationHeader(t *testing.T) {
14+
t.Run("Basic", func(t *testing.T) {
15+
cases := []struct {
16+
headerValue string
17+
user, pass string
18+
ok bool
19+
}{
20+
{"", "", "", false},
21+
{"?", "", "", false},
22+
{"foo", "", "", false},
23+
{"Basic ?", "", "", false},
24+
{"Basic " + base64.StdEncoding.EncodeToString([]byte("foo")), "", "", false},
25+
{"Basic " + base64.StdEncoding.EncodeToString([]byte("foo:bar")), "foo", "bar", true},
26+
{"basic " + base64.StdEncoding.EncodeToString([]byte("foo:bar")), "foo", "bar", true},
27+
}
28+
for _, c := range cases {
29+
user, pass, ok := ParseAuthorizationHeaderBasic(c.headerValue)
30+
assert.Equal(t, c.ok, ok, "header %q", c.headerValue)
31+
assert.Equal(t, c.user, user, "header %q", c.headerValue)
32+
assert.Equal(t, c.pass, pass, "header %q", c.headerValue)
33+
}
34+
})
35+
t.Run("BearerToken", func(t *testing.T) {
36+
cases := []struct {
37+
headerValue string
38+
expected string
39+
ok bool
40+
}{
41+
{"", "", false},
42+
{"?", "", false},
43+
{"any value", "", false},
44+
{"token value", "value", true},
45+
{"Token value", "value", true},
46+
{"bearer value", "value", true},
47+
{"Bearer value", "value", true},
48+
{"Bearer wrong value", "", false},
49+
}
50+
for _, c := range cases {
51+
token, ok := ParseAuthorizationHeaderBearerToken(c.headerValue)
52+
assert.Equal(t, c.ok, ok, "header %q", c.headerValue)
53+
assert.Equal(t, c.expected, token, "header %q", c.headerValue)
54+
}
55+
})
56+
}

modules/base/tool.go

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,10 @@ import (
88
"crypto/sha1"
99
"crypto/sha256"
1010
"crypto/subtle"
11-
"encoding/base64"
1211
"encoding/hex"
13-
"errors"
1412
"fmt"
1513
"hash"
1614
"strconv"
17-
"strings"
1815
"time"
1916

2017
"code.gitea.io/gitea/modules/setting"
@@ -36,19 +33,6 @@ func ShortSha(sha1 string) string {
3633
return util.TruncateRunes(sha1, 10)
3734
}
3835

39-
// BasicAuthDecode decode basic auth string
40-
func BasicAuthDecode(encoded string) (string, string, error) {
41-
s, err := base64.StdEncoding.DecodeString(encoded)
42-
if err != nil {
43-
return "", "", err
44-
}
45-
46-
if username, password, ok := strings.Cut(string(s), ":"); ok {
47-
return username, password, nil
48-
}
49-
return "", "", errors.New("invalid basic authentication")
50-
}
51-
5236
// VerifyTimeLimitCode verify time limit code
5337
func VerifyTimeLimitCode(now time.Time, data string, minutes int, code string) bool {
5438
if len(code) <= 18 {

modules/base/tool_test.go

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,25 +26,6 @@ func TestShortSha(t *testing.T) {
2626
assert.Equal(t, "veryverylo", ShortSha("veryverylong"))
2727
}
2828

29-
func TestBasicAuthDecode(t *testing.T) {
30-
_, _, err := BasicAuthDecode("?")
31-
assert.Equal(t, "illegal base64 data at input byte 0", err.Error())
32-
33-
user, pass, err := BasicAuthDecode("Zm9vOmJhcg==")
34-
assert.NoError(t, err)
35-
assert.Equal(t, "foo", user)
36-
assert.Equal(t, "bar", pass)
37-
38-
_, _, err = BasicAuthDecode("aW52YWxpZA==")
39-
assert.Error(t, err)
40-
41-
_, _, err = BasicAuthDecode("invalid")
42-
assert.Error(t, err)
43-
44-
_, _, err = BasicAuthDecode("YWxpY2U=") // "alice", no colon
45-
assert.Error(t, err)
46-
}
47-
4829
func TestVerifyTimeLimitCode(t *testing.T) {
4930
defer test.MockVariableValue(&setting.InstallLock, true)()
5031
initGeneralSecret := func(secret string) {

modules/util/string.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,24 @@ func SplitTrimSpace(input, sep string) []string {
110110
}
111111
return stringList
112112
}
113+
114+
func asciiLower(b byte) byte {
115+
if 'A' <= b && b <= 'Z' {
116+
return b + ('a' - 'A')
117+
}
118+
return b
119+
}
120+
121+
// AsciiEqualFold is from Golang https://cs.opensource.google/go/go/+/refs/tags/go1.24.4:src/net/http/internal/ascii/print.go
122+
// ASCII only. In most cases for protocols, we should only use this but not [strings.EqualFold]
123+
func AsciiEqualFold(s, t string) bool { //nolint:revive // PascalCase
124+
if len(s) != len(t) {
125+
return false
126+
}
127+
for i := 0; i < len(s); i++ {
128+
if asciiLower(s[i]) != asciiLower(t[i]) {
129+
return false
130+
}
131+
}
132+
return true
133+
}

routers/web/auth/oauth2_provider.go

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,16 @@
44
package auth
55

66
import (
7-
"errors"
87
"fmt"
98
"html"
109
"html/template"
1110
"net/http"
1211
"net/url"
1312
"strconv"
14-
"strings"
1513

1614
"code.gitea.io/gitea/models/auth"
1715
user_model "code.gitea.io/gitea/models/user"
18-
"code.gitea.io/gitea/modules/base"
16+
"code.gitea.io/gitea/modules/auth/httpauth"
1917
"code.gitea.io/gitea/modules/json"
2018
"code.gitea.io/gitea/modules/log"
2119
"code.gitea.io/gitea/modules/setting"
@@ -108,9 +106,8 @@ func InfoOAuth(ctx *context.Context) {
108106

109107
var accessTokenScope auth.AccessTokenScope
110108
if auHead := ctx.Req.Header.Get("Authorization"); auHead != "" {
111-
auths := strings.Fields(auHead)
112-
if len(auths) == 2 && (auths[0] == "token" || strings.ToLower(auths[0]) == "bearer") {
113-
accessTokenScope, _ = auth_service.GetOAuthAccessTokenScopeAndUserID(ctx, auths[1])
109+
if headerAuthToken, ok := httpauth.ParseAuthorizationHeaderBearerToken(auHead); ok {
110+
accessTokenScope, _ = auth_service.GetOAuthAccessTokenScopeAndUserID(ctx, headerAuthToken)
114111
}
115112
}
116113

@@ -127,18 +124,11 @@ func InfoOAuth(ctx *context.Context) {
127124
ctx.JSON(http.StatusOK, response)
128125
}
129126

130-
func parseBasicAuth(ctx *context.Context) (username, password string, err error) {
131-
authHeader := ctx.Req.Header.Get("Authorization")
132-
if authType, authData, ok := strings.Cut(authHeader, " "); ok && strings.EqualFold(authType, "Basic") {
133-
return base.BasicAuthDecode(authData)
134-
}
135-
return "", "", errors.New("invalid basic authentication")
136-
}
137-
138127
// IntrospectOAuth introspects an oauth token
139128
func IntrospectOAuth(ctx *context.Context) {
140129
clientIDValid := false
141-
if clientID, clientSecret, err := parseBasicAuth(ctx); err == nil {
130+
authHeader := ctx.Req.Header.Get("Authorization")
131+
if clientID, clientSecret, ok := httpauth.ParseAuthorizationHeaderBasic(authHeader); ok {
142132
app, err := auth.GetOAuth2ApplicationByClientID(ctx, clientID)
143133
if err != nil && !auth.IsErrOauthClientIDInvalid(err) {
144134
// this is likely a database error; log it and respond without details
@@ -465,10 +455,9 @@ func AccessTokenOAuth(ctx *context.Context) {
465455
form := *web.GetForm(ctx).(*forms.AccessTokenForm)
466456
// if there is no ClientID or ClientSecret in the request body, fill these fields by the Authorization header and ensure the provided field matches the Authorization header
467457
if form.ClientID == "" || form.ClientSecret == "" {
468-
authHeader := ctx.Req.Header.Get("Authorization")
469-
if authType, authData, ok := strings.Cut(authHeader, " "); ok && strings.EqualFold(authType, "Basic") {
470-
clientID, clientSecret, err := base.BasicAuthDecode(authData)
471-
if err != nil {
458+
if authHeader := ctx.Req.Header.Get("Authorization"); authHeader != "" {
459+
clientID, clientSecret, ok := httpauth.ParseAuthorizationHeaderBasic(authHeader)
460+
if !ok {
472461
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
473462
ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidRequest,
474463
ErrorDescription: "cannot parse basic auth header",

services/auth/basic.go

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@ package auth
77
import (
88
"errors"
99
"net/http"
10-
"strings"
1110

1211
actions_model "code.gitea.io/gitea/models/actions"
1312
auth_model "code.gitea.io/gitea/models/auth"
1413
user_model "code.gitea.io/gitea/models/user"
15-
"code.gitea.io/gitea/modules/base"
14+
"code.gitea.io/gitea/modules/auth/httpauth"
1615
"code.gitea.io/gitea/modules/log"
1716
"code.gitea.io/gitea/modules/setting"
1817
"code.gitea.io/gitea/modules/timeutil"
@@ -54,17 +53,11 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
5453
return nil, nil
5554
}
5655

57-
baHead := req.Header.Get("Authorization")
58-
if len(baHead) == 0 {
56+
authHeader := req.Header.Get("Authorization")
57+
if authHeader == "" {
5958
return nil, nil
6059
}
61-
62-
auths := strings.SplitN(baHead, " ", 2)
63-
if len(auths) != 2 || (strings.ToLower(auths[0]) != "basic") {
64-
return nil, nil
65-
}
66-
67-
uname, passwd, _ := base.BasicAuthDecode(auths[1])
60+
uname, passwd, _ := httpauth.ParseAuthorizationHeaderBasic(authHeader)
6861

6962
// Check if username or password is a token
7063
isUsernameToken := len(passwd) == 0 || passwd == "x-oauth-basic"

services/auth/oauth2.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
actions_model "code.gitea.io/gitea/models/actions"
1414
auth_model "code.gitea.io/gitea/models/auth"
1515
user_model "code.gitea.io/gitea/models/user"
16+
"code.gitea.io/gitea/modules/auth/httpauth"
1617
"code.gitea.io/gitea/modules/log"
1718
"code.gitea.io/gitea/modules/setting"
1819
"code.gitea.io/gitea/modules/timeutil"
@@ -97,10 +98,7 @@ func parseToken(req *http.Request) (string, bool) {
9798

9899
// check header token
99100
if auHead := req.Header.Get("Authorization"); auHead != "" {
100-
auths := strings.Fields(auHead)
101-
if len(auths) == 2 && (auths[0] == "token" || strings.ToLower(auths[0]) == "bearer") {
102-
return auths[1], true
103-
}
101+
return httpauth.ParseAuthorizationHeaderBearerToken(auHead)
104102
}
105103
return "", false
106104
}

services/lfs/server.go

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
repo_model "code.gitea.io/gitea/models/repo"
2828
"code.gitea.io/gitea/models/unit"
2929
user_model "code.gitea.io/gitea/models/user"
30+
"code.gitea.io/gitea/modules/auth/httpauth"
3031
"code.gitea.io/gitea/modules/json"
3132
lfs_module "code.gitea.io/gitea/modules/lfs"
3233
"code.gitea.io/gitea/modules/log"
@@ -594,19 +595,11 @@ func parseToken(ctx stdCtx.Context, authorization string, target *repo_model.Rep
594595
if authorization == "" {
595596
return nil, errors.New("no token")
596597
}
597-
598-
parts := strings.SplitN(authorization, " ", 2)
599-
if len(parts) != 2 {
600-
return nil, errors.New("no token")
601-
}
602-
tokenSHA := parts[1]
603-
switch strings.ToLower(parts[0]) {
604-
case "bearer":
605-
fallthrough
606-
case "token":
607-
return handleLFSToken(ctx, tokenSHA, target, mode)
598+
token, ok := httpauth.ParseAuthorizationHeaderBearerToken(authorization)
599+
if !ok {
600+
return nil, errors.New("token not found")
608601
}
609-
return nil, errors.New("token not found")
602+
return handleLFSToken(ctx, token, target, mode)
610603
}
611604

612605
func requireAuth(ctx *context.Context) {

0 commit comments

Comments
 (0)