mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-07-25 10:57:37 +00:00
**Backport:** https://codeberg.org/forgejo/forgejo/pulls/13304 Regression in #12776 -- `[service].REQUIRE_SIGNIN_VIEW` and `[service.explore].REQUIRE_SIGNIN_VIEW` values were being cached into a constructed middleware during module initialization, before the `settings` object had been initialized by reading settings from the config. As a result, neither setting applied to the middleware. Apparently zero automated tests cover this capability. Fixed by changing the bool into a closure for realtime access to the setting. Added automated tests. Verified that the tests failed when broken. ### Tests for Go changes - I added test coverage for Go changes... - [ ] in their respective `*_test.go` for unit tests. - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I ran... - [x] `make pr-go` before pushing ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [ ] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change. - [x] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change. - Prerelease regression. Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/13307 Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org>
205 lines
6.6 KiB
Go
205 lines
6.6 KiB
Go
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package shared
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
auth_model "forgejo.org/models/auth"
|
|
"forgejo.org/modules/log"
|
|
"forgejo.org/modules/setting"
|
|
apiv1_permissions "forgejo.org/routers/api/v1/permissions"
|
|
apiv1_permissions_testhelpers "forgejo.org/routers/api/v1/permissions/testhelpers"
|
|
"forgejo.org/routers/common"
|
|
"forgejo.org/services/auth"
|
|
auth_method "forgejo.org/services/auth/method"
|
|
"forgejo.org/services/context"
|
|
|
|
"github.com/go-chi/cors"
|
|
)
|
|
|
|
func Middlewares() (stack []any) {
|
|
stack = append(stack, securityHeaders())
|
|
|
|
if setting.CORSConfig.Enabled {
|
|
stack = append(stack, cors.Handler(cors.Options{
|
|
AllowedOrigins: setting.CORSConfig.AllowDomain,
|
|
AllowedMethods: setting.CORSConfig.Methods,
|
|
AllowCredentials: setting.CORSConfig.AllowCredentials,
|
|
AllowedHeaders: append([]string{"Authorization", "X-Gitea-OTP", "X-Forgejo-OTP"}, setting.CORSConfig.Headers...),
|
|
MaxAge: int(setting.CORSConfig.MaxAge.Seconds()),
|
|
}))
|
|
}
|
|
return append(stack,
|
|
context.APIContexter(),
|
|
|
|
checkDeprecatedAuthMethods,
|
|
// Get user from session if logged in.
|
|
apiAuthentication(buildAuthGroup()),
|
|
apiAuthorization(),
|
|
verifyAuthWithOptions(&common.VerifyOptions{
|
|
SignInRequired: func() bool { return setting.Service.RequireSignInView },
|
|
}),
|
|
)
|
|
}
|
|
|
|
func buildAuthGroup() *auth_method.Group {
|
|
group := auth_method.NewGroup(
|
|
&auth_method.OAuth2{},
|
|
&auth_method.HTTPSign{},
|
|
&auth_method.Basic{}, // FIXME: this should be removed once we don't allow basic auth in API
|
|
&auth_method.AccessToken{
|
|
PermitBasic: true,
|
|
PermitBearer: true,
|
|
},
|
|
&auth_method.ActionRuntimeToken{},
|
|
&auth_method.ActionTaskToken{
|
|
PermitBasic: true,
|
|
PermitBearer: true,
|
|
},
|
|
&auth_method.AuthorizedIntegration{},
|
|
)
|
|
if setting.Service.EnableReverseProxyAuthAPI {
|
|
group.Add(&auth_method.ReverseProxy{})
|
|
}
|
|
|
|
return group
|
|
}
|
|
|
|
func apiAuthentication(authMethod auth.Method) func(*context.APIContext) {
|
|
return func(ctx *context.APIContext) {
|
|
output := common.AuthShared(ctx.Base, nil, authMethod)
|
|
var ar auth.AuthenticationResult
|
|
switch v := output.(type) {
|
|
case *auth.AuthenticationSuccess:
|
|
ar = v.Result
|
|
case *auth.AuthenticationNotAttempted:
|
|
ar = &auth.UnauthenticatedResult{}
|
|
case *auth.AuthenticationAttemptedIncorrectCredential:
|
|
ctx.Error(http.StatusUnauthorized, "APIAuth", v.Error)
|
|
return
|
|
case *auth.AuthenticationError:
|
|
ctx.ServerError("authentication error", v.Error)
|
|
return
|
|
default:
|
|
ctx.ServerError("authentication error", errors.New("unexpected result from common.AuthShared"))
|
|
return
|
|
}
|
|
if ar == nil {
|
|
ctx.ServerError("nil authentication result", errors.New("nil authentication result"))
|
|
return
|
|
}
|
|
ctx.SetDoer(ar.User())
|
|
ctx.SetIsSigned(ctx.Doer() != nil)
|
|
ctx.SetAuthentication(ar)
|
|
}
|
|
}
|
|
|
|
func apiAuthorization() func(ctx *context.APIContext) {
|
|
apiv1_permissions_testhelpers.RecordSignature(apiv1_permissions.APIAuthorization)
|
|
return func(ctx *context.APIContext) {
|
|
apiv1_permissions.APIAuthorization(ctx)
|
|
}
|
|
}
|
|
|
|
// verifyAuthWithOptions checks authentication according to options
|
|
func verifyAuthWithOptions(options *common.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
|
|
}
|
|
|
|
if ctx.Doer().MustHaveTwoFactor() {
|
|
hasTwoFactor, err := auth_model.HasTwoFactorByUID(ctx, ctx.Doer().ID)
|
|
if err != nil {
|
|
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
|
|
log.Error("Error getting 2fa: %s", err)
|
|
ctx.JSON(http.StatusInternalServerError, map[string]string{
|
|
"message": fmt.Sprintf("Error getting 2fa: %s", err),
|
|
})
|
|
return
|
|
}
|
|
if !hasTwoFactor {
|
|
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
|
|
ctx.JSON(http.StatusForbidden, map[string]string{
|
|
"message": ctx.Locale.TrString("error.must_enable_2fa", fmt.Sprintf("%suser/settings/security", setting.AppURL)),
|
|
})
|
|
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 != nil && 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.JSON(http.StatusForbidden, map[string]string{
|
|
"message": "This account is not activated.",
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
if options.AdminRequired {
|
|
if !ctx.IsUserSiteAdmin() {
|
|
ctx.JSON(http.StatusForbidden, map[string]string{
|
|
"message": "You have no permission to request for this.",
|
|
})
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// check for and warn against deprecated authentication options
|
|
func checkDeprecatedAuthMethods(ctx *context.APIContext) {
|
|
if ctx.FormString("token") != "" || ctx.FormString("access_token") != "" {
|
|
ctx.Resp.Header().Set("Warning", "token and access_token API authentication is deprecated and will be removed in Forgejo v13.0.0. Please use AuthorizationHeaderToken instead. Existing queries will continue to work but without authorization.")
|
|
}
|
|
}
|
|
|
|
func securityHeaders() func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
|
// CORB: https://www.chromium.org/Home/chromium-security/corb-for-developers
|
|
// http://stackoverflow.com/a/3146618/244009
|
|
resp.Header().Set("x-content-type-options", "nosniff")
|
|
next.ServeHTTP(resp, req)
|
|
})
|
|
}
|
|
}
|