Skip to content

Commit c30c9c3

Browse files
committed
fix
1 parent 6fe5c4c commit c30c9c3

File tree

9 files changed

+127
-69
lines changed

9 files changed

+127
-69
lines changed

modules/auth/httpauth/httpauth.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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(encoded string) (string, string, bool) {
14+
s, err := base64.StdEncoding.DecodeString(encoded)
15+
if err != nil {
16+
return "", "", false
17+
}
18+
if u, p, ok := strings.Cut(string(s), ":"); ok {
19+
return u, p, true
20+
}
21+
return "", "", false
22+
}
23+
24+
func ParseAuthorizationHeaderBearerToken(header string) (string, bool) {
25+
parts := strings.Fields(header)
26+
if len(parts) != 2 {
27+
return "", false
28+
}
29+
if util.AsciiEqualFold(parts[0], "token") || util.AsciiEqualFold(parts[0], "bearer") {
30+
return parts[1], true
31+
}
32+
return "", false
33+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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+
{base64.StdEncoding.EncodeToString([]byte("foo")), "", "", false},
24+
{base64.StdEncoding.EncodeToString([]byte("foo:bar")), "foo", "bar", true},
25+
}
26+
for _, c := range cases {
27+
user, pass, ok := ParseAuthorizationHeaderBasic(c.headerValue)
28+
assert.Equal(t, c.ok, ok, "header %q", c.headerValue)
29+
assert.Equal(t, c.user, user, "header %q", c.headerValue)
30+
assert.Equal(t, c.pass, pass, "header %q", c.headerValue)
31+
}
32+
})
33+
t.Run("BearerToken", func(t *testing.T) {
34+
cases := []struct {
35+
headerValue string
36+
expected string
37+
ok bool
38+
}{
39+
{"", "", false},
40+
{"?", "", false},
41+
{"any value", "", false},
42+
{"token value", "value", true},
43+
{"Token value", "value", true},
44+
{"bearer value", "value", true},
45+
{"Bearer value", "value", true},
46+
{"Bearer wrong value", "", false},
47+
}
48+
for _, c := range cases {
49+
token, ok := ParseAuthorizationHeaderBearerToken(c.headerValue)
50+
assert.Equal(t, c.ok, ok, "header %q", c.headerValue)
51+
assert.Equal(t, c.expected, token, "header %q", c.headerValue)
52+
}
53+
})
54+
}

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

routers/web/auth/oauth2_provider.go

Lines changed: 9 additions & 16 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,15 @@ func InfoOAuth(ctx *context.Context) {
127124
ctx.JSON(http.StatusOK, response)
128125
}
129126

130-
func parseBasicAuth(ctx *context.Context) (username, password string, err error) {
127+
func parseBasicAuth(ctx *context.Context) (username, password string, ok bool) {
131128
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")
129+
return httpauth.ParseAuthorizationHeaderBasic(authHeader)
136130
}
137131

138132
// IntrospectOAuth introspects an oauth token
139133
func IntrospectOAuth(ctx *context.Context) {
140134
clientIDValid := false
141-
if clientID, clientSecret, err := parseBasicAuth(ctx); err == nil {
135+
if clientID, clientSecret, ok := parseBasicAuth(ctx); ok {
142136
app, err := auth.GetOAuth2ApplicationByClientID(ctx, clientID)
143137
if err != nil && !auth.IsErrOauthClientIDInvalid(err) {
144138
// this is likely a database error; log it and respond without details
@@ -465,10 +459,9 @@ func AccessTokenOAuth(ctx *context.Context) {
465459
form := *web.GetForm(ctx).(*forms.AccessTokenForm)
466460
// 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
467461
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 {
462+
if authHeader := ctx.Req.Header.Get("Authorization"); authHeader != "" {
463+
clientID, clientSecret, ok := httpauth.ParseAuthorizationHeaderBasic(authHeader)
464+
if !ok {
472465
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
473466
ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidRequest,
474467
ErrorDescription: "cannot parse basic auth header",

services/auth/basic.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
actions_model "code.gitea.io/gitea/models/actions"
1313
auth_model "code.gitea.io/gitea/models/auth"
1414
user_model "code.gitea.io/gitea/models/user"
15-
"code.gitea.io/gitea/modules/base"
15+
"code.gitea.io/gitea/modules/auth/httpauth"
1616
"code.gitea.io/gitea/modules/log"
1717
"code.gitea.io/gitea/modules/setting"
1818
"code.gitea.io/gitea/modules/timeutil"
@@ -64,7 +64,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
6464
return nil, nil
6565
}
6666

67-
uname, passwd, _ := base.BasicAuthDecode(auths[1])
67+
uname, passwd, _ := httpauth.ParseAuthorizationHeaderBasic(auths[1])
6868

6969
// Check if username or password is a token
7070
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)