Authentication
The gallery uses Lucia for authentication with multiple providers.
Supported Providers
| Provider | Best For | Scopes |
|---|---|---|
GitHub |
Theme developers |
|
GitLab |
GitLab-hosted themes |
|
General users |
|
|
Email/Password |
Privacy-focused users |
N/A |
OAuth Flow
sequenceDiagram
participant User
participant App
participant OAuth as OAuth Provider
participant DB as Database
User->>App: Click "Login with GitHub"
App->>OAuth: Redirect with state
OAuth->>User: "Authorize this app?"
User->>OAuth: Approve
OAuth->>App: Callback with code
App->>OAuth: Exchange code for token
App->>OAuth: Fetch user profile
App->>DB: Find/create user
App->>DB: Create session
App->>User: Set cookie, redirect
API Endpoints
Session Security
Sessions are stored in the database with:
-
Random UUID as session ID
-
User ID reference
-
Expiration timestamp
Cookies are:
-
HttpOnly- No JavaScript access -
Secure- HTTPS only (in production) -
SameSite: Lax- CSRF protection
Account Linking
When a user authenticates via OAuth:
-
Check for existing OAuth account with that provider ID
-
If found → log in as that user
-
If not → check for user with same email
-
If email match → link OAuth account to existing user
-
If no match → create new user + OAuth account
This allows:
-
Multiple OAuth providers per account
-
Recovery if user forgets which provider they used
-
Gradual migration between providers
Password Hashing
Email/password accounts use Argon2id:
{
memoryCost: 19456, // 19 MiB
timeCost: 2, // 2 iterations
outputLen: 32, // 32 bytes
parallelism: 1
}
These settings follow OWASP recommendations.
Protecting Routes
In Astro Pages
---
import { getSession } from '../lib/auth';
const { user } = await getSession(Astro.request);
if (!user) {
return Astro.redirect('/login');
}
---
<h1>Welcome, {user.name}</h1>
In API Routes
import type { APIContext } from 'astro';
import { getSession } from '../lib/auth';
export async function POST(context: APIContext) {
const { user } = await getSession(context.request);
if (!user) {
return new Response('Unauthorized', { status: 401 });
}
// Handle authenticated request
}