Auth Flow¶
The CyberEco Hub uses a hybrid authentication model: client-side Firebase Authentication for credential verification, combined with an httpOnly cookie for server-side route protection.
Overview¶
sequenceDiagram
participant U as User (Browser)
participant R as React Island
participant FA as Firebase Auth
participant API as Hub API Server
participant MW as Astro Middleware
Note over U,MW: Step 1: User submits credentials
U->>R: Enter email + password
R->>FA: signInWithEmailAndPassword(email, password)
FA-->>R: UserCredential { user.uid }
Note over U,MW: Step 2: Set server-side cookie
R->>API: POST /api/auth/set-cookie { uid }
API-->>API: Set httpOnly cookie (cybereco-auth-token = uid)
API-->>R: { success: true }
Note over U,MW: Step 3: Navigate to protected route
R->>U: window.location.href = '/dashboard'
U->>MW: GET /dashboard (cookie attached)
MW-->>MW: Read cybereco-auth-token cookie
MW-->>U: 200 OK (render dashboard page)
Note over U,MW: Step 4: Background sync (onAuthStateChanged)
FA-->>R: onAuthStateChanged(user)
R->>API: POST /api/auth/set-cookie { uid }
API-->>R: { success: true }
Note over U,MW: Step 5: Cookie refresh (every 45 min)
R->>API: POST /api/auth/refresh
API-->>API: Reset cookie Max-Age to 3600s
API-->>R: { success: true }
Note over U,MW: Step 6: Sign out
R->>FA: signOut()
R->>API: DELETE /api/auth/refresh
API-->>API: Set cookie Max-Age to 0
API-->>R: { success: true }
R->>U: window.location.href = '/auth/signin'
Step-by-Step Walkthrough¶
Step 1: Credential Submission¶
The user enters their email and password into the SignInForm React island. The form calls Firebase Auth directly from the client:
import { getAuth, signInWithEmailAndPassword } from 'firebase/auth';
const auth = getAuth(firebaseApp);
const credential = await signInWithEmailAndPassword(auth, email, password);
Firebase Auth validates the credentials against the Firebase project and returns a UserCredential containing the user's UID, email, display name, and other profile data.
Why client-side Firebase Auth?
Firebase Auth manages password hashing, rate limiting, email verification, and OAuth flows. The Hub delegates all credential management to Firebase and only needs the resulting UID for session tracking.
Step 2: Cookie Establishment¶
After successful Firebase authentication, the form calls the Hub's POST /api/auth/set-cookie endpoint with the Firebase UID:
await fetch('/api/auth/set-cookie', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ uid: user.uid }),
});
The API route sets an httpOnly cookie:
cookies.set('cybereco-auth-token', uid, {
path: '/',
maxAge: 3600, // 1 hour
sameSite: 'lax',
httpOnly: true, // Not accessible via JavaScript (XSS protection)
secure: !isDev, // HTTPS only in production
});
Step 3: Protected Route Access¶
After the cookie is set, the form performs a full-page navigation:
Full-page navigation, not SPA routing
The Hub uses window.location.href (not React Router or Astro's navigate()) to ensure the browser sends the newly-set cookie with the request. SPA-style navigation would not trigger a new server request, and the middleware would not see the cookie.
The Astro middleware intercepts the request, reads the cybereco-auth-token cookie, and either allows access or redirects to the sign-in page:
const authToken = cookies.get('cybereco-auth-token');
if (!authToken?.value) {
const returnUrl = encodeURIComponent(pathname);
return redirect(`/auth/signin?returnUrl=${returnUrl}`, 302);
}
Step 4: Background Sync¶
The HubAuthProvider registers a Firebase onAuthStateChanged listener. When Firebase detects the user is authenticated (including on page reload), it calls POST /api/auth/set-cookie to ensure the server-side cookie stays in sync:
onAuthStateChanged(auth, async (user) => {
if (user) {
await fetch('/api/auth/set-cookie', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ uid: user.uid }),
});
}
});
This handles edge cases where the cookie expires but the Firebase Auth session is still valid (Firebase sessions persist in IndexedDB).
Step 5: Cookie Refresh¶
The cookie has a 1-hour Max-Age. To prevent session expiry during active use, the client calls POST /api/auth/refresh every 45 minutes:
// In HubAuthProvider
const REFRESH_INTERVAL = 45 * 60 * 1000; // 45 minutes
useEffect(() => {
const interval = setInterval(() => {
fetch('/api/auth/refresh', { method: 'POST' });
}, REFRESH_INTERVAL);
return () => clearInterval(interval);
}, []);
The refresh endpoint resets the cookie's Max-Age to 3600 seconds without changing its value.
Step 6: Sign Out¶
Sign-out involves two steps:
- Firebase sign-out -- clears the client-side Firebase Auth session
- Cookie clearing --
DELETE /api/auth/refreshsets the cookie'sMax-Ageto 0
const auth = getAuth(firebaseApp);
await signOut(auth);
await fetch('/api/auth/refresh', { method: 'DELETE' });
window.location.href = '/auth/signin';
Cookie Properties¶
| Property | Value | Purpose |
|---|---|---|
| Name | cybereco-auth-token |
Identifies the cookie |
| Value | Firebase UID (string) | Identifies the authenticated user |
| Max-Age | 3600 (1 hour) |
Cookie expiry; refreshed every 45 min |
| Path | / |
Available on all routes |
| SameSite | Lax |
Sent on same-site requests and top-level navigations |
| HttpOnly | true |
Not accessible via document.cookie (XSS protection) |
| Secure | true in production |
Only sent over HTTPS |
Protected Routes¶
The middleware checks the auth cookie for these route prefixes:
| Prefix | Pages |
|---|---|
/dashboard |
Main dashboard |
/apps |
Ecosystem app directory |
/my-data |
Personal data management |
/profile |
User profile |
/privacy |
Privacy settings |
/settings |
Account settings |
/billing |
Subscription and billing |
/security |
Security settings (password, 2FA, sessions, devices) |
/admin |
Admin panel |
These routes are always public (no cookie required):
| Prefix | Pages |
|---|---|
/auth |
Sign-in, sign-up, password reset |
/api |
All API routes (they check auth internally) |
/coming-soon |
Placeholder pages |
SSO Token Generation¶
For cross-app navigation (Hub to JustSplit, Somos, etc.), the Hub generates HMAC-SHA256 signed tokens via POST /api/auth/generate-token:
sequenceDiagram
participant U as User
participant Hub as Hub App
participant API as Hub API
participant App as Ecosystem App
U->>Hub: Click "Open JustSplit"
Hub->>API: POST /api/auth/generate-token { appId, appUrl }
API-->>API: Validate appUrl against allowlist
API-->>API: Sign { appId, timestamp, exp } with HMAC-SHA256
API-->>Hub: { url, token }
Hub->>U: Redirect to url (includes ?authToken=...)
U->>App: GET /dashboard?authToken=...&fromHub=true&appId=justsplit
App-->>App: Verify HMAC signature, check exp
App-->>U: 200 OK (authenticated session)
Token properties:
- Signing algorithm: HMAC-SHA256 (Web Crypto API)
- Format:
base64url(payload).base64url(signature) - Expiry: 5 minutes from generation
- Payload:
{ appId, timestamp, exp } - Secret:
SSO_TOKEN_SECRETenvironment variable
Allowed origins:
https://justsplit.cybere.cohttps://somos.cybere.cohttps://demos.cybere.cohttps://plantopia.cybere.cohttps://cybere.co- Development:
http://localhost:40000-40002
Security Considerations¶
Important Security Notes
- The
cybereco-auth-tokencookie contains the Firebase UID, not a Firebase ID token. This is a deliberate simplification for the current implementation. In production, you should consider storing a server-signed session token instead. - The httpOnly flag prevents JavaScript access, protecting against XSS-based cookie theft.
- The Secure flag (production only) ensures the cookie is never sent over plain HTTP.
- CSRF protection is handled separately by the middleware via the double-submit cookie pattern. See Middleware for details.
Related Documentation¶
- Middleware -- Route protection, CSRF, and security headers
- API Reference -- Full endpoint specifications
- Hub Overview -- Architecture and technology stack