"net/url"
"strings"
- "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
}
}
-// CheckForOTP validates OTP
-func (ctx *APIContext) CheckForOTP() {
- if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
- return // Skip 2FA
- }
-
- otpHeader := ctx.Req.Header.Get("X-Gitea-OTP")
- twofa, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
- if err != nil {
- if auth.IsErrTwoFactorNotEnrolled(err) {
- return // No 2FA enrollment for this user
- }
- ctx.Error(http.StatusInternalServerError, "GetTwoFactorByUID", err)
- return
- }
- ok, err := twofa.ValidateTOTP(otpHeader)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, "ValidateTOTP", err)
- return
- }
- if !ok {
- ctx.Error(http.StatusUnauthorized, "", nil)
- return
- }
-}
-
// APIContexter returns apicontext as middleware
func APIContexter() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return
}
- if ctx.IsBasicAuth {
- ctx.CheckForOTP()
- return
- }
if ctx.IsSigned {
return
}
ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "auth required")
return
}
- ctx.CheckForOTP()
}
}
}
}
-// The OAuth2 plugin is expected to be executed first, as it must ignore the user id stored
-// in the session (if there is a user id stored in session other plugins might return the user
-// object for that id).
-//
-// The Session plugin is expected to be executed second, in order to skip authentication
-// for users that have already signed in.
func buildAuthGroup() *auth.Group {
group := auth.NewGroup(
&auth.OAuth2{},
})
return
}
- if ctx.IsSigned && ctx.IsBasicAuth {
- if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
- return // Skip 2FA
- }
- twofa, err := auth_model.GetTwoFactorByUID(ctx, ctx.Doer.ID)
- if err != nil {
- if auth_model.IsErrTwoFactorNotEnrolled(err) {
- return // No 2FA enrollment for this user
- }
- ctx.InternalServerError(err)
- return
- }
- otpHeader := ctx.Req.Header.Get("X-Gitea-OTP")
- ok, err := twofa.ValidateTOTP(otpHeader)
- if err != nil {
- ctx.InternalServerError(err)
- return
- }
- if !ok {
- ctx.JSON(http.StatusForbidden, map[string]string{
- "message": "Only signed in user is allowed to call APIs.",
- })
- return
- }
- }
}
if options.AdminRequired {
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware"
)
return nil, err
}
- if skipper, ok := source.Cfg.(LocalTwoFASkipper); ok && skipper.IsSkipLocalTwoFA() {
- store.GetData()["SkipLocalTwoFA"] = true
+ if skipper, ok := source.Cfg.(LocalTwoFASkipper); !ok || !skipper.IsSkipLocalTwoFA() {
+ if err := validateTOTP(req, u); err != nil {
+ return nil, err
+ }
}
log.Trace("Basic Authorization: Logged in user %-v", u)
return u, nil
}
+
+func validateTOTP(req *http.Request, u *user_model.User) error {
+ twofa, err := auth_model.GetTwoFactorByUID(req.Context(), u.ID)
+ if err != nil {
+ if auth_model.IsErrTwoFactorNotEnrolled(err) {
+ // No 2FA enrollment for this user
+ return nil
+ }
+ return err
+ }
+ if ok, err := twofa.ValidateTOTP(req.Header.Get("X-Gitea-OTP")); err != nil {
+ return err
+ } else if !ok {
+ return util.NewInvalidArgumentErrorf("invalid provided OTP")
+ }
+ return nil
+}
--- /dev/null
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+ "time"
+
+ auth_model "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/pquerna/otp/totp"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPITwoFactor(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 16})
+
+ req := NewRequestf(t, "GET", "/api/v1/user")
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusOK)
+
+ otpKey, err := totp.Generate(totp.GenerateOpts{
+ SecretSize: 40,
+ Issuer: "gitea-test",
+ AccountName: user.Name,
+ })
+ assert.NoError(t, err)
+
+ tfa := &auth_model.TwoFactor{
+ UID: user.ID,
+ }
+ assert.NoError(t, tfa.SetSecret(otpKey.Secret()))
+
+ assert.NoError(t, auth_model.NewTwoFactor(db.DefaultContext, tfa))
+
+ req = NewRequestf(t, "GET", "/api/v1/user")
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ passcode, err := totp.GenerateCode(otpKey.Secret(), time.Now())
+ assert.NoError(t, err)
+
+ req = NewRequestf(t, "GET", "/api/v1/user")
+ req = AddBasicAuthHeader(req, user.Name)
+ req.Header.Set("X-Gitea-OTP", passcode)
+ MakeRequest(t, req, http.StatusOK)
+}