mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-17 05:53:15 +02:00
Add optional OIDC authentication with Keycloak, Authentik, and Pocket-ID support
This commit is contained in:
238
internal/auth/oidc.go
Normal file
238
internal/auth/oidc.go
Normal file
@@ -0,0 +1,238 @@
|
||||
// Fail2ban UI - A Swiss made, management interface for Fail2ban.
|
||||
//
|
||||
// Copyright (C) 2025 Swissmakers GmbH (https://swissmakers.ch)
|
||||
//
|
||||
// Licensed under the GNU General Public License, Version 3 (GPL-3.0)
|
||||
// You may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.gnu.org/licenses/gpl-3.0.en.html
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/swissmakers/fail2ban-ui/internal/config"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// OIDCClient holds the OIDC provider, verifier, and OAuth2 configuration
|
||||
type OIDCClient struct {
|
||||
Provider *oidc.Provider
|
||||
Verifier *oidc.IDTokenVerifier
|
||||
OAuth2Config *oauth2.Config
|
||||
Config *config.OIDCConfig
|
||||
}
|
||||
|
||||
// UserInfo represents the authenticated user information
|
||||
type UserInfo struct {
|
||||
ID string
|
||||
Email string
|
||||
Name string
|
||||
Username string
|
||||
}
|
||||
|
||||
var (
|
||||
oidcClient *OIDCClient
|
||||
)
|
||||
|
||||
// contextWithSkipVerify returns a context with an HTTP client that skips TLS verification if enabled
|
||||
func contextWithSkipVerify(ctx context.Context, skipVerify bool) context.Context {
|
||||
if !skipVerify {
|
||||
return ctx
|
||||
}
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
return oidc.ClientContext(ctx, client)
|
||||
}
|
||||
|
||||
// InitializeOIDC sets up the OIDC client from configuration
|
||||
func InitializeOIDC(cfg *config.OIDCConfig) (*OIDCClient, error) {
|
||||
if cfg == nil || !cfg.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Retry OIDC provider discovery with exponential backoff
|
||||
// This handles cases where the provider isn't ready yet (e.g., Keycloak starting up)
|
||||
maxRetries := 10
|
||||
retryDelay := 2 * time.Second
|
||||
var provider *oidc.Provider
|
||||
var err error
|
||||
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
// Create context with timeout for each attempt
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
ctx = contextWithSkipVerify(ctx, cfg.SkipVerify)
|
||||
|
||||
// Try to discover OIDC provider
|
||||
provider, err = oidc.NewProvider(ctx, cfg.IssuerURL)
|
||||
cancel()
|
||||
|
||||
if err == nil {
|
||||
// Success - provider discovered
|
||||
break
|
||||
}
|
||||
|
||||
// Log retry attempt (but don't fail yet)
|
||||
config.DebugLog("OIDC provider discovery attempt %d/%d failed: %v, retrying in %v...", attempt+1, maxRetries, err, retryDelay)
|
||||
|
||||
if attempt < maxRetries-1 {
|
||||
time.Sleep(retryDelay)
|
||||
// Exponential backoff: increase delay for each retry
|
||||
retryDelay = time.Duration(float64(retryDelay) * 1.5)
|
||||
if retryDelay > 10*time.Second {
|
||||
retryDelay = 10 * time.Second
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to discover OIDC provider after %d attempts: %w", maxRetries, err)
|
||||
}
|
||||
|
||||
// Create OAuth2 configuration
|
||||
oauth2Config := &oauth2.Config{
|
||||
ClientID: cfg.ClientID,
|
||||
ClientSecret: cfg.ClientSecret,
|
||||
RedirectURL: cfg.RedirectURL,
|
||||
Endpoint: provider.Endpoint(),
|
||||
Scopes: cfg.Scopes,
|
||||
}
|
||||
|
||||
// Create ID token verifier
|
||||
verifier := provider.Verifier(&oidc.Config{
|
||||
ClientID: cfg.ClientID,
|
||||
})
|
||||
|
||||
oidcClient = &OIDCClient{
|
||||
Provider: provider,
|
||||
Verifier: verifier,
|
||||
OAuth2Config: oauth2Config,
|
||||
Config: cfg,
|
||||
}
|
||||
|
||||
config.DebugLog("OIDC authentication initialized with provider: %s, issuer: %s", cfg.Provider, cfg.IssuerURL)
|
||||
|
||||
return oidcClient, nil
|
||||
}
|
||||
|
||||
// GetOIDCClient returns the initialized OIDC client
|
||||
func GetOIDCClient() *OIDCClient {
|
||||
return oidcClient
|
||||
}
|
||||
|
||||
// IsEnabled returns whether OIDC is enabled
|
||||
func IsEnabled() bool {
|
||||
return oidcClient != nil && oidcClient.Config != nil && oidcClient.Config.Enabled
|
||||
}
|
||||
|
||||
// GetAuthURL generates the authorization URL for OIDC login
|
||||
func (c *OIDCClient) GetAuthURL(state string) string {
|
||||
return c.OAuth2Config.AuthCodeURL(state, oauth2.AccessTypeOffline)
|
||||
}
|
||||
|
||||
// ExchangeCode exchanges the authorization code for tokens
|
||||
func (c *OIDCClient) ExchangeCode(ctx context.Context, code string) (*oauth2.Token, error) {
|
||||
if c.OAuth2Config == nil {
|
||||
return nil, fmt.Errorf("OIDC client not properly initialized")
|
||||
}
|
||||
|
||||
ctx = contextWithSkipVerify(ctx, c.Config.SkipVerify)
|
||||
token, err := c.OAuth2Config.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to exchange code for token: %w", err)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// VerifyToken verifies the ID token and extracts user information
|
||||
func (c *OIDCClient) VerifyToken(ctx context.Context, token *oauth2.Token) (*UserInfo, error) {
|
||||
rawIDToken, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no id_token in token response")
|
||||
}
|
||||
|
||||
ctx = contextWithSkipVerify(ctx, c.Config.SkipVerify)
|
||||
// Verify the ID token
|
||||
idToken, err := c.Verifier.Verify(ctx, rawIDToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to verify ID token: %w", err)
|
||||
}
|
||||
|
||||
// Extract claims
|
||||
var claims struct {
|
||||
Subject string `json:"sub"`
|
||||
Email string `json:"email"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
Name string `json:"name"`
|
||||
PreferredUsername string `json:"preferred_username"`
|
||||
GivenName string `json:"given_name"`
|
||||
FamilyName string `json:"family_name"`
|
||||
}
|
||||
|
||||
if err := idToken.Claims(&claims); err != nil {
|
||||
return nil, fmt.Errorf("failed to extract claims: %w", err)
|
||||
}
|
||||
|
||||
userInfo := &UserInfo{
|
||||
ID: claims.Subject,
|
||||
Email: claims.Email,
|
||||
Name: claims.Name,
|
||||
}
|
||||
|
||||
// Determine username based on configured claim
|
||||
switch c.Config.UsernameClaim {
|
||||
case "email":
|
||||
userInfo.Username = claims.Email
|
||||
case "preferred_username":
|
||||
userInfo.Username = claims.PreferredUsername
|
||||
if userInfo.Username == "" {
|
||||
userInfo.Username = claims.Email // Fallback to email
|
||||
}
|
||||
default:
|
||||
// Try to get the claim value dynamically
|
||||
var claimValue interface{}
|
||||
if err := idToken.Claims(&map[string]interface{}{
|
||||
c.Config.UsernameClaim: &claimValue,
|
||||
}); err == nil {
|
||||
if str, ok := claimValue.(string); ok {
|
||||
userInfo.Username = str
|
||||
}
|
||||
}
|
||||
if userInfo.Username == "" {
|
||||
userInfo.Username = claims.PreferredUsername
|
||||
if userInfo.Username == "" {
|
||||
userInfo.Username = claims.Email
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback name construction
|
||||
if userInfo.Name == "" {
|
||||
if claims.GivenName != "" || claims.FamilyName != "" {
|
||||
userInfo.Name = fmt.Sprintf("%s %s", claims.GivenName, claims.FamilyName)
|
||||
userInfo.Name = strings.TrimSpace(userInfo.Name)
|
||||
}
|
||||
if userInfo.Name == "" {
|
||||
userInfo.Name = userInfo.Username
|
||||
}
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
206
internal/auth/session.go
Normal file
206
internal/auth/session.go
Normal file
@@ -0,0 +1,206 @@
|
||||
// Fail2ban UI - A Swiss made, management interface for Fail2ban.
|
||||
//
|
||||
// Copyright (C) 2025 Swissmakers GmbH (https://swissmakers.ch)
|
||||
//
|
||||
// Licensed under the GNU General Public License, Version 3 (GPL-3.0)
|
||||
// You may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.gnu.org/licenses/gpl-3.0.en.html
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
sessionCookieName = "fail2ban_ui_session"
|
||||
sessionKeyLength = 32 // AES-256
|
||||
)
|
||||
|
||||
// Session represents a user session
|
||||
type Session struct {
|
||||
UserID string `json:"userID"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
}
|
||||
|
||||
var sessionSecret []byte
|
||||
|
||||
// InitializeSessionSecret initializes the session encryption secret
|
||||
func InitializeSessionSecret(secret string) error {
|
||||
if secret == "" {
|
||||
return fmt.Errorf("session secret cannot be empty")
|
||||
}
|
||||
|
||||
// Decode base64 secret or use directly if not base64
|
||||
decoded, err := base64.URLEncoding.DecodeString(secret)
|
||||
if err != nil {
|
||||
// Not base64, use as-is (but ensure it's 32 bytes for AES-256)
|
||||
if len(secret) < sessionKeyLength {
|
||||
return fmt.Errorf("session secret must be at least %d bytes", sessionKeyLength)
|
||||
}
|
||||
// Use first 32 bytes
|
||||
sessionSecret = []byte(secret[:sessionKeyLength])
|
||||
} else {
|
||||
if len(decoded) < sessionKeyLength {
|
||||
return fmt.Errorf("decoded session secret must be at least %d bytes", sessionKeyLength)
|
||||
}
|
||||
sessionSecret = decoded[:sessionKeyLength]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateSession creates a new encrypted session cookie
|
||||
func CreateSession(w http.ResponseWriter, r *http.Request, userInfo *UserInfo, maxAge int) error {
|
||||
session := &Session{
|
||||
UserID: userInfo.ID,
|
||||
Email: userInfo.Email,
|
||||
Name: userInfo.Name,
|
||||
Username: userInfo.Username,
|
||||
ExpiresAt: time.Now().Add(time.Duration(maxAge) * time.Second),
|
||||
}
|
||||
|
||||
// Serialize session to JSON
|
||||
sessionData, err := json.Marshal(session)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal session: %w", err)
|
||||
}
|
||||
|
||||
// Encrypt session data
|
||||
encrypted, err := encrypt(sessionData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt session: %w", err)
|
||||
}
|
||||
|
||||
// Determine if we're using HTTPS
|
||||
isSecure := r != nil && (r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https")
|
||||
|
||||
// Create secure cookie
|
||||
cookie := &http.Cookie{
|
||||
Name: sessionCookieName,
|
||||
Value: encrypted,
|
||||
Path: "/",
|
||||
MaxAge: maxAge,
|
||||
HttpOnly: true,
|
||||
Secure: isSecure, // Only secure over HTTPS
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
|
||||
http.SetCookie(w, cookie)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSession retrieves and validates a session from the cookie
|
||||
func GetSession(r *http.Request) (*Session, error) {
|
||||
cookie, err := r.Cookie(sessionCookieName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("no session cookie: %w", err)
|
||||
}
|
||||
|
||||
// Decrypt session data
|
||||
decrypted, err := decrypt(cookie.Value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt session: %w", err)
|
||||
}
|
||||
|
||||
// Deserialize session
|
||||
var session Session
|
||||
if err := json.Unmarshal(decrypted, &session); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal session: %w", err)
|
||||
}
|
||||
|
||||
// Check if session is expired
|
||||
if time.Now().After(session.ExpiresAt) {
|
||||
return nil, fmt.Errorf("session expired")
|
||||
}
|
||||
|
||||
return &session, nil
|
||||
}
|
||||
|
||||
// DeleteSession clears the session cookie
|
||||
func DeleteSession(w http.ResponseWriter, r *http.Request) {
|
||||
// Determine if we're using HTTPS
|
||||
isSecure := r != nil && (r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https")
|
||||
|
||||
cookie := &http.Cookie{
|
||||
Name: sessionCookieName,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
Secure: isSecure, // Only secure over HTTPS
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
}
|
||||
|
||||
// encrypt encrypts data using AES-GCM
|
||||
func encrypt(plaintext []byte) (string, error) {
|
||||
block, err := aes.NewCipher(sessionSecret)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
|
||||
return base64.URLEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
// decrypt decrypts data using AES-GCM
|
||||
func decrypt(ciphertext string) ([]byte, error) {
|
||||
data, err := base64.URLEncoding.DecodeString(ciphertext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(sessionSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(data) < nonceSize {
|
||||
return nil, fmt.Errorf("ciphertext too short")
|
||||
}
|
||||
|
||||
nonce, ciphertextBytes := data[:nonceSize], data[nonceSize:]
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertextBytes, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
@@ -87,6 +87,22 @@ type AppSettings struct {
|
||||
ConsoleOutput bool `json:"consoleOutput"` // Enable console output in web UI (default: false)
|
||||
}
|
||||
|
||||
// OIDCConfig holds OIDC authentication configuration
|
||||
type OIDCConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Provider string `json:"provider"` // keycloak, authentik, pocketid
|
||||
IssuerURL string `json:"issuerURL"`
|
||||
ClientID string `json:"clientID"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
RedirectURL string `json:"redirectURL"`
|
||||
Scopes []string `json:"scopes"` // Default: ["openid", "profile", "email"]
|
||||
SessionSecret string `json:"sessionSecret"` // For session encryption
|
||||
SessionMaxAge int `json:"sessionMaxAge"` // Session timeout in seconds
|
||||
SkipVerify bool `json:"skipVerify"` // Skip TLS verification (dev only)
|
||||
UsernameClaim string `json:"usernameClaim"` // Claim to use as username
|
||||
LogoutURL string `json:"logoutURL"` // Provider logout URL (optional)
|
||||
}
|
||||
|
||||
type AdvancedActionsConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Threshold int `json:"threshold"`
|
||||
@@ -1428,6 +1444,103 @@ func GetBindAddressFromEnv() (string, bool) {
|
||||
return "0.0.0.0", false
|
||||
}
|
||||
|
||||
// GetOIDCConfigFromEnv reads OIDC configuration from environment variables
|
||||
// Returns nil if OIDC is not enabled
|
||||
func GetOIDCConfigFromEnv() (*OIDCConfig, error) {
|
||||
enabled := os.Getenv("OIDC_ENABLED")
|
||||
if enabled != "true" && enabled != "1" {
|
||||
return nil, nil // OIDC not enabled
|
||||
}
|
||||
|
||||
config := &OIDCConfig{
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
// Required fields
|
||||
config.Provider = os.Getenv("OIDC_PROVIDER")
|
||||
if config.Provider == "" {
|
||||
return nil, fmt.Errorf("OIDC_PROVIDER environment variable is required when OIDC_ENABLED=true")
|
||||
}
|
||||
if config.Provider != "keycloak" && config.Provider != "authentik" && config.Provider != "pocketid" {
|
||||
return nil, fmt.Errorf("OIDC_PROVIDER must be one of: keycloak, authentik, pocketid")
|
||||
}
|
||||
|
||||
config.IssuerURL = os.Getenv("OIDC_ISSUER_URL")
|
||||
if config.IssuerURL == "" {
|
||||
return nil, fmt.Errorf("OIDC_ISSUER_URL environment variable is required when OIDC_ENABLED=true")
|
||||
}
|
||||
|
||||
config.ClientID = os.Getenv("OIDC_CLIENT_ID")
|
||||
if config.ClientID == "" {
|
||||
return nil, fmt.Errorf("OIDC_CLIENT_ID environment variable is required when OIDC_ENABLED=true")
|
||||
}
|
||||
|
||||
config.ClientSecret = os.Getenv("OIDC_CLIENT_SECRET")
|
||||
// If client secret is "auto-configured", try to read from file
|
||||
// This is primarily used for Keycloak's automatic client setup in development
|
||||
if config.ClientSecret == "auto-configured" {
|
||||
secretFile := os.Getenv("OIDC_CLIENT_SECRET_FILE")
|
||||
if secretFile == "" {
|
||||
secretFile = "/config/keycloak-client-secret" // Default path for Keycloak auto-configuration
|
||||
}
|
||||
if secretBytes, err := os.ReadFile(secretFile); err == nil {
|
||||
config.ClientSecret = strings.TrimSpace(string(secretBytes))
|
||||
} else {
|
||||
return nil, fmt.Errorf("OIDC_CLIENT_SECRET is set to 'auto-configured' but could not read from file %s: %w", secretFile, err)
|
||||
}
|
||||
}
|
||||
if config.ClientSecret == "" {
|
||||
return nil, fmt.Errorf("OIDC_CLIENT_SECRET environment variable is required when OIDC_ENABLED=true")
|
||||
}
|
||||
|
||||
config.RedirectURL = os.Getenv("OIDC_REDIRECT_URL")
|
||||
if config.RedirectURL == "" {
|
||||
return nil, fmt.Errorf("OIDC_REDIRECT_URL environment variable is required when OIDC_ENABLED=true")
|
||||
}
|
||||
|
||||
// Optional fields with defaults
|
||||
scopesEnv := os.Getenv("OIDC_SCOPES")
|
||||
if scopesEnv != "" {
|
||||
config.Scopes = strings.Split(scopesEnv, ",")
|
||||
for i := range config.Scopes {
|
||||
config.Scopes[i] = strings.TrimSpace(config.Scopes[i])
|
||||
}
|
||||
} else {
|
||||
config.Scopes = []string{"openid", "profile", "email"}
|
||||
}
|
||||
|
||||
// Set default session max age
|
||||
config.SessionMaxAge = 3600 // Default: 1 hour
|
||||
sessionMaxAgeEnv := os.Getenv("OIDC_SESSION_MAX_AGE")
|
||||
if sessionMaxAgeEnv != "" {
|
||||
if maxAge, err := strconv.Atoi(sessionMaxAgeEnv); err == nil && maxAge > 0 {
|
||||
config.SessionMaxAge = maxAge
|
||||
}
|
||||
}
|
||||
|
||||
config.SessionSecret = os.Getenv("OIDC_SESSION_SECRET")
|
||||
if config.SessionSecret == "" {
|
||||
// Generate a random session secret
|
||||
secretBytes := make([]byte, 32)
|
||||
if _, err := rand.Read(secretBytes); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate session secret: %w", err)
|
||||
}
|
||||
config.SessionSecret = base64.URLEncoding.EncodeToString(secretBytes)
|
||||
}
|
||||
|
||||
skipVerifyEnv := os.Getenv("OIDC_SKIP_VERIFY")
|
||||
config.SkipVerify = (skipVerifyEnv == "true" || skipVerifyEnv == "1")
|
||||
|
||||
config.UsernameClaim = os.Getenv("OIDC_USERNAME_CLAIM")
|
||||
if config.UsernameClaim == "" {
|
||||
config.UsernameClaim = "preferred_username" // Default claim
|
||||
}
|
||||
|
||||
config.LogoutURL = os.Getenv("OIDC_LOGOUT_URL") // Optional
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func GetSettings() AppSettings {
|
||||
settingsLock.RLock()
|
||||
defer settingsLock.RUnlock()
|
||||
|
||||
@@ -368,6 +368,14 @@
|
||||
"toast.ban.title": "Neue Blockierung aufgetreten",
|
||||
"toast.ban.action": "gesperrt in",
|
||||
"toast.unban.title": "IP entsperrt",
|
||||
"toast.unban.action": "entsperrt von"
|
||||
"toast.unban.action": "entsperrt von",
|
||||
"auth.login_title": "Bei Fail2ban UI anmelden",
|
||||
"auth.login_description": "Bitte authentifizieren Sie sich, um auf die Verwaltungsoberfläche zuzugreifen",
|
||||
"auth.login_button": "Mit OIDC anmelden",
|
||||
"auth.logging_in": "Weiterleitung zur Anmeldung...",
|
||||
"auth.logout": "Abmelden",
|
||||
"auth.user_info": "Benutzerinformationen",
|
||||
"auth.session_expired": "Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.",
|
||||
"auth.login_required": "Authentifizierung erforderlich"
|
||||
}
|
||||
|
||||
@@ -368,6 +368,14 @@
|
||||
"toast.ban.title": "Neui Blockierig ufträte",
|
||||
"toast.ban.action": "gsperrt i",
|
||||
"toast.unban.title": "IP entsperrt",
|
||||
"toast.unban.action": "entsperrt vo"
|
||||
"toast.unban.action": "entsperrt vo",
|
||||
"auth.login_title": "Bi Fail2ban UI amäudä",
|
||||
"auth.login_description": "Bitte log di ih, zum uf d Verwaltigsoberflächi zuezgriifä",
|
||||
"auth.login_button": "Mit OIDC amäudä",
|
||||
"auth.logging_in": "Wiiterleitig zur Amäudig...",
|
||||
"auth.logout": "Abmäudä",
|
||||
"auth.user_info": "Benutzerinformationä",
|
||||
"auth.session_expired": "Ihri Sitzig isch abglaufä. Bitte mäudä di erneut a.",
|
||||
"auth.login_required": "Authentifizierig erforderlich"
|
||||
}
|
||||
|
||||
@@ -368,6 +368,14 @@
|
||||
"toast.ban.title": "New block occurred",
|
||||
"toast.ban.action": "banned in",
|
||||
"toast.unban.title": "IP unblocked",
|
||||
"toast.unban.action": "unblocked from"
|
||||
"toast.unban.action": "unblocked from",
|
||||
"auth.login_title": "Sign in to Fail2ban UI",
|
||||
"auth.login_description": "Please authenticate to access the management interface",
|
||||
"auth.login_button": "Sign in with OIDC",
|
||||
"auth.logging_in": "Redirecting to login...",
|
||||
"auth.logout": "Logout",
|
||||
"auth.user_info": "User Information",
|
||||
"auth.session_expired": "Your session has expired. Please log in again.",
|
||||
"auth.login_required": "Authentication required"
|
||||
}
|
||||
|
||||
@@ -368,5 +368,13 @@
|
||||
"toast.ban.title": "Nuevo bloqueo ocurrido",
|
||||
"toast.ban.action": "bloqueado en",
|
||||
"toast.unban.title": "IP desbloqueada",
|
||||
"toast.unban.action": "desbloqueada de"
|
||||
"toast.unban.action": "desbloqueada de",
|
||||
"auth.login_title": "Iniciar sesión en Fail2ban UI",
|
||||
"auth.login_description": "Por favor, autentíquese para acceder a la interfaz de gestión",
|
||||
"auth.login_button": "Iniciar sesión con OIDC",
|
||||
"auth.logging_in": "Redirigiendo al inicio de sesión...",
|
||||
"auth.logout": "Cerrar sesión",
|
||||
"auth.user_info": "Información del usuario",
|
||||
"auth.session_expired": "Su sesión ha expirado. Por favor, inicie sesión nuevamente.",
|
||||
"auth.login_required": "Autenticación requerida"
|
||||
}
|
||||
|
||||
@@ -368,5 +368,13 @@
|
||||
"toast.ban.title": "Nouveau blocage survenu",
|
||||
"toast.ban.action": "banni dans",
|
||||
"toast.unban.title": "IP débloquée",
|
||||
"toast.unban.action": "débloquée de"
|
||||
"toast.unban.action": "débloquée de",
|
||||
"auth.login_title": "Se connecter à Fail2ban UI",
|
||||
"auth.login_description": "Veuillez vous authentifier pour accéder à l'interface de gestion",
|
||||
"auth.login_button": "Se connecter avec OIDC",
|
||||
"auth.logging_in": "Redirection vers la connexion...",
|
||||
"auth.logout": "Déconnexion",
|
||||
"auth.user_info": "Informations utilisateur",
|
||||
"auth.session_expired": "Votre session a expiré. Veuillez vous reconnecter.",
|
||||
"auth.login_required": "Authentification requise"
|
||||
}
|
||||
|
||||
@@ -368,5 +368,13 @@
|
||||
"toast.ban.title": "Nuovo blocco verificato",
|
||||
"toast.ban.action": "bannato in",
|
||||
"toast.unban.title": "IP sbloccato",
|
||||
"toast.unban.action": "sbloccato da"
|
||||
"toast.unban.action": "sbloccato da",
|
||||
"auth.login_title": "Accedi a Fail2ban UI",
|
||||
"auth.login_description": "Si prega di autenticarsi per accedere all'interfaccia di gestione",
|
||||
"auth.login_button": "Accedi con OIDC",
|
||||
"auth.logging_in": "Reindirizzamento al login...",
|
||||
"auth.logout": "Esci",
|
||||
"auth.user_info": "Informazioni utente",
|
||||
"auth.session_expired": "La tua sessione è scaduta. Si prega di accedere nuovamente.",
|
||||
"auth.login_required": "Autenticazione richiesta"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user