Skip to content

Commit 591f586

Browse files
authored
Extract auth middleware from service (#27028)
Related #27027 Extract the router logic from `services/auth/middleware.go` into `routers/web` <-> `routers/common` <-> `routers/api`.
1 parent 7818121 commit 591f586

File tree

4 files changed

+262
-266
lines changed

4 files changed

+262
-266
lines changed

routers/api/v1/api.go

+103-2
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ import (
9090
"code.gitea.io/gitea/routers/api/v1/repo"
9191
"code.gitea.io/gitea/routers/api/v1/settings"
9292
"code.gitea.io/gitea/routers/api/v1/user"
93+
"code.gitea.io/gitea/routers/common"
9394
"code.gitea.io/gitea/services/auth"
9495
context_service "code.gitea.io/gitea/services/context"
9596
"code.gitea.io/gitea/services/forms"
@@ -709,6 +710,106 @@ func buildAuthGroup() *auth.Group {
709710
return group
710711
}
711712

713+
func apiAuth(authMethod auth.Method) func(*context.APIContext) {
714+
return func(ctx *context.APIContext) {
715+
ar, err := common.AuthShared(ctx.Base, nil, authMethod)
716+
if err != nil {
717+
ctx.Error(http.StatusUnauthorized, "APIAuth", err)
718+
return
719+
}
720+
ctx.Doer = ar.Doer
721+
ctx.IsSigned = ar.Doer != nil
722+
ctx.IsBasicAuth = ar.IsBasicAuth
723+
}
724+
}
725+
726+
// verifyAuthWithOptions checks authentication according to options
727+
func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.APIContext) {
728+
return func(ctx *context.APIContext) {
729+
// Check prohibit login users.
730+
if ctx.IsSigned {
731+
if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm {
732+
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
733+
ctx.JSON(http.StatusForbidden, map[string]string{
734+
"message": "This account is not activated.",
735+
})
736+
return
737+
}
738+
if !ctx.Doer.IsActive || ctx.Doer.ProhibitLogin {
739+
log.Info("Failed authentication attempt for %s from %s", ctx.Doer.Name, ctx.RemoteAddr())
740+
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
741+
ctx.JSON(http.StatusForbidden, map[string]string{
742+
"message": "This account is prohibited from signing in, please contact your site administrator.",
743+
})
744+
return
745+
}
746+
747+
if ctx.Doer.MustChangePassword {
748+
ctx.JSON(http.StatusForbidden, map[string]string{
749+
"message": "You must change your password. Change it at: " + setting.AppURL + "/user/change_password",
750+
})
751+
return
752+
}
753+
}
754+
755+
// Redirect to dashboard if user tries to visit any non-login page.
756+
if options.SignOutRequired && ctx.IsSigned && ctx.Req.URL.RequestURI() != "/" {
757+
ctx.Redirect(setting.AppSubURL + "/")
758+
return
759+
}
760+
761+
if options.SignInRequired {
762+
if !ctx.IsSigned {
763+
// Restrict API calls with error message.
764+
ctx.JSON(http.StatusForbidden, map[string]string{
765+
"message": "Only signed in user is allowed to call APIs.",
766+
})
767+
return
768+
} else if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm {
769+
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
770+
ctx.JSON(http.StatusForbidden, map[string]string{
771+
"message": "This account is not activated.",
772+
})
773+
return
774+
}
775+
if ctx.IsSigned && ctx.IsBasicAuth {
776+
if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
777+
return // Skip 2FA
778+
}
779+
twofa, err := auth_model.GetTwoFactorByUID(ctx.Doer.ID)
780+
if err != nil {
781+
if auth_model.IsErrTwoFactorNotEnrolled(err) {
782+
return // No 2FA enrollment for this user
783+
}
784+
ctx.InternalServerError(err)
785+
return
786+
}
787+
otpHeader := ctx.Req.Header.Get("X-Gitea-OTP")
788+
ok, err := twofa.ValidateTOTP(otpHeader)
789+
if err != nil {
790+
ctx.InternalServerError(err)
791+
return
792+
}
793+
if !ok {
794+
ctx.JSON(http.StatusForbidden, map[string]string{
795+
"message": "Only signed in user is allowed to call APIs.",
796+
})
797+
return
798+
}
799+
}
800+
}
801+
802+
if options.AdminRequired {
803+
if !ctx.Doer.IsAdmin {
804+
ctx.JSON(http.StatusForbidden, map[string]string{
805+
"message": "You have no permission to request for this.",
806+
})
807+
return
808+
}
809+
}
810+
}
811+
}
812+
712813
// Routes registers all v1 APIs routes to web application.
713814
func Routes() *web.Route {
714815
m := web.NewRoute()
@@ -728,9 +829,9 @@ func Routes() *web.Route {
728829
m.Use(context.APIContexter())
729830

730831
// Get user from session if logged in.
731-
m.Use(auth.APIAuth(buildAuthGroup()))
832+
m.Use(apiAuth(buildAuthGroup()))
732833

733-
m.Use(auth.VerifyAuthWithOptionsAPI(&auth.VerifyOptions{
834+
m.Use(verifyAuthWithOptions(&common.VerifyOptions{
734835
SignInRequired: setting.Service.RequireSignInView,
735836
}))
736837

routers/common/auth.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright 2022 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package common
5+
6+
import (
7+
user_model "code.gitea.io/gitea/models/user"
8+
"code.gitea.io/gitea/modules/context"
9+
"code.gitea.io/gitea/modules/web/middleware"
10+
auth_service "code.gitea.io/gitea/services/auth"
11+
)
12+
13+
type AuthResult struct {
14+
Doer *user_model.User
15+
IsBasicAuth bool
16+
}
17+
18+
func AuthShared(ctx *context.Base, sessionStore auth_service.SessionStore, authMethod auth_service.Method) (ar AuthResult, err error) {
19+
ar.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, sessionStore)
20+
if err != nil {
21+
return ar, err
22+
}
23+
if ar.Doer != nil {
24+
if ctx.Locale.Language() != ar.Doer.Language {
25+
ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req)
26+
}
27+
ar.IsBasicAuth = ctx.Data["AuthedMethod"].(string) == auth_service.BasicMethodName
28+
29+
ctx.Data["IsSigned"] = true
30+
ctx.Data[middleware.ContextDataKeySignedUser] = ar.Doer
31+
ctx.Data["SignedUserID"] = ar.Doer.ID
32+
ctx.Data["IsAdmin"] = ar.Doer.IsAdmin
33+
} else {
34+
ctx.Data["SignedUserID"] = int64(0)
35+
}
36+
return ar, nil
37+
}
38+
39+
// VerifyOptions contains required or check options
40+
type VerifyOptions struct {
41+
SignInRequired bool
42+
SignOutRequired bool
43+
AdminRequired bool
44+
DisableCSRF bool
45+
}

routers/web/web.go

+114-9
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package web
66
import (
77
gocontext "context"
88
"net/http"
9+
"strings"
910

1011
"code.gitea.io/gitea/models/perm"
1112
"code.gitea.io/gitea/models/unit"
@@ -19,6 +20,7 @@ import (
1920
"code.gitea.io/gitea/modules/templates"
2021
"code.gitea.io/gitea/modules/validation"
2122
"code.gitea.io/gitea/modules/web"
23+
"code.gitea.io/gitea/modules/web/middleware"
2224
"code.gitea.io/gitea/modules/web/routing"
2325
"code.gitea.io/gitea/routers/common"
2426
"code.gitea.io/gitea/routers/web/admin"
@@ -46,7 +48,7 @@ import (
4648

4749
"gitea.com/go-chi/captcha"
4850
"github.com/NYTimes/gziphandler"
49-
"github.com/go-chi/chi/v5/middleware"
51+
chi_middleware "github.com/go-chi/chi/v5/middleware"
5052
"github.com/go-chi/cors"
5153
"github.com/prometheus/client_golang/prometheus"
5254
)
@@ -95,6 +97,109 @@ func buildAuthGroup() *auth_service.Group {
9597
return group
9698
}
9799

100+
func webAuth(authMethod auth_service.Method) func(*context.Context) {
101+
return func(ctx *context.Context) {
102+
ar, err := common.AuthShared(ctx.Base, ctx.Session, authMethod)
103+
if err != nil {
104+
log.Error("Failed to verify user: %v", err)
105+
ctx.Error(http.StatusUnauthorized, "Verify")
106+
return
107+
}
108+
ctx.Doer = ar.Doer
109+
ctx.IsSigned = ar.Doer != nil
110+
ctx.IsBasicAuth = ar.IsBasicAuth
111+
if ctx.Doer == nil {
112+
// ensure the session uid is deleted
113+
_ = ctx.Session.Delete("uid")
114+
}
115+
}
116+
}
117+
118+
// verifyAuthWithOptions checks authentication according to options
119+
func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Context) {
120+
return func(ctx *context.Context) {
121+
// Check prohibit login users.
122+
if ctx.IsSigned {
123+
if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm {
124+
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
125+
ctx.HTML(http.StatusOK, "user/auth/activate")
126+
return
127+
}
128+
if !ctx.Doer.IsActive || ctx.Doer.ProhibitLogin {
129+
log.Info("Failed authentication attempt for %s from %s", ctx.Doer.Name, ctx.RemoteAddr())
130+
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
131+
ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
132+
return
133+
}
134+
135+
if ctx.Doer.MustChangePassword {
136+
if ctx.Req.URL.Path != "/user/settings/change_password" {
137+
if strings.HasPrefix(ctx.Req.UserAgent(), "git") {
138+
ctx.Error(http.StatusUnauthorized, ctx.Tr("auth.must_change_password"))
139+
return
140+
}
141+
ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
142+
ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/change_password"
143+
if ctx.Req.URL.Path != "/user/events" {
144+
middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
145+
}
146+
ctx.Redirect(setting.AppSubURL + "/user/settings/change_password")
147+
return
148+
}
149+
} else if ctx.Req.URL.Path == "/user/settings/change_password" {
150+
// make sure that the form cannot be accessed by users who don't need this
151+
ctx.Redirect(setting.AppSubURL + "/")
152+
return
153+
}
154+
}
155+
156+
// Redirect to dashboard (or alternate location) if user tries to visit any non-login page.
157+
if options.SignOutRequired && ctx.IsSigned && ctx.Req.URL.RequestURI() != "/" {
158+
ctx.RedirectToFirst(ctx.FormString("redirect_to"))
159+
return
160+
}
161+
162+
if !options.SignOutRequired && !options.DisableCSRF && ctx.Req.Method == "POST" {
163+
ctx.Csrf.Validate(ctx)
164+
if ctx.Written() {
165+
return
166+
}
167+
}
168+
169+
if options.SignInRequired {
170+
if !ctx.IsSigned {
171+
if ctx.Req.URL.Path != "/user/events" {
172+
middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
173+
}
174+
ctx.Redirect(setting.AppSubURL + "/user/login")
175+
return
176+
} else if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm {
177+
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
178+
ctx.HTML(http.StatusOK, "user/auth/activate")
179+
return
180+
}
181+
}
182+
183+
// Redirect to log in page if auto-signin info is provided and has not signed in.
184+
if !options.SignOutRequired && !ctx.IsSigned &&
185+
len(ctx.GetSiteCookie(setting.CookieUserName)) > 0 {
186+
if ctx.Req.URL.Path != "/user/events" {
187+
middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
188+
}
189+
ctx.Redirect(setting.AppSubURL + "/user/login")
190+
return
191+
}
192+
193+
if options.AdminRequired {
194+
if !ctx.Doer.IsAdmin {
195+
ctx.Error(http.StatusForbidden)
196+
return
197+
}
198+
ctx.Data["PageIsAdmin"] = true
199+
}
200+
}
201+
}
202+
98203
func ctxDataSet(args ...any) func(ctx *context.Context) {
99204
return func(ctx *context.Context) {
100205
for i := 0; i < len(args); i += 2 {
@@ -144,10 +249,10 @@ func Routes() *web.Route {
144249
mid = append(mid, common.Sessioner(), context.Contexter())
145250

146251
// Get user from session if logged in.
147-
mid = append(mid, auth_service.Auth(buildAuthGroup()))
252+
mid = append(mid, webAuth(buildAuthGroup()))
148253

149254
// GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route
150-
mid = append(mid, middleware.GetHead)
255+
mid = append(mid, chi_middleware.GetHead)
151256

152257
if setting.API.EnableSwagger {
153258
// Note: The route is here but no in API routes because it renders a web page
@@ -168,12 +273,12 @@ func Routes() *web.Route {
168273

169274
// registerRoutes register routes
170275
func registerRoutes(m *web.Route) {
171-
reqSignIn := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{SignInRequired: true})
172-
reqSignOut := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{SignOutRequired: true})
276+
reqSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true})
277+
reqSignOut := verifyAuthWithOptions(&common.VerifyOptions{SignOutRequired: true})
173278
// TODO: rename them to "optSignIn", which means that the "sign-in" could be optional, depends on the VerifyOptions (RequireSignInView)
174-
ignSignIn := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{SignInRequired: setting.Service.RequireSignInView})
175-
ignExploreSignIn := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{SignInRequired: setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView})
176-
ignSignInAndCsrf := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{DisableCSRF: true})
279+
ignSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInView})
280+
ignExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView})
281+
ignSignInAndCsrf := verifyAuthWithOptions(&common.VerifyOptions{DisableCSRF: true})
177282
validation.AddBindingRules()
178283

179284
linkAccountEnabled := func(ctx *context.Context) {
@@ -543,7 +648,7 @@ func registerRoutes(m *web.Route) {
543648

544649
m.Get("/avatar/{hash}", user.AvatarByEmailHash)
545650

546-
adminReq := auth_service.VerifyAuthWithOptions(&auth_service.VerifyOptions{SignInRequired: true, AdminRequired: true})
651+
adminReq := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true, AdminRequired: true})
547652

548653
// ***** START: Admin *****
549654
m.Group("/admin", func() {

0 commit comments

Comments
 (0)