Implement additional env variable to hide loginpage if desired

This commit is contained in:
2026-01-20 19:03:47 +01:00
parent de92a640e2
commit 9dd7c9bc52
9 changed files with 163 additions and 15 deletions

View File

@@ -229,6 +229,9 @@ services:
- OIDC_SESSION_MAX_AGE=7200 - OIDC_SESSION_MAX_AGE=7200
- OIDC_USERNAME_CLAIM=preferred_username - OIDC_USERNAME_CLAIM=preferred_username
- OIDC_SKIP_VERIFY=true - 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 # Optional: Logout URL
#- OIDC_LOGOUT_URL=${KEYCLOAK_URL}/realms/master/protocol/openid-connect/logout #- OIDC_LOGOUT_URL=${KEYCLOAK_URL}/realms/master/protocol/openid-connect/logout

View File

@@ -100,6 +100,9 @@ services:
# Authentik: https://authentik.example.com/application/o/your-client-slug/protocol/openid-connect/logout # 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 # 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 # - 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: volumes:
# Required for fail2ban-ui: Stores SQLite database, application settings, and SSH keys of the fail2ban-ui container # Required for fail2ban-ui: Stores SQLite database, application settings, and SSH keys of the fail2ban-ui container

View File

@@ -81,6 +81,9 @@ services:
# Authentik: https://authentik.example.com/application/o/your-client-slug/protocol/openid-connect/logout # 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 # 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 # - 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: volumes:
# Required for fail2ban-ui: Stores SQLite database, application settings, and SSH keys of the fail2ban-ui container # Required for fail2ban-ui: Stores SQLite database, application settings, and SSH keys of the fail2ban-ui container

View File

@@ -141,6 +141,14 @@ func IsEnabled() bool {
return oidcClient != nil && oidcClient.Config != nil && oidcClient.Config.Enabled 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 // GetAuthURL generates the authorization URL for OIDC login
func (c *OIDCClient) GetAuthURL(state string) string { func (c *OIDCClient) GetAuthURL(state string) string {
return c.OAuth2Config.AuthCodeURL(state, oauth2.AccessTypeOffline) return c.OAuth2Config.AuthCodeURL(state, oauth2.AccessTypeOffline)

View File

@@ -101,6 +101,7 @@ type OIDCConfig struct {
SkipVerify bool `json:"skipVerify"` // Skip TLS verification (dev only) SkipVerify bool `json:"skipVerify"` // Skip TLS verification (dev only)
UsernameClaim string `json:"usernameClaim"` // Claim to use as username UsernameClaim string `json:"usernameClaim"` // Claim to use as username
LogoutURL string `json:"logoutURL"` // Provider logout URL (optional) 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 { 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") config.SessionSecret = os.Getenv("OIDC_SESSION_SECRET")
if config.SessionSecret == "" { if config.SessionSecret == "" {
// Generate a random session secret // Generate a random session secret

View File

@@ -1126,10 +1126,22 @@ func renderIndexPage(c *gin.Context) {
// Default is enabled (false means enabled, true means disabled) // Default is enabled (false means enabled, true means disabled)
disableExternalIP := os.Getenv("DISABLE_EXTERNAL_IP_LOOKUP") == "true" || os.Getenv("DISABLE_EXTERNAL_IP_LOOKUP") == "1" 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{ c.HTML(http.StatusOK, "index.html", gin.H{
"timestamp": time.Now().Format(time.RFC1123), "timestamp": time.Now().Format(time.RFC1123),
"version": time.Now().Unix(), "version": time.Now().Unix(),
"disableExternalIP": disableExternalIP, "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 // LoginHandler shows the login page or initiates the OIDC login flow
// If action=redirect query parameter is present, redirects to OIDC provider // 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 // Otherwise, renders the login page
func LoginHandler(c *gin.Context) { func LoginHandler(c *gin.Context) {
oidcClient := auth.GetOIDCClient() oidcClient := auth.GetOIDCClient()
@@ -3026,6 +3039,40 @@ func LoginHandler(c *gin.Context) {
return 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) // Check if this is a redirect action (triggered by clicking the login button)
if c.Query("action") == "redirect" { if c.Query("action") == "redirect" {
// Generate state parameter for CSRF protection // Generate state parameter for CSRF protection
@@ -3205,11 +3252,18 @@ func AuthStatusHandler(c *gin.Context) {
return return
} }
oidcConfig := auth.GetConfig()
skipLoginPage := false
if oidcConfig != nil {
skipLoginPage = oidcConfig.SkipLoginPage
}
session, err := auth.GetSession(c.Request) session, err := auth.GetSession(c.Request)
if err != nil { if err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"enabled": true, "enabled": true,
"authenticated": false, "authenticated": false,
"skipLoginPage": skipLoginPage,
}) })
return return
} }
@@ -3217,6 +3271,7 @@ func AuthStatusHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"enabled": true, "enabled": true,
"authenticated": true, "authenticated": true,
"skipLoginPage": skipLoginPage,
"user": gin.H{ "user": gin.H{
"id": session.UserID, "id": session.UserID,
"email": session.Email, "email": session.Email,

View File

@@ -33,6 +33,35 @@
z-index: 1; 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 */ /* Ensure login page is visible when shown */
body:has(#loginPage:not(.hidden)) { body:has(#loginPage:not(.hidden)) {
background-color: #f3f4f6; background-color: #f3f4f6;

View File

@@ -7,15 +7,30 @@ let currentUser = null;
// Check authentication status on page load // Check authentication status on page load
async function checkAuthStatus() { 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 mainContent = document.getElementById('mainContent');
const nav = document.querySelector('nav'); 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) { if (mainContent) {
mainContent.classList.add('hidden');
mainContent.style.display = 'none'; mainContent.style.display = 'none';
} }
if (nav) { if (nav) {
nav.classList.add('hidden');
nav.style.display = 'none'; nav.style.display = 'none';
} }
if (footer) {
footer.classList.add('hidden');
footer.style.display = 'none';
}
try { try {
const response = await fetch('/auth/status', { const response = await fetch('/auth/status', {
@@ -29,25 +44,42 @@ async function checkAuthStatus() {
const data = await response.json(); const data = await response.json();
authEnabled = data.enabled || false; authEnabled = data.enabled || false;
isAuthenticated = data.authenticated || false; isAuthenticated = data.authenticated || false;
const skipLoginPageFlag = data.skipLoginPage || false;
if (authEnabled) { if (authEnabled) {
if (isAuthenticated && data.user) { if (isAuthenticated && data.user) {
// Authenticated: show main content, hide login page
currentUser = data.user; currentUser = data.user;
showAuthenticatedUI(); showAuthenticatedUI();
} else { } 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(); showLoginPage();
} }
}
} else { } else {
// OIDC not enabled, show main content // OIDC not enabled: show main content, hide login page
showMainContent(); showMainContent();
} }
return { enabled: authEnabled, authenticated: isAuthenticated, user: currentUser }; return { enabled: authEnabled, authenticated: isAuthenticated, user: currentUser };
} catch (error) { } catch (error) {
console.error('Error checking auth status:', error); console.error('Error checking auth status:', error);
// If auth check fails and we're on a protected route, show login // On error, check OIDC status from data attributes
if (authEnabled) { 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(); showLoginPage();
}
} else { } else {
showMainContent(); showMainContent();
} }
@@ -126,8 +158,9 @@ function showLoginPage() {
const loginPage = document.getElementById('loginPage'); const loginPage = document.getElementById('loginPage');
const mainContent = document.getElementById('mainContent'); const mainContent = document.getElementById('mainContent');
const nav = document.querySelector('nav'); const nav = document.querySelector('nav');
const footer = document.getElementById('footer');
// Hide main content and nav immediately // Hide main content, nav, and footer
if (mainContent) { if (mainContent) {
mainContent.style.display = 'none'; mainContent.style.display = 'none';
mainContent.classList.add('hidden'); mainContent.classList.add('hidden');
@@ -136,6 +169,10 @@ function showLoginPage() {
nav.style.display = 'none'; nav.style.display = 'none';
nav.classList.add('hidden'); nav.classList.add('hidden');
} }
if (footer) {
footer.style.display = 'none';
footer.classList.add('hidden');
}
// Show login page // Show login page
if (loginPage) { if (loginPage) {
@@ -149,22 +186,27 @@ function showMainContent() {
const loginPage = document.getElementById('loginPage'); const loginPage = document.getElementById('loginPage');
const mainContent = document.getElementById('mainContent'); const mainContent = document.getElementById('mainContent');
const nav = document.querySelector('nav'); const nav = document.querySelector('nav');
const footer = document.getElementById('footer');
// Hide login page immediately // Hide login page
if (loginPage) { if (loginPage) {
loginPage.style.display = 'none'; loginPage.style.display = 'none';
loginPage.classList.add('hidden'); loginPage.classList.add('hidden');
} }
// Show main content and nav // Show main content, nav, and footer
if (mainContent) { if (mainContent) {
mainContent.style.display = ''; mainContent.style.display = 'block';
mainContent.classList.remove('hidden'); mainContent.classList.remove('hidden');
} }
if (nav) { if (nav) {
nav.style.display = ''; nav.style.display = 'block';
nav.classList.remove('hidden'); nav.classList.remove('hidden');
} }
if (footer) {
footer.style.display = 'block';
footer.classList.remove('hidden');
}
} }
// Toggle user menu dropdown // Toggle user menu dropdown

View File

@@ -40,7 +40,7 @@
<link rel="stylesheet" href="/static/vendor/fonts/google-fonts.css?v={{.version}}"> <link rel="stylesheet" href="/static/vendor/fonts/google-fonts.css?v={{.version}}">
</head> </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 --> <!-- 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"> <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> </nav>
<!-- ************************ Navigation END *************************** --> <!-- ************************ Navigation END *************************** -->
<!-- Login Page (shown when not authenticated) --> <!-- Login Page (hidden by default, shown only when OIDC enabled and 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"> <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"> <div class="max-w-md w-full">
<!-- Login Card --> <!-- Login Card -->
<div class="bg-white rounded-lg shadow-lg p-8 border border-gray-200"> <div class="bg-white rounded-lg shadow-lg p-8 border border-gray-200">
@@ -929,7 +929,7 @@
</main> </main>
<!-- Footer --> <!-- 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"> <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"> <p class="mb-0">
&copy; <a href="https://swissmakers.ch" target="_blank" class="text-blue-600 hover:text-blue-800">Swissmakers GmbH</a> &copy; <a href="https://swissmakers.ch" target="_blank" class="text-blue-600 hover:text-blue-800">Swissmakers GmbH</a>