From 3e74c8a4699e3cf094eb8ab2c0cc30fb401500a4 Mon Sep 17 00:00:00 2001 From: Abhishek Shroff Date: Wed, 16 Jul 2025 01:17:05 +0530 Subject: [PATCH] [server][auth] Print warning and disable password reset and magic links if necessary --- server/internal/api/v1/api.go | 5 ++- server/internal/api/v1/auth/routes.go | 50 ++++++++++++--------- server/internal/auth/auth.go | 23 ++++++++-- server/internal/auth/config.go | 2 + server/internal/command/config.defaults.yml | 2 + server/internal/command/serve/cmd.go | 3 +- server/internal/mail/mail.go | 1 - 7 files changed, 57 insertions(+), 29 deletions(-) diff --git a/server/internal/api/v1/api.go b/server/internal/api/v1/api.go index 690aaa9d..c58b83ca 100644 --- a/server/internal/api/v1/api.go +++ b/server/internal/api/v1/api.go @@ -9,11 +9,12 @@ import ( "codeberg.org/shroff/phylum/server/internal/api/v1/trash" "codeberg.org/shroff/phylum/server/internal/api/v1/users" "github.com/gin-gonic/gin" + "github.com/rs/zerolog" ) -func Setup(r *gin.RouterGroup) { +func Setup(r *gin.RouterGroup, logger *zerolog.Logger) { // Unauthenticated routes - auth.SetupRoutes(r) + auth.SetupRoutes(r, logger) // Authenticated routes r.Use(authenticator.Require) diff --git a/server/internal/api/v1/auth/routes.go b/server/internal/api/v1/auth/routes.go index efc48be8..9ed5c123 100644 --- a/server/internal/api/v1/auth/routes.go +++ b/server/internal/api/v1/auth/routes.go @@ -14,11 +14,13 @@ import ( "codeberg.org/shroff/phylum/server/internal/db" "codeberg.org/shroff/phylum/server/internal/mail" "github.com/gin-gonic/gin" + "github.com/rs/zerolog" ) type PasswordConfigResponse struct { Password bool `json:"password"` PasswordReset bool `json:"password_reset"` + MagicLinks bool `json:"magic_links"` OpenIDProviders []openid.Provider `json:"openid_providers"` } @@ -41,39 +43,45 @@ type resetPasswordParams struct { Password string `json:"password" form:"password" binding:"required"` } -func SetupRoutes(r *gin.RouterGroup) { +var passwordConfigResponse PasswordConfigResponse + +func SetupRoutes(r *gin.RouterGroup, logger *zerolog.Logger) { group := r.Group("/auth") group.GET("/config", handleConfig) - if passwordLoginSupported() { + if auth.PasswordAuthEnabled() { + passwordConfigResponse.Password = true group.POST("/password/login", handlePasswordAuth) } - if passwordResetSupported() { - group.POST("/password/request-reset", handleRequestPasswordReset) - group.POST("/password/reset", handleResetPassword) + if auth.PasswordResetEnabled() { + if mail.Configured() { + passwordConfigResponse.PasswordReset = true + group.POST("/password/request-reset", handleRequestPasswordReset) + group.POST("/password/reset", handleResetPassword) + } else { + logger.Warn().Msg("Password reset enabled but mail not configured") + } } + + group.POST("/token/login", handleTokenLogin) + if auth.MagicLinksEnabled() { + if mail.Configured() { + passwordConfigResponse.MagicLinks = true + group.POST("/token/request", handleTokenRequest) + } else { + logger.Warn().Msg("Magic login links enabled but mail not configured") + } + } + + passwordConfigResponse.OpenIDProviders = auth.OpenIDProviders() group.GET("/oauth/start", handleOAuthStart) group.GET("/oauth/redirect", handleOAuthRedirect) - group.POST("/token/login", handleTokenLogin) - group.POST("/token/request", handleTokenRequest) + group.POST("/set-cookie", authenticator.RequireAPIKey, handleSetCookie) } func handleConfig(c *gin.Context) { - response := PasswordConfigResponse{ - Password: passwordLoginSupported(), - PasswordReset: passwordResetSupported(), - OpenIDProviders: auth.OpenIDProviders(), - } - c.JSON(http.StatusOK, response) -} - -func passwordLoginSupported() bool { - return auth.SupportsPasswordAuth() -} - -func passwordResetSupported() bool { - return auth.SupportsPasswordReset() && mail.Configured() + c.JSON(http.StatusOK, passwordConfigResponse) } func handleOAuthStart(c *gin.Context) { diff --git a/server/internal/auth/auth.go b/server/internal/auth/auth.go index 3ae2c99a..6e7abcac 100644 --- a/server/internal/auth/auth.go +++ b/server/internal/auth/auth.go @@ -18,6 +18,8 @@ import ( var autoCreate []*regexp.Regexp var passwordRequirements PasswordRequirements var passwordBackend PasswordBackend +var magicLinksEnabled bool +var passwordResetEnabled bool const apiKeyLength = 15 const resetTokenLength = 15 @@ -31,12 +33,16 @@ var b32Encoder = base32.StdEncoding.WithPadding(base32.NoPadding) var ErrTokenInvalid = errors.New("invalid token") var ErrCredentialsInvalid = errors.New("invalid credentials") -func SupportsPasswordAuth() bool { +func PasswordAuthEnabled() bool { return passwordBackend != nil } -func SupportsPasswordReset() bool { - return passwordBackend != nil && passwordBackend.SupportsPasswordUpdate() +func PasswordResetEnabled() bool { + return passwordResetEnabled +} + +func MagicLinksEnabled() bool { + return magicLinksEnabled } func OpenIDProviders() []openid.Provider { @@ -45,6 +51,7 @@ func OpenIDProviders() []openid.Provider { func Initialize(cfg Config, log zerolog.Logger) error { passwordRequirements = cfg.Password.Requirements + magicLinksEnabled = cfg.MagicLink if cfg.Password.Backend == "crypt" { if crypt, err := crypt.New(cfg.Password.Crypt); err != nil { @@ -68,6 +75,16 @@ func Initialize(cfg Config, log zerolog.Logger) error { autoCreate[i] = regexp.MustCompile(strings.ReplaceAll(strings.ToLower(w), "*", ".*")) } + if cfg.Password.ResetEnabled { + if passwordBackend == nil { + log.Warn().Msg("Password reset enabled but no backend configured") + } else if !passwordBackend.SupportsPasswordUpdate() { + log.Warn().Msg("Password reset enabled but backend does not support update") + } else { + passwordResetEnabled = true + } + } + return openid.Init(cfg.OpenID, log) } diff --git a/server/internal/auth/config.go b/server/internal/auth/config.go index ae57e487..2f43e856 100644 --- a/server/internal/auth/config.go +++ b/server/internal/auth/config.go @@ -9,11 +9,13 @@ import ( type Config struct { AutoCreate []string `koanf:"auto_create"` Password PasswordConfig `koanf:"password"` + MagicLink bool `koanf:"magic_link"` OpenID openid.Config `koanf:"openid"` } type PasswordConfig struct { Backend string `koanf:"backend"` + ResetEnabled bool `koanf:"reset_enabled"` Crypt crypt.Config `koanf:"crypt"` LDAP ldap.Config `koanf:"ldap"` Requirements PasswordRequirements `koanf:"requirements"` diff --git a/server/internal/command/config.defaults.yml b/server/internal/command/config.defaults.yml index b4d60aa7..eb658d22 100644 --- a/server/internal/command/config.defaults.yml +++ b/server/internal/command/config.defaults.yml @@ -30,9 +30,11 @@ auth: # - "*@example.com" # - "user@example.com" # - "user*@example.com" + magic_link: true password: # backend is one of crypt, ldap, or none backend: crypt + reset_enabled: true crypt: # hash is either argon2 or pbkdf2 hash: argon2 diff --git a/server/internal/command/serve/cmd.go b/server/internal/command/serve/cmd.go index 76995830..efd4c531 100644 --- a/server/internal/command/serve/cmd.go +++ b/server/internal/command/serve/cmd.go @@ -52,7 +52,6 @@ func SetupCommand() *cobra.Command { if !Cfg.Debug { gin.SetMode(gin.ReleaseMode) } - cmd.Context() logger := zerolog.Ctx(cmd.Context()) engine := createEngine(logger) @@ -61,7 +60,7 @@ func SetupCommand() *cobra.Command { webdav.SetupHandler(r) logger.Info().Str("path", r.BasePath()).Msg("WebDAV Enabled") } - apiv1.Setup(engine.Group("/api/v1")) + apiv1.Setup(engine.Group("/api/v1"), logger) publink.Setup(engine.Group(Cfg.PublinkPath)) setupWebApp(engine, "web") diff --git a/server/internal/mail/mail.go b/server/internal/mail/mail.go index e856089b..c617aeb6 100644 --- a/server/internal/mail/mail.go +++ b/server/internal/mail/mail.go @@ -25,7 +25,6 @@ func Initialize(cfg Config, l zerolog.Logger) error { initialized = true logger = l.With().Str("mod", "email").Logger() if cfg.SMTP.Host == "" { - logger.Info().Msg("email not configured") return ErrEmailNotConfigured }