Add logic to share session across configured domains

This commit is contained in:
2025-08-05 19:53:03 +10:00
parent 6dc0dd1733
commit a90a7bb452
5 changed files with 178 additions and 6 deletions
+9
View File
@@ -45,6 +45,14 @@ auth:
# Set to false to allow automatic approval for trusted environments
require_approval: true
# Cookie domain for session cookies
# Leave empty for single domain (cookies only work on current domain)
# Set to ".yourdomain.com" to share cookies across all subdomains
# Examples:
# - "" (empty) - cookies only work on the exact domain
# - ".example.com" - cookies work on example.com and all subdomains
cookie_domain: ""
# Email allowlist - list of email addresses allowed to register
# Leave empty to allow any email address (not recommended for production)
allowed_emails:
@@ -65,3 +73,4 @@ auth:
# - SESSION_SECRET: Session encryption secret
# - ALLOWED_EMAILS: Comma-separated list of allowed emails
# - ADMIN_EMAIL: Admin email address
# - COOKIE_DOMAIN: Cookie domain for session cookies
+5
View File
@@ -41,6 +41,7 @@ type AuthConfig struct {
RequireApproval bool `yaml:"require_approval"`
AllowedEmails []string `yaml:"allowed_emails"`
AdminEmail string `yaml:"admin_email"`
CookieDomain string `yaml:"cookie_domain"`
}
func Load() (*Config, error) {
@@ -75,6 +76,7 @@ func Load() (*Config, error) {
SessionSecret: "change-me-in-production",
RequireApproval: true,
AllowedEmails: []string{}, // Empty means no email restrictions
CookieDomain: "", // Empty means no domain restriction (current domain only)
},
}
@@ -117,6 +119,9 @@ func Load() (*Config, error) {
if adminEmail := os.Getenv("ADMIN_EMAIL"); adminEmail != "" {
config.Auth.AdminEmail = adminEmail
}
if cookieDomain := os.Getenv("COOKIE_DOMAIN"); cookieDomain != "" {
config.Auth.CookieDomain = cookieDomain
}
return config, nil
}
+17 -1
View File
@@ -35,6 +35,7 @@ func New(db *database.DB, webAuthn *auth.WebAuthn, config *config.Config) *Handl
HttpOnly: true,
Secure: false, // Set to true in production with HTTPS
SameSite: http.SameSiteLaxMode,
Domain: config.Auth.CookieDomain, // Share cookies across subdomains if configured
}
return &Handlers{
@@ -393,10 +394,24 @@ func (h *Handlers) Logout(w http.ResponseWriter, r *http.Request) {
// AuthCheck implements the nginx auth_request protocol
func (h *Handlers) AuthCheck(w http.ResponseWriter, r *http.Request) {
session, _ := h.store.Get(r, "auth-session")
// Debug logging
logrus.Debugf("AuthCheck request from %s", r.RemoteAddr)
logrus.Debugf("AuthCheck headers: %+v", r.Header)
logrus.Debugf("AuthCheck cookies: %+v", r.Cookies())
session, err := h.store.Get(r, "auth-session")
if err != nil {
logrus.Errorf("Failed to get auth session: %v", err)
w.WriteHeader(http.StatusUnauthorized)
return
}
authenticated, ok := session.Values["authenticated"].(bool)
logrus.Debugf("Session authenticated: %v, ok: %v", authenticated, ok)
logrus.Debugf("Session values: %+v", session.Values)
if !ok || !authenticated {
logrus.Debugf("User not authenticated, returning 401")
w.WriteHeader(http.StatusUnauthorized)
return
}
@@ -409,6 +424,7 @@ func (h *Handlers) AuthCheck(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Auth-User", userEmail)
}
logrus.Debugf("User authenticated, returning 200")
w.WriteHeader(http.StatusOK)
}
+5 -5
View File
@@ -263,13 +263,13 @@
// Prefer 'redirect' over 'rd' if both are present
const redirectParam = urlParams.get('redirect');
const rdParam = urlParams.get('rd');
console.log('URL params:', {
redirect: redirectParam,
rd: rdParam,
search: window.location.search
});
return redirectParam || rdParam;
}
@@ -281,14 +281,14 @@
// Decode the URL if it's encoded
const decodedUrl = decodeURIComponent(redirectUrl);
console.log('Decoded redirect URL:', decodedUrl);
// Validate the URL
const url = new URL(decodedUrl);
console.log('Parsed URL:', url.href);
// Show a brief message before redirecting
showAlert(`Redirecting to ${url.hostname}...`, 'success');
setTimeout(() => {
console.log('Executing redirect to:', decodedUrl);
window.location.href = decodedUrl;
+142
View File
@@ -0,0 +1,142 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Passkey Authentication</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
background: #f8f9fa;
}
.container {
text-align: center;
max-width: 400px;
padding: 2rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<div class="spinner"></div>
<h2>🔐 Passkey Authentication</h2>
<p id="status">Checking authentication status...</p>
</div>
<script>
function updateStatus(message) {
document.getElementById('status').textContent = message;
}
function getRedirectUrl() {
const urlParams = new URLSearchParams(window.location.search);
// Check for both 'redirect' and 'rd' parameters (nginx uses 'rd' by default)
// Prefer 'redirect' over 'rd' if both are present
const redirectParam = urlParams.get('redirect');
const rdParam = urlParams.get('rd');
console.log('URL params:', {
redirect: redirectParam,
rd: rdParam,
search: window.location.search
});
return redirectParam || rdParam;
}
function redirectToTarget(url) {
try {
const decodedUrl = decodeURIComponent(url);
updateStatus(`Redirecting to ${new URL(decodedUrl).hostname}...`);
console.log('Redirecting to:', decodedUrl);
// Small delay to show the message
setTimeout(() => {
window.location.href = decodedUrl;
}, 1000);
return true;
} catch (error) {
console.error('Error processing redirect URL:', error);
updateStatus('Invalid redirect URL');
return false;
}
}
function redirectToLogin() {
const redirectUrl = getRedirectUrl();
if (redirectUrl) {
// Preserve the redirect parameter when going to login
const loginUrl = `/login.html?redirect=${encodeURIComponent(redirectUrl)}`;
updateStatus('Redirecting to login...');
setTimeout(() => {
window.location.href = loginUrl;
}, 1000);
} else {
// No redirect parameter, just go to login
window.location.href = '/login.html';
}
}
async function checkAuthAndRedirect() {
const redirectUrl = getRedirectUrl();
if (!redirectUrl) {
updateStatus('No redirect URL provided');
setTimeout(() => {
window.location.href = '/login.html';
}, 2000);
return;
}
try {
updateStatus('Checking authentication...');
const response = await fetch('/api/auth/status', {
credentials: 'include'
});
if (response.ok) {
const userData = await response.json();
if (userData.authenticated) {
updateStatus(`Welcome back, ${userData.user.display_name}!`);
redirectToTarget(redirectUrl);
return;
}
}
// Not authenticated, redirect to login with the target URL
updateStatus('Authentication required...');
redirectToLogin();
} catch (error) {
console.error('Auth check failed:', error);
updateStatus('Authentication check failed...');
redirectToLogin();
}
}
// Start the process when page loads
document.addEventListener('DOMContentLoaded', checkAuthAndRedirect);
</script>
</body>
</html>