Authentication

The gallery uses Lucia for authentication with multiple providers.

Supported Providers

Provider Best For Scopes

GitHub

Theme developers

user:email

GitLab

GitLab-hosted themes

read_user

Google

General users

openid email profile

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

Initiate Login

  • GET /api/auth/login/github

  • GET /api/auth/login/gitlab

  • GET /api/auth/login/google

OAuth Callbacks

  • GET /api/auth/callback/github

  • GET /api/auth/callback/gitlab

  • GET /api/auth/callback/google

Email/Password

  • POST /api/auth/login - Login

  • POST /api/auth/register - Register

Session

  • GET /api/auth/logout - End session

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:

  1. Check for existing OAuth account with that provider ID

  2. If found → log in as that user

  3. If not → check for user with same email

  4. If email match → link OAuth account to existing user

  5. 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
}