githaven-fork/services/auth/middleware.go
wxiaoguang 5d77691d42
Improve template system and panic recovery (#24461)
Partially for #24457

Major changes:

1. The old `signedUserNameStringPointerKey` is quite hacky, use
`ctx.Data[SignedUser]` instead
2. Move duplicate code from `Contexter` to `CommonTemplateContextData`
3. Remove incorrect copying&pasting code `ctx.Data["Err_Password"] =
true` in API handlers
4. Use one unique `RenderPanicErrorPage` for panic error page rendering
5. Move `stripSlashesMiddleware` to be the first middleware
6. Install global panic recovery handler, it works for both `install`
and `web`
7. Make `500.tmpl` only depend minimal template functions/variables,
avoid triggering new panics

Screenshot:

<details>

![image](https://user-images.githubusercontent.com/2114189/235444895-cecbabb8-e7dc-4360-a31c-b982d11946a7.png)

</details>
2023-05-04 14:36:34 +08:00

240 lines
7.2 KiB
Go

// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package auth
import (
"net/http"
"strings"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware"
)
// Auth is a middleware to authenticate a web user
func Auth(authMethod Method) func(*context.Context) {
return func(ctx *context.Context) {
if err := authShared(ctx, authMethod); err != nil {
log.Error("Failed to verify user: %v", err)
ctx.Error(http.StatusUnauthorized, "Verify")
return
}
if ctx.Doer == nil {
// ensure the session uid is deleted
_ = ctx.Session.Delete("uid")
}
}
}
// APIAuth is a middleware to authenticate an api user
func APIAuth(authMethod Method) func(*context.APIContext) {
return func(ctx *context.APIContext) {
if err := authShared(ctx.Context, authMethod); err != nil {
ctx.Error(http.StatusUnauthorized, "APIAuth", err)
}
}
}
func authShared(ctx *context.Context, authMethod Method) error {
var err error
ctx.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
if err != nil {
return err
}
if ctx.Doer != nil {
if ctx.Locale.Language() != ctx.Doer.Language {
ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req)
}
ctx.IsBasicAuth = ctx.Data["AuthedMethod"].(string) == BasicMethodName
ctx.IsSigned = true
ctx.Data["IsSigned"] = ctx.IsSigned
ctx.Data[middleware.ContextDataKeySignedUser] = ctx.Doer
ctx.Data["SignedUserID"] = ctx.Doer.ID
ctx.Data["IsAdmin"] = ctx.Doer.IsAdmin
} else {
ctx.Data["SignedUserID"] = int64(0)
}
return nil
}
// VerifyOptions contains required or check options
type VerifyOptions struct {
SignInRequired bool
SignOutRequired bool
AdminRequired bool
DisableCSRF bool
}
// Checks authentication according to options
func VerifyAuthWithOptions(options *VerifyOptions) func(ctx *context.Context) {
return func(ctx *context.Context) {
// Check prohibit login users.
if ctx.IsSigned {
if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm {
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
ctx.HTML(http.StatusOK, "user/auth/activate")
return
}
if !ctx.Doer.IsActive || ctx.Doer.ProhibitLogin {
log.Info("Failed authentication attempt for %s from %s", ctx.Doer.Name, ctx.RemoteAddr())
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
return
}
if ctx.Doer.MustChangePassword {
if ctx.Req.URL.Path != "/user/settings/change_password" {
if strings.HasPrefix(ctx.Req.UserAgent(), "git") {
ctx.Error(http.StatusUnauthorized, ctx.Tr("auth.must_change_password"))
return
}
ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/change_password"
if ctx.Req.URL.Path != "/user/events" {
middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
}
ctx.Redirect(setting.AppSubURL + "/user/settings/change_password")
return
}
} else if ctx.Req.URL.Path == "/user/settings/change_password" {
// make sure that the form cannot be accessed by users who don't need this
ctx.Redirect(setting.AppSubURL + "/")
return
}
}
// Redirect to dashboard if user tries to visit any non-login page.
if options.SignOutRequired && ctx.IsSigned && ctx.Req.URL.RequestURI() != "/" {
ctx.Redirect(setting.AppSubURL + "/")
return
}
if !options.SignOutRequired && !options.DisableCSRF && ctx.Req.Method == "POST" {
ctx.Csrf.Validate(ctx)
if ctx.Written() {
return
}
}
if options.SignInRequired {
if !ctx.IsSigned {
if ctx.Req.URL.Path != "/user/events" {
middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
}
ctx.Redirect(setting.AppSubURL + "/user/login")
return
} else if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm {
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
ctx.HTML(http.StatusOK, "user/auth/activate")
return
}
}
// Redirect to log in page if auto-signin info is provided and has not signed in.
if !options.SignOutRequired && !ctx.IsSigned &&
len(ctx.GetSiteCookie(setting.CookieUserName)) > 0 {
if ctx.Req.URL.Path != "/user/events" {
middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
}
ctx.Redirect(setting.AppSubURL + "/user/login")
return
}
if options.AdminRequired {
if !ctx.Doer.IsAdmin {
ctx.Error(http.StatusForbidden)
return
}
ctx.Data["PageIsAdmin"] = true
}
}
}
// Checks authentication according to options
func VerifyAuthWithOptionsAPI(options *VerifyOptions) func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
// Check prohibit login users.
if ctx.IsSigned {
if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm {
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
ctx.JSON(http.StatusForbidden, map[string]string{
"message": "This account is not activated.",
})
return
}
if !ctx.Doer.IsActive || ctx.Doer.ProhibitLogin {
log.Info("Failed authentication attempt for %s from %s", ctx.Doer.Name, ctx.RemoteAddr())
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
ctx.JSON(http.StatusForbidden, map[string]string{
"message": "This account is prohibited from signing in, please contact your site administrator.",
})
return
}
if ctx.Doer.MustChangePassword {
ctx.JSON(http.StatusForbidden, map[string]string{
"message": "You must change your password. Change it at: " + setting.AppURL + "/user/change_password",
})
return
}
}
// Redirect to dashboard if user tries to visit any non-login page.
if options.SignOutRequired && ctx.IsSigned && ctx.Req.URL.RequestURI() != "/" {
ctx.Redirect(setting.AppSubURL + "/")
return
}
if options.SignInRequired {
if !ctx.IsSigned {
// Restrict API calls with error message.
ctx.JSON(http.StatusForbidden, map[string]string{
"message": "Only signed in user is allowed to call APIs.",
})
return
} else if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm {
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
ctx.HTML(http.StatusOK, "user/auth/activate")
return
}
if ctx.IsSigned && ctx.IsBasicAuth {
if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
return // Skip 2FA
}
twofa, err := auth.GetTwoFactorByUID(ctx.Doer.ID)
if err != nil {
if auth.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 {
if !ctx.Doer.IsAdmin {
ctx.JSON(http.StatusForbidden, map[string]string{
"message": "You have no permission to request for this.",
})
return
}
}
}
}