mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-11 13:47:05 +02:00
Implement additional env variable to hide loginpage if desired
This commit is contained in:
@@ -229,6 +229,9 @@ services:
|
||||
- OIDC_SESSION_MAX_AGE=7200
|
||||
- OIDC_USERNAME_CLAIM=preferred_username
|
||||
- OIDC_SKIP_VERIFY=true
|
||||
# Optional: Skip login page and redirect directly to OIDC provider (default: false)
|
||||
# When set to true, users are immediately redirected to the OIDC provider without showing the login page
|
||||
#- OIDC_SKIP_LOGINPAGE=true
|
||||
# Optional: Logout URL
|
||||
#- OIDC_LOGOUT_URL=${KEYCLOAK_URL}/realms/master/protocol/openid-connect/logout
|
||||
|
||||
|
||||
@@ -100,6 +100,9 @@ services:
|
||||
# Authentik: https://authentik.example.com/application/o/your-client-slug/protocol/openid-connect/logout
|
||||
# Pocket-ID: https://pocket-id.example.com/protocol/openid-connect/logout
|
||||
# - OIDC_LOGOUT_URL=https://keycloak.example.com/realms/your-realm/protocol/openid-connect/logout
|
||||
# Optional: Skip login page and redirect directly to OIDC provider (default: false)
|
||||
# When set to true, users are immediately redirected to the OIDC provider without showing the login page
|
||||
# - OIDC_SKIP_LOGINPAGE=true
|
||||
|
||||
volumes:
|
||||
# Required for fail2ban-ui: Stores SQLite database, application settings, and SSH keys of the fail2ban-ui container
|
||||
|
||||
@@ -81,6 +81,9 @@ services:
|
||||
# Authentik: https://authentik.example.com/application/o/your-client-slug/protocol/openid-connect/logout
|
||||
# Pocket-ID: https://pocket-id.example.com/protocol/openid-connect/logout
|
||||
# - OIDC_LOGOUT_URL=https://keycloak.example.com/realms/your-realm/protocol/openid-connect/logout
|
||||
# Optional: Skip login page and redirect directly to OIDC provider (default: false)
|
||||
# When set to true, users are immediately redirected to the OIDC provider without showing the login page
|
||||
# - OIDC_SKIP_LOGINPAGE=true
|
||||
|
||||
volumes:
|
||||
# Required for fail2ban-ui: Stores SQLite database, application settings, and SSH keys of the fail2ban-ui container
|
||||
|
||||
@@ -141,6 +141,14 @@ func IsEnabled() bool {
|
||||
return oidcClient != nil && oidcClient.Config != nil && oidcClient.Config.Enabled
|
||||
}
|
||||
|
||||
// GetConfig returns the OIDC configuration
|
||||
func GetConfig() *config.OIDCConfig {
|
||||
if oidcClient == nil {
|
||||
return nil
|
||||
}
|
||||
return oidcClient.Config
|
||||
}
|
||||
|
||||
// GetAuthURL generates the authorization URL for OIDC login
|
||||
func (c *OIDCClient) GetAuthURL(state string) string {
|
||||
return c.OAuth2Config.AuthCodeURL(state, oauth2.AccessTypeOffline)
|
||||
|
||||
@@ -101,6 +101,7 @@ type OIDCConfig struct {
|
||||
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)
|
||||
SkipLoginPage bool `json:"skipLoginPage"` // Skip login page and redirect directly to OIDC provider (default: false)
|
||||
}
|
||||
|
||||
type AdvancedActionsConfig struct {
|
||||
@@ -1518,6 +1519,10 @@ func GetOIDCConfigFromEnv() (*OIDCConfig, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Skip login page option (default: false)
|
||||
skipLoginPageEnv := os.Getenv("OIDC_SKIP_LOGINPAGE")
|
||||
config.SkipLoginPage = skipLoginPageEnv == "true" || skipLoginPageEnv == "1"
|
||||
|
||||
config.SessionSecret = os.Getenv("OIDC_SESSION_SECRET")
|
||||
if config.SessionSecret == "" {
|
||||
// Generate a random session secret
|
||||
|
||||
@@ -1126,10 +1126,22 @@ func renderIndexPage(c *gin.Context) {
|
||||
// Default is enabled (false means enabled, true means disabled)
|
||||
disableExternalIP := os.Getenv("DISABLE_EXTERNAL_IP_LOOKUP") == "true" || os.Getenv("DISABLE_EXTERNAL_IP_LOOKUP") == "1"
|
||||
|
||||
// Check if OIDC is enabled and skip login page setting
|
||||
oidcEnabled := auth.IsEnabled()
|
||||
skipLoginPage := false
|
||||
if oidcEnabled {
|
||||
oidcConfig := auth.GetConfig()
|
||||
if oidcConfig != nil {
|
||||
skipLoginPage = oidcConfig.SkipLoginPage
|
||||
}
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
||||
"timestamp": time.Now().Format(time.RFC1123),
|
||||
"version": time.Now().Unix(),
|
||||
"disableExternalIP": disableExternalIP,
|
||||
"oidcEnabled": oidcEnabled,
|
||||
"skipLoginPage": skipLoginPage,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3018,6 +3030,7 @@ func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||
|
||||
// LoginHandler shows the login page or initiates the OIDC login flow
|
||||
// If action=redirect query parameter is present, redirects to OIDC provider
|
||||
// If OIDC_SKIP_LOGINPAGE is true, redirects directly to OIDC provider
|
||||
// Otherwise, renders the login page
|
||||
func LoginHandler(c *gin.Context) {
|
||||
oidcClient := auth.GetOIDCClient()
|
||||
@@ -3026,6 +3039,40 @@ func LoginHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if skip login page is enabled
|
||||
oidcConfig := auth.GetConfig()
|
||||
if oidcConfig != nil && oidcConfig.SkipLoginPage {
|
||||
// Skip login page - redirect directly to OIDC provider
|
||||
// Generate state parameter for CSRF protection
|
||||
stateBytes := make([]byte, 32)
|
||||
if _, err := rand.Read(stateBytes); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate state parameter"})
|
||||
return
|
||||
}
|
||||
state := base64.URLEncoding.EncodeToString(stateBytes)
|
||||
|
||||
// Determine if we're using HTTPS
|
||||
isSecure := c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https"
|
||||
|
||||
// Store state in session cookie for validation
|
||||
stateCookie := &http.Cookie{
|
||||
Name: "oidc_state",
|
||||
Value: state,
|
||||
Path: "/",
|
||||
MaxAge: 600, // 10 minutes
|
||||
HttpOnly: true,
|
||||
Secure: isSecure, // Only secure over HTTPS
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
http.SetCookie(c.Writer, stateCookie)
|
||||
config.DebugLog("Set state cookie: %s (Secure: %v)", state, isSecure)
|
||||
|
||||
// Get authorization URL and redirect
|
||||
authURL := oidcClient.GetAuthURL(state)
|
||||
c.Redirect(http.StatusFound, authURL)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this is a redirect action (triggered by clicking the login button)
|
||||
if c.Query("action") == "redirect" {
|
||||
// Generate state parameter for CSRF protection
|
||||
@@ -3205,11 +3252,18 @@ func AuthStatusHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
oidcConfig := auth.GetConfig()
|
||||
skipLoginPage := false
|
||||
if oidcConfig != nil {
|
||||
skipLoginPage = oidcConfig.SkipLoginPage
|
||||
}
|
||||
|
||||
session, err := auth.GetSession(c.Request)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"enabled": true,
|
||||
"authenticated": false,
|
||||
"skipLoginPage": skipLoginPage,
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -3217,6 +3271,7 @@ func AuthStatusHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"enabled": true,
|
||||
"authenticated": true,
|
||||
"skipLoginPage": skipLoginPage,
|
||||
"user": gin.H{
|
||||
"id": session.UserID,
|
||||
"email": session.Email,
|
||||
|
||||
@@ -33,6 +33,35 @@
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Both login page and main content are hidden by default */
|
||||
#loginPage.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#mainContent.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
nav.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#footer.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide login page when OIDC is not enabled (additional safety) */
|
||||
body[data-oidc-enabled="false"] #loginPage {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
/* Hide login page when skipLoginPage is enabled (additional safety) */
|
||||
body[data-skip-login-page="true"] #loginPage {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
/* Ensure login page is visible when shown */
|
||||
body:has(#loginPage:not(.hidden)) {
|
||||
background-color: #f3f4f6;
|
||||
|
||||
@@ -7,15 +7,30 @@ let currentUser = null;
|
||||
|
||||
// Check authentication status on page load
|
||||
async function checkAuthStatus() {
|
||||
// Immediately hide main content to prevent flash
|
||||
// Both login page and main content are hidden by default
|
||||
// We'll show the appropriate one based on authentication status
|
||||
const mainContent = document.getElementById('mainContent');
|
||||
const nav = document.querySelector('nav');
|
||||
const loginPage = document.getElementById('loginPage');
|
||||
const footer = document.getElementById('footer');
|
||||
|
||||
// Ensure all are hidden initially to prevent flash
|
||||
if (loginPage) {
|
||||
loginPage.classList.add('hidden');
|
||||
loginPage.style.display = 'none';
|
||||
}
|
||||
if (mainContent) {
|
||||
mainContent.classList.add('hidden');
|
||||
mainContent.style.display = 'none';
|
||||
}
|
||||
if (nav) {
|
||||
nav.classList.add('hidden');
|
||||
nav.style.display = 'none';
|
||||
}
|
||||
if (footer) {
|
||||
footer.classList.add('hidden');
|
||||
footer.style.display = 'none';
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/auth/status', {
|
||||
@@ -29,25 +44,42 @@ async function checkAuthStatus() {
|
||||
const data = await response.json();
|
||||
authEnabled = data.enabled || false;
|
||||
isAuthenticated = data.authenticated || false;
|
||||
const skipLoginPageFlag = data.skipLoginPage || false;
|
||||
|
||||
if (authEnabled) {
|
||||
if (isAuthenticated && data.user) {
|
||||
// Authenticated: show main content, hide login page
|
||||
currentUser = data.user;
|
||||
showAuthenticatedUI();
|
||||
} else {
|
||||
// Not authenticated
|
||||
if (skipLoginPageFlag) {
|
||||
// Skip login page: redirect directly to OIDC provider
|
||||
window.location.href = '/auth/login';
|
||||
return { enabled: authEnabled, authenticated: false, user: null };
|
||||
} else {
|
||||
// Show login page, hide main content
|
||||
showLoginPage();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// OIDC not enabled, show main content
|
||||
// OIDC not enabled: show main content, hide login page
|
||||
showMainContent();
|
||||
}
|
||||
|
||||
return { enabled: authEnabled, authenticated: isAuthenticated, user: currentUser };
|
||||
} catch (error) {
|
||||
console.error('Error checking auth status:', error);
|
||||
// If auth check fails and we're on a protected route, show login
|
||||
if (authEnabled) {
|
||||
// On error, check OIDC status from data attributes
|
||||
const oidcEnabled = document.body.getAttribute('data-oidc-enabled') === 'true';
|
||||
const skipLoginPage = document.body.getAttribute('data-skip-login-page') === 'true';
|
||||
|
||||
if (oidcEnabled) {
|
||||
if (skipLoginPage) {
|
||||
window.location.href = '/auth/login';
|
||||
} else {
|
||||
showLoginPage();
|
||||
}
|
||||
} else {
|
||||
showMainContent();
|
||||
}
|
||||
@@ -126,8 +158,9 @@ function showLoginPage() {
|
||||
const loginPage = document.getElementById('loginPage');
|
||||
const mainContent = document.getElementById('mainContent');
|
||||
const nav = document.querySelector('nav');
|
||||
const footer = document.getElementById('footer');
|
||||
|
||||
// Hide main content and nav immediately
|
||||
// Hide main content, nav, and footer
|
||||
if (mainContent) {
|
||||
mainContent.style.display = 'none';
|
||||
mainContent.classList.add('hidden');
|
||||
@@ -136,6 +169,10 @@ function showLoginPage() {
|
||||
nav.style.display = 'none';
|
||||
nav.classList.add('hidden');
|
||||
}
|
||||
if (footer) {
|
||||
footer.style.display = 'none';
|
||||
footer.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Show login page
|
||||
if (loginPage) {
|
||||
@@ -149,22 +186,27 @@ function showMainContent() {
|
||||
const loginPage = document.getElementById('loginPage');
|
||||
const mainContent = document.getElementById('mainContent');
|
||||
const nav = document.querySelector('nav');
|
||||
const footer = document.getElementById('footer');
|
||||
|
||||
// Hide login page immediately
|
||||
// Hide login page
|
||||
if (loginPage) {
|
||||
loginPage.style.display = 'none';
|
||||
loginPage.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Show main content and nav
|
||||
// Show main content, nav, and footer
|
||||
if (mainContent) {
|
||||
mainContent.style.display = '';
|
||||
mainContent.style.display = 'block';
|
||||
mainContent.classList.remove('hidden');
|
||||
}
|
||||
if (nav) {
|
||||
nav.style.display = '';
|
||||
nav.style.display = 'block';
|
||||
nav.classList.remove('hidden');
|
||||
}
|
||||
if (footer) {
|
||||
footer.style.display = 'block';
|
||||
footer.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle user menu dropdown
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<link rel="stylesheet" href="/static/vendor/fonts/google-fonts.css?v={{.version}}">
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-50 overflow-y-scroll">
|
||||
<body class="bg-gray-50 overflow-y-scroll" data-skip-login-page="{{if .skipLoginPage}}true{{else}}false{{end}}" data-oidc-enabled="{{if .oidcEnabled}}true{{else}}false{{end}}">
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<div id="loading-overlay" class="fixed inset-0 flex items-center justify-center z-50 bg-black bg-opacity-50 backdrop-blur-sm">
|
||||
@@ -129,8 +129,8 @@
|
||||
</nav>
|
||||
<!-- ************************ Navigation END *************************** -->
|
||||
|
||||
<!-- Login Page (shown when not authenticated) -->
|
||||
<div id="loginPage" class="min-h-screen flex items-center justify-center bg-gray-100 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<!-- Login Page (hidden by default, shown only when OIDC enabled and not authenticated) -->
|
||||
<div id="loginPage" class="hidden min-h-screen flex items-center justify-center bg-gray-100 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full">
|
||||
<!-- Login Card -->
|
||||
<div class="bg-white rounded-lg shadow-lg p-8 border border-gray-200">
|
||||
@@ -929,7 +929,7 @@
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-gray-100 py-4">
|
||||
<footer id="footer" class="hidden bg-gray-100 py-4">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center text-gray-600 text-sm">
|
||||
<p class="mb-0">
|
||||
© <a href="https://swissmakers.ch" target="_blank" class="text-blue-600 hover:text-blue-800">Swissmakers GmbH</a>
|
||||
|
||||
Reference in New Issue
Block a user