Skip to content

Commit 00705da

Browse files
authored
Unify two factor check (#27915) (#27939)
Backport of #27915 Fixes #27819 We have support for two factor logins with the normal web login and with basic auth. For basic auth the two factor check was implemented at three different places and you need to know that this check is necessary. This PR moves the check into the basic auth itself.
1 parent 4a48370 commit 00705da

File tree

5 files changed

+76
-66
lines changed

5 files changed

+76
-66
lines changed

modules/context/api.go

-27
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"net/url"
1212
"strings"
1313

14-
"code.gitea.io/gitea/models/auth"
1514
repo_model "code.gitea.io/gitea/models/repo"
1615
"code.gitea.io/gitea/models/unit"
1716
user_model "code.gitea.io/gitea/models/user"
@@ -197,32 +196,6 @@ func (ctx *APIContext) SetLinkHeader(total, pageSize int) {
197196
}
198197
}
199198

200-
// CheckForOTP validates OTP
201-
func (ctx *APIContext) CheckForOTP() {
202-
if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
203-
return // Skip 2FA
204-
}
205-
206-
otpHeader := ctx.Req.Header.Get("X-Gitea-OTP")
207-
twofa, err := auth.GetTwoFactorByUID(ctx.Doer.ID)
208-
if err != nil {
209-
if auth.IsErrTwoFactorNotEnrolled(err) {
210-
return // No 2FA enrollment for this user
211-
}
212-
ctx.Error(http.StatusInternalServerError, "GetTwoFactorByUID", err)
213-
return
214-
}
215-
ok, err := twofa.ValidateTOTP(otpHeader)
216-
if err != nil {
217-
ctx.Error(http.StatusInternalServerError, "ValidateTOTP", err)
218-
return
219-
}
220-
if !ok {
221-
ctx.Error(http.StatusUnauthorized, "", nil)
222-
return
223-
}
224-
}
225-
226199
// APIContexter returns apicontext as middleware
227200
func APIContexter() func(http.Handler) http.Handler {
228201
return func(next http.Handler) http.Handler {

routers/api/v1/api.go

-11
Original file line numberDiff line numberDiff line change
@@ -315,10 +315,6 @@ func reqToken() func(ctx *context.APIContext) {
315315
return
316316
}
317317

318-
if ctx.IsBasicAuth {
319-
ctx.CheckForOTP()
320-
return
321-
}
322318
if ctx.IsSigned {
323319
return
324320
}
@@ -340,7 +336,6 @@ func reqBasicAuth() func(ctx *context.APIContext) {
340336
ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "auth required")
341337
return
342338
}
343-
ctx.CheckForOTP()
344339
}
345340
}
346341

@@ -687,12 +682,6 @@ func bind[T any](_ T) any {
687682
}
688683
}
689684

690-
// The OAuth2 plugin is expected to be executed first, as it must ignore the user id stored
691-
// in the session (if there is a user id stored in session other plugins might return the user
692-
// object for that id).
693-
//
694-
// The Session plugin is expected to be executed second, in order to skip authentication
695-
// for users that have already signed in.
696685
func buildAuthGroup() *auth.Group {
697686
group := auth.NewGroup(
698687
&auth.OAuth2{},

services/auth/basic.go

+22-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"code.gitea.io/gitea/modules/log"
1616
"code.gitea.io/gitea/modules/setting"
1717
"code.gitea.io/gitea/modules/timeutil"
18+
"code.gitea.io/gitea/modules/util"
1819
"code.gitea.io/gitea/modules/web/middleware"
1920
)
2021

@@ -132,11 +133,30 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
132133
return nil, err
133134
}
134135

135-
if skipper, ok := source.Cfg.(LocalTwoFASkipper); ok && skipper.IsSkipLocalTwoFA() {
136-
store.GetData()["SkipLocalTwoFA"] = true
136+
if skipper, ok := source.Cfg.(LocalTwoFASkipper); !ok || !skipper.IsSkipLocalTwoFA() {
137+
if err := validateTOTP(req, u); err != nil {
138+
return nil, err
139+
}
137140
}
138141

139142
log.Trace("Basic Authorization: Logged in user %-v", u)
140143

141144
return u, nil
142145
}
146+
147+
func validateTOTP(req *http.Request, u *user_model.User) error {
148+
twofa, err := auth_model.GetTwoFactorByUID(u.ID)
149+
if err != nil {
150+
if auth_model.IsErrTwoFactorNotEnrolled(err) {
151+
// No 2FA enrollment for this user
152+
return nil
153+
}
154+
return err
155+
}
156+
if ok, err := twofa.ValidateTOTP(req.Header.Get("X-Gitea-OTP")); err != nil {
157+
return err
158+
} else if !ok {
159+
return util.NewInvalidArgumentErrorf("invalid provided OTP")
160+
}
161+
return nil
162+
}

services/auth/middleware.go

-26
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"net/http"
88
"strings"
99

10-
"code.gitea.io/gitea/models/auth"
1110
user_model "code.gitea.io/gitea/models/user"
1211
"code.gitea.io/gitea/modules/context"
1312
"code.gitea.io/gitea/modules/log"
@@ -216,31 +215,6 @@ func VerifyAuthWithOptionsAPI(options *VerifyOptions) func(ctx *context.APIConte
216215
})
217216
return
218217
}
219-
if ctx.IsSigned && ctx.IsBasicAuth {
220-
if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
221-
return // Skip 2FA
222-
}
223-
twofa, err := auth.GetTwoFactorByUID(ctx.Doer.ID)
224-
if err != nil {
225-
if auth.IsErrTwoFactorNotEnrolled(err) {
226-
return // No 2FA enrollment for this user
227-
}
228-
ctx.InternalServerError(err)
229-
return
230-
}
231-
otpHeader := ctx.Req.Header.Get("X-Gitea-OTP")
232-
ok, err := twofa.ValidateTOTP(otpHeader)
233-
if err != nil {
234-
ctx.InternalServerError(err)
235-
return
236-
}
237-
if !ok {
238-
ctx.JSON(http.StatusForbidden, map[string]string{
239-
"message": "Only signed in user is allowed to call APIs.",
240-
})
241-
return
242-
}
243-
}
244218
}
245219

246220
if options.AdminRequired {

tests/integration/api_twofa_test.go

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package integration
5+
6+
import (
7+
"net/http"
8+
"testing"
9+
"time"
10+
11+
auth_model "code.gitea.io/gitea/models/auth"
12+
"code.gitea.io/gitea/models/unittest"
13+
user_model "code.gitea.io/gitea/models/user"
14+
"code.gitea.io/gitea/tests"
15+
16+
"github.com/pquerna/otp/totp"
17+
"github.com/stretchr/testify/assert"
18+
)
19+
20+
func TestAPITwoFactor(t *testing.T) {
21+
defer tests.PrepareTestEnv(t)()
22+
23+
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 16})
24+
25+
req := NewRequestf(t, "GET", "/api/v1/user")
26+
req = AddBasicAuthHeader(req, user.Name)
27+
MakeRequest(t, req, http.StatusOK)
28+
29+
otpKey, err := totp.Generate(totp.GenerateOpts{
30+
SecretSize: 40,
31+
Issuer: "gitea-test",
32+
AccountName: user.Name,
33+
})
34+
assert.NoError(t, err)
35+
36+
tfa := &auth_model.TwoFactor{
37+
UID: user.ID,
38+
}
39+
assert.NoError(t, tfa.SetSecret(otpKey.Secret()))
40+
41+
assert.NoError(t, auth_model.NewTwoFactor(tfa))
42+
43+
req = NewRequestf(t, "GET", "/api/v1/user")
44+
req = AddBasicAuthHeader(req, user.Name)
45+
MakeRequest(t, req, http.StatusUnauthorized)
46+
47+
passcode, err := totp.GenerateCode(otpKey.Secret(), time.Now())
48+
assert.NoError(t, err)
49+
50+
req = NewRequestf(t, "GET", "/api/v1/user")
51+
req = AddBasicAuthHeader(req, user.Name)
52+
req.Header.Set("X-Gitea-OTP", passcode)
53+
MakeRequest(t, req, http.StatusOK)
54+
}

0 commit comments

Comments
 (0)