Skip to content

Commit 4f4fea7

Browse files
authored
Unify two factor check (#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 8557a94 commit 4f4fea7

File tree

4 files changed

+77
-65
lines changed

4 files changed

+77
-65
lines changed

modules/context/api.go

Lines changed: 0 additions & 27 deletions
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"
@@ -211,32 +210,6 @@ func (ctx *APIContext) SetLinkHeader(total, pageSize int) {
211210
}
212211
}
213212

214-
// CheckForOTP validates OTP
215-
func (ctx *APIContext) CheckForOTP() {
216-
if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
217-
return // Skip 2FA
218-
}
219-
220-
otpHeader := ctx.Req.Header.Get("X-Gitea-OTP")
221-
twofa, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
222-
if err != nil {
223-
if auth.IsErrTwoFactorNotEnrolled(err) {
224-
return // No 2FA enrollment for this user
225-
}
226-
ctx.Error(http.StatusInternalServerError, "GetTwoFactorByUID", err)
227-
return
228-
}
229-
ok, err := twofa.ValidateTOTP(otpHeader)
230-
if err != nil {
231-
ctx.Error(http.StatusInternalServerError, "ValidateTOTP", err)
232-
return
233-
}
234-
if !ok {
235-
ctx.Error(http.StatusUnauthorized, "", nil)
236-
return
237-
}
238-
}
239-
240213
// APIContexter returns apicontext as middleware
241214
func APIContexter() func(http.Handler) http.Handler {
242215
return func(next http.Handler) http.Handler {

routers/api/v1/api.go

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -316,10 +316,6 @@ func reqToken() func(ctx *context.APIContext) {
316316
return
317317
}
318318

319-
if ctx.IsBasicAuth {
320-
ctx.CheckForOTP()
321-
return
322-
}
323319
if ctx.IsSigned {
324320
return
325321
}
@@ -344,7 +340,6 @@ func reqBasicOrRevProxyAuth() func(ctx *context.APIContext) {
344340
ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "auth required")
345341
return
346342
}
347-
ctx.CheckForOTP()
348343
}
349344
}
350345

@@ -701,12 +696,6 @@ func bind[T any](_ T) any {
701696
}
702697
}
703698

704-
// The OAuth2 plugin is expected to be executed first, as it must ignore the user id stored
705-
// in the session (if there is a user id stored in session other plugins might return the user
706-
// object for that id).
707-
//
708-
// The Session plugin is expected to be executed second, in order to skip authentication
709-
// for users that have already signed in.
710699
func buildAuthGroup() *auth.Group {
711700
group := auth.NewGroup(
712701
&auth.OAuth2{},
@@ -786,31 +775,6 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.APIC
786775
})
787776
return
788777
}
789-
if ctx.IsSigned && ctx.IsBasicAuth {
790-
if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
791-
return // Skip 2FA
792-
}
793-
twofa, err := auth_model.GetTwoFactorByUID(ctx, ctx.Doer.ID)
794-
if err != nil {
795-
if auth_model.IsErrTwoFactorNotEnrolled(err) {
796-
return // No 2FA enrollment for this user
797-
}
798-
ctx.InternalServerError(err)
799-
return
800-
}
801-
otpHeader := ctx.Req.Header.Get("X-Gitea-OTP")
802-
ok, err := twofa.ValidateTOTP(otpHeader)
803-
if err != nil {
804-
ctx.InternalServerError(err)
805-
return
806-
}
807-
if !ok {
808-
ctx.JSON(http.StatusForbidden, map[string]string{
809-
"message": "Only signed in user is allowed to call APIs.",
810-
})
811-
return
812-
}
813-
}
814778
}
815779

816780
if options.AdminRequired {

services/auth/basic.go

Lines changed: 22 additions & 2 deletions
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

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

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

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

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

tests/integration/api_twofa_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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/db"
13+
"code.gitea.io/gitea/models/unittest"
14+
user_model "code.gitea.io/gitea/models/user"
15+
"code.gitea.io/gitea/tests"
16+
17+
"github.com/pquerna/otp/totp"
18+
"github.com/stretchr/testify/assert"
19+
)
20+
21+
func TestAPITwoFactor(t *testing.T) {
22+
defer tests.PrepareTestEnv(t)()
23+
24+
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 16})
25+
26+
req := NewRequestf(t, "GET", "/api/v1/user")
27+
req = AddBasicAuthHeader(req, user.Name)
28+
MakeRequest(t, req, http.StatusOK)
29+
30+
otpKey, err := totp.Generate(totp.GenerateOpts{
31+
SecretSize: 40,
32+
Issuer: "gitea-test",
33+
AccountName: user.Name,
34+
})
35+
assert.NoError(t, err)
36+
37+
tfa := &auth_model.TwoFactor{
38+
UID: user.ID,
39+
}
40+
assert.NoError(t, tfa.SetSecret(otpKey.Secret()))
41+
42+
assert.NoError(t, auth_model.NewTwoFactor(db.DefaultContext, tfa))
43+
44+
req = NewRequestf(t, "GET", "/api/v1/user")
45+
req = AddBasicAuthHeader(req, user.Name)
46+
MakeRequest(t, req, http.StatusUnauthorized)
47+
48+
passcode, err := totp.GenerateCode(otpKey.Secret(), time.Now())
49+
assert.NoError(t, err)
50+
51+
req = NewRequestf(t, "GET", "/api/v1/user")
52+
req = AddBasicAuthHeader(req, user.Name)
53+
req.Header.Set("X-Gitea-OTP", passcode)
54+
MakeRequest(t, req, http.StatusOK)
55+
}

0 commit comments

Comments
 (0)