Skip to content
This repository was archived by the owner on Nov 24, 2025. It is now read-only.

Commit a90a7bb

Browse files
committed
Add logic to share session across configured domains
1 parent 6dc0dd1 commit a90a7bb

5 files changed

Lines changed: 178 additions & 6 deletions

File tree

config.example.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ auth:
4545
# Set to false to allow automatic approval for trusted environments
4646
require_approval: true
4747

48+
# Cookie domain for session cookies
49+
# Leave empty for single domain (cookies only work on current domain)
50+
# Set to ".yourdomain.com" to share cookies across all subdomains
51+
# Examples:
52+
# - "" (empty) - cookies only work on the exact domain
53+
# - ".example.com" - cookies work on example.com and all subdomains
54+
cookie_domain: ""
55+
4856
# Email allowlist - list of email addresses allowed to register
4957
# Leave empty to allow any email address (not recommended for production)
5058
allowed_emails:
@@ -65,3 +73,4 @@ auth:
6573
# - SESSION_SECRET: Session encryption secret
6674
# - ALLOWED_EMAILS: Comma-separated list of allowed emails
6775
# - ADMIN_EMAIL: Admin email address
76+
# - COOKIE_DOMAIN: Cookie domain for session cookies

internal/config/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type AuthConfig struct {
4141
RequireApproval bool `yaml:"require_approval"`
4242
AllowedEmails []string `yaml:"allowed_emails"`
4343
AdminEmail string `yaml:"admin_email"`
44+
CookieDomain string `yaml:"cookie_domain"`
4445
}
4546

4647
func Load() (*Config, error) {
@@ -75,6 +76,7 @@ func Load() (*Config, error) {
7576
SessionSecret: "change-me-in-production",
7677
RequireApproval: true,
7778
AllowedEmails: []string{}, // Empty means no email restrictions
79+
CookieDomain: "", // Empty means no domain restriction (current domain only)
7880
},
7981
}
8082

@@ -117,6 +119,9 @@ func Load() (*Config, error) {
117119
if adminEmail := os.Getenv("ADMIN_EMAIL"); adminEmail != "" {
118120
config.Auth.AdminEmail = adminEmail
119121
}
122+
if cookieDomain := os.Getenv("COOKIE_DOMAIN"); cookieDomain != "" {
123+
config.Auth.CookieDomain = cookieDomain
124+
}
120125

121126
return config, nil
122127
}

internal/handlers/handlers.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ func New(db *database.DB, webAuthn *auth.WebAuthn, config *config.Config) *Handl
3535
HttpOnly: true,
3636
Secure: false, // Set to true in production with HTTPS
3737
SameSite: http.SameSiteLaxMode,
38+
Domain: config.Auth.CookieDomain, // Share cookies across subdomains if configured
3839
}
3940

4041
return &Handlers{
@@ -393,10 +394,24 @@ func (h *Handlers) Logout(w http.ResponseWriter, r *http.Request) {
393394

394395
// AuthCheck implements the nginx auth_request protocol
395396
func (h *Handlers) AuthCheck(w http.ResponseWriter, r *http.Request) {
396-
session, _ := h.store.Get(r, "auth-session")
397+
// Debug logging
398+
logrus.Debugf("AuthCheck request from %s", r.RemoteAddr)
399+
logrus.Debugf("AuthCheck headers: %+v", r.Header)
400+
logrus.Debugf("AuthCheck cookies: %+v", r.Cookies())
401+
402+
session, err := h.store.Get(r, "auth-session")
403+
if err != nil {
404+
logrus.Errorf("Failed to get auth session: %v", err)
405+
w.WriteHeader(http.StatusUnauthorized)
406+
return
407+
}
397408

398409
authenticated, ok := session.Values["authenticated"].(bool)
410+
logrus.Debugf("Session authenticated: %v, ok: %v", authenticated, ok)
411+
logrus.Debugf("Session values: %+v", session.Values)
412+
399413
if !ok || !authenticated {
414+
logrus.Debugf("User not authenticated, returning 401")
400415
w.WriteHeader(http.StatusUnauthorized)
401416
return
402417
}
@@ -409,6 +424,7 @@ func (h *Handlers) AuthCheck(w http.ResponseWriter, r *http.Request) {
409424
w.Header().Set("X-Auth-User", userEmail)
410425
}
411426

427+
logrus.Debugf("User authenticated, returning 200")
412428
w.WriteHeader(http.StatusOK)
413429
}
414430

web/login.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -263,13 +263,13 @@ <h3>User Management</h3>
263263
// Prefer 'redirect' over 'rd' if both are present
264264
const redirectParam = urlParams.get('redirect');
265265
const rdParam = urlParams.get('rd');
266-
266+
267267
console.log('URL params:', {
268268
redirect: redirectParam,
269269
rd: rdParam,
270270
search: window.location.search
271271
});
272-
272+
273273
return redirectParam || rdParam;
274274
}
275275

@@ -281,14 +281,14 @@ <h3>User Management</h3>
281281
// Decode the URL if it's encoded
282282
const decodedUrl = decodeURIComponent(redirectUrl);
283283
console.log('Decoded redirect URL:', decodedUrl);
284-
284+
285285
// Validate the URL
286286
const url = new URL(decodedUrl);
287287
console.log('Parsed URL:', url.href);
288-
288+
289289
// Show a brief message before redirecting
290290
showAlert(`Redirecting to ${url.hostname}...`, 'success');
291-
291+
292292
setTimeout(() => {
293293
console.log('Executing redirect to:', decodedUrl);
294294
window.location.href = decodedUrl;

web/redirect.html

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Passkey Authentication</title>
7+
<style>
8+
body {
9+
font-family: system-ui, -apple-system, sans-serif;
10+
display: flex;
11+
align-items: center;
12+
justify-content: center;
13+
min-height: 100vh;
14+
margin: 0;
15+
background: #f8f9fa;
16+
}
17+
.container {
18+
text-align: center;
19+
max-width: 400px;
20+
padding: 2rem;
21+
background: white;
22+
border-radius: 8px;
23+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
24+
}
25+
.spinner {
26+
width: 40px;
27+
height: 40px;
28+
border: 4px solid #f3f3f3;
29+
border-top: 4px solid #007bff;
30+
border-radius: 50%;
31+
animation: spin 1s linear infinite;
32+
margin: 0 auto 1rem;
33+
}
34+
@keyframes spin {
35+
0% { transform: rotate(0deg); }
36+
100% { transform: rotate(360deg); }
37+
}
38+
</style>
39+
</head>
40+
<body>
41+
<div class="container">
42+
<div class="spinner"></div>
43+
<h2>🔐 Passkey Authentication</h2>
44+
<p id="status">Checking authentication status...</p>
45+
</div>
46+
47+
<script>
48+
function updateStatus(message) {
49+
document.getElementById('status').textContent = message;
50+
}
51+
52+
function getRedirectUrl() {
53+
const urlParams = new URLSearchParams(window.location.search);
54+
// Check for both 'redirect' and 'rd' parameters (nginx uses 'rd' by default)
55+
// Prefer 'redirect' over 'rd' if both are present
56+
const redirectParam = urlParams.get('redirect');
57+
const rdParam = urlParams.get('rd');
58+
59+
console.log('URL params:', {
60+
redirect: redirectParam,
61+
rd: rdParam,
62+
search: window.location.search
63+
});
64+
65+
return redirectParam || rdParam;
66+
}
67+
68+
function redirectToTarget(url) {
69+
try {
70+
const decodedUrl = decodeURIComponent(url);
71+
updateStatus(`Redirecting to ${new URL(decodedUrl).hostname}...`);
72+
console.log('Redirecting to:', decodedUrl);
73+
// Small delay to show the message
74+
setTimeout(() => {
75+
window.location.href = decodedUrl;
76+
}, 1000);
77+
return true;
78+
} catch (error) {
79+
console.error('Error processing redirect URL:', error);
80+
updateStatus('Invalid redirect URL');
81+
return false;
82+
}
83+
}
84+
85+
function redirectToLogin() {
86+
const redirectUrl = getRedirectUrl();
87+
if (redirectUrl) {
88+
// Preserve the redirect parameter when going to login
89+
const loginUrl = `/login.html?redirect=${encodeURIComponent(redirectUrl)}`;
90+
updateStatus('Redirecting to login...');
91+
setTimeout(() => {
92+
window.location.href = loginUrl;
93+
}, 1000);
94+
} else {
95+
// No redirect parameter, just go to login
96+
window.location.href = '/login.html';
97+
}
98+
}
99+
100+
async function checkAuthAndRedirect() {
101+
const redirectUrl = getRedirectUrl();
102+
103+
if (!redirectUrl) {
104+
updateStatus('No redirect URL provided');
105+
setTimeout(() => {
106+
window.location.href = '/login.html';
107+
}, 2000);
108+
return;
109+
}
110+
111+
try {
112+
updateStatus('Checking authentication...');
113+
114+
const response = await fetch('/api/auth/status', {
115+
credentials: 'include'
116+
});
117+
118+
if (response.ok) {
119+
const userData = await response.json();
120+
if (userData.authenticated) {
121+
updateStatus(`Welcome back, ${userData.user.display_name}!`);
122+
redirectToTarget(redirectUrl);
123+
return;
124+
}
125+
}
126+
127+
// Not authenticated, redirect to login with the target URL
128+
updateStatus('Authentication required...');
129+
redirectToLogin();
130+
131+
} catch (error) {
132+
console.error('Auth check failed:', error);
133+
updateStatus('Authentication check failed...');
134+
redirectToLogin();
135+
}
136+
}
137+
138+
// Start the process when page loads
139+
document.addEventListener('DOMContentLoaded', checkAuthAndRedirect);
140+
</script>
141+
</body>
142+
</html>

0 commit comments

Comments
 (0)