Add SMTP support for Authelia notifications and fix config persistence

SMTP: Add host, port, username, sender, and password fields to the
Authelia configuration. Uses modern address format (submission:// for
STARTTLS on 587, smtps:// for implicit TLS on 465) with explicit TLS
server name. Falls back to filesystem notifier when SMTP is not
configured.

Fixes: Config changes now persist before attempting service restart,
so a restart failure (e.g. bad SMTP credentials) no longer prevents
saving. The specific Authelia error is extracted from the journal and
shown in the UI.

Frontend: SMTP fields added to the Authentication config card. Form
no longer continuously resets from server state while user is editing.
OIDC client edit UI added (pencil icon).
This commit is contained in:
2026-07-12 12:42:36 +00:00
parent d796a79f79
commit 134c01fe5e
7 changed files with 224 additions and 40 deletions

View File

@@ -31,6 +31,19 @@ func (api *API) AutheliaStatus(w http.ResponseWriter, r *http.Request) {
defaultPolicy = state.Cloud.Authelia.DefaultPolicy
}
smtp := struct {
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Sender string `json:"sender"`
}{}
if state != nil {
smtp.Host = state.Cloud.Authelia.SMTP.Host
smtp.Port = state.Cloud.Authelia.SMTP.Port
smtp.Username = state.Cloud.Authelia.SMTP.Username
smtp.Sender = state.Cloud.Authelia.SMTP.Sender
}
respondJSON(w, http.StatusOK, map[string]any{
"active": status.Active,
"version": status.Version,
@@ -40,6 +53,7 @@ func (api *API) AutheliaStatus(w http.ResponseWriter, r *http.Request) {
"userCount": api.authelia.UserCount(),
"clientCount": api.authelia.OIDCClientCount(),
"installed": api.authelia.IsInstalled(),
"smtp": smtp,
})
}
@@ -59,6 +73,11 @@ func (api *API) AutheliaUpdateConfig(w http.ResponseWriter, r *http.Request) {
Enabled *bool `json:"enabled"`
Domain *string `json:"domain"`
DefaultPolicy *string `json:"defaultPolicy"`
SMTPHost *string `json:"smtpHost"`
SMTPPort *int `json:"smtpPort"`
SMTPUsername *string `json:"smtpUsername"`
SMTPSender *string `json:"smtpSender"`
SMTPPassword *string `json:"smtpPassword"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid JSON")
@@ -79,6 +98,21 @@ func (api *API) AutheliaUpdateConfig(w http.ResponseWriter, r *http.Request) {
if req.Enabled != nil {
state.Cloud.Authelia.Enabled = *req.Enabled
}
if req.SMTPHost != nil {
state.Cloud.Authelia.SMTP.Host = *req.SMTPHost
}
if req.SMTPPort != nil {
state.Cloud.Authelia.SMTP.Port = *req.SMTPPort
}
if req.SMTPUsername != nil {
state.Cloud.Authelia.SMTP.Username = *req.SMTPUsername
}
if req.SMTPSender != nil {
state.Cloud.Authelia.SMTP.Sender = *req.SMTPSender
}
if req.SMTPPassword != nil && *req.SMTPPassword != "" {
_ = api.secrets.SetSecret("authelia.smtpPassword", *req.SMTPPassword)
}
// Enabling Authelia
if state.Cloud.Authelia.Enabled {
@@ -132,27 +166,36 @@ func (api *API) AutheliaUpdateConfig(w http.ResponseWriter, r *http.Request) {
api.ensureAuthDomain(state.Cloud.Authelia.Domain)
// Start service — requires at least one user
startErr := error(nil)
if api.authelia.UserCount() > 0 {
if err := api.authelia.RestartService(); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to start Authelia: %v", err))
return
}
startErr = api.authelia.RestartService()
}
// Save state and reconcile regardless of whether the service started
if err := config.SaveState(state, api.statePath()); err != nil {
respondError(w, http.StatusInternalServerError, "Failed to save state")
return
}
go api.reconcileNetworking()
api.broadcastAutheliaEvent("authelia:config", "Authelia configuration updated")
if startErr != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Configuration saved but Authelia failed to start: %v", startErr))
return
}
} else {
// Disabling — stop service and deregister domain
_ = api.authelia.StopService()
api.deregisterAuthDomain()
if err := config.SaveState(state, api.statePath()); err != nil {
respondError(w, http.StatusInternalServerError, "Failed to save state")
return
}
go api.reconcileNetworking()
api.broadcastAutheliaEvent("authelia:config", "Authelia configuration updated")
}
if err := config.SaveState(state, api.statePath()); err != nil {
respondError(w, http.StatusInternalServerError, "Failed to save state")
return
}
// Trigger reconciliation to update HAProxy
go api.reconcileNetworking()
api.broadcastAutheliaEvent("authelia:config", "Authelia configuration updated")
respondMessage(w, http.StatusOK, "Authelia configuration updated")
}
@@ -382,6 +425,8 @@ func (api *API) generateAutheliaConfig(state *config.State) error {
sessionDomain := authelia.SessionDomainFromAuthDomain(state.Cloud.Authelia.Domain)
smtpPassword, _ := api.secrets.GetSecret("authelia.smtpPassword")
opts := authelia.ConfigOpts{
Domain: state.Cloud.Authelia.Domain,
JWTSecret: jwtSecret,
@@ -391,6 +436,11 @@ func (api *API) generateAutheliaConfig(state *config.State) error {
DefaultPolicy: defaultPolicy,
SessionDomain: sessionDomain,
OIDCClients: clients,
SMTPHost: state.Cloud.Authelia.SMTP.Host,
SMTPPort: state.Cloud.Authelia.SMTP.Port,
SMTPUsername: state.Cloud.Authelia.SMTP.Username,
SMTPSender: state.Cloud.Authelia.SMTP.Sender,
SMTPPassword: smtpPassword,
}
return api.authelia.GenerateConfig(opts)