Skip to content

Commit a8e505a

Browse files
GiteaBotKN4CK3R
andauthored
Unify two factor check (#27915) (#27929)
Backport #27915 by @KN4CK3R 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. Co-authored-by: KN4CK3R <[email protected]>
1 parent e47b31c commit a8e505a

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"
@@ -205,32 +204,6 @@ func (ctx *APIContext) SetLinkHeader(total, pageSize int) {
205204
}
206205
}
207206

208-
// CheckForOTP validates OTP
209-
func (ctx *APIContext) CheckForOTP() {
210-
if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
211-
return // Skip 2FA
212-
}
213-
214-
otpHeader := ctx.Req.Header.Get("X-Gitea-OTP")
215-
twofa, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
216-
if err != nil {
217-
if auth.IsErrTwoFactorNotEnrolled(err) {
218-
return // No 2FA enrollment for this user
219-
}
220-
ctx.Error(http.StatusInternalServerError, "GetTwoFactorByUID", err)
221-
return
222-
}
223-
ok, err := twofa.ValidateTOTP(otpHeader)
224-
if err != nil {
225-
ctx.Error(http.StatusInternalServerError, "ValidateTOTP", err)
226-
return
227-
}
228-
if !ok {
229-
ctx.Error(http.StatusUnauthorized, "", nil)
230-
return
231-
}
232-
}
233-
234207
// APIContexter returns apicontext as middleware
235208
func APIContexter() func(http.Handler) http.Handler {
236209
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
@@ -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
}
@@ -343,7 +339,6 @@ func reqBasicOrRevProxyAuth() func(ctx *context.APIContext) {
343339
ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "auth required")
344340
return
345341
}
346-
ctx.CheckForOTP()
347342
}
348343
}
349344

@@ -700,12 +695,6 @@ func bind[T any](_ T) any {
700695
}
701696
}
702697

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

815779
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)