Authorization in Astro: Complete Guide to Lucia v3 and Astro 5
- Ctrl Man
- Web Development , Security
- 14 Mar, 2026
Authorization in Astro: Complete Guide to Lucia v3
Authentication and authorization are critical for any modern web application. In this comprehensive guide, we’ll explore how to implement robust authorization in Astro using Lucia v3, the latest version of this popular authentication library.
What’s New in Lucia v3
Lucia v3 brings significant improvements over v2:
- Simplified API with better TypeScript support
- Improved session management
- Native support for more databases
- Better OAuth integration
- Enhanced security defaults
Setting Up Lucia v3 with Astro 5
Prerequisites
# Create a new Astro project (if needed)
npm create astro@latest my-auth-app
cd my-auth-app
# Install Lucia and required packages
npm install lucia @lucia-auth/adapter-sqlite better-sqlite3
# OR for MongoDB:
npm install lucia @lucia-auth/adapter-mongodb mongodb
Project Structure
src/
├── lib/
│ └── auth/
│ ├── index.ts # Main auth configuration
│ └── db.ts # Database setup
├── pages/
│ ├── api/
│ │ ├── login.ts
│ │ ├── logout.ts
│ │ └── register.ts
│ ├── login.astro
│ ├── dashboard.astro
│ └── index.astro
├── middleware.ts # Astro 5 middleware
└── components/
└── AuthButton.astro
Database Configuration
SQLite Setup (Recommended for Beginners)
// src/lib/auth/db.ts
import sqlite from "better-sqlite3";
const db = sqlite("auth.db");
// Initialize tables
db.exec(`
CREATE TABLE IF NOT EXISTS user (
id TEXT NOT NULL PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS session (
id TEXT NOT NULL PRIMARY KEY,
user_id TEXT NOT NULL,
expires_at INTEGER NOT NULL,
FOREIGN KEY (user_id) REFERENCES user(id)
);
`);
export { db };
MongoDB Setup (For Production)
// src/lib/auth/db.ts
import { MongoClient, Collection } from "mongodb";
const client = new MongoClient(process.env.MONGODB_URI || "mongodb://localhost:27017");
await client.connect();
const db = client.db("myapp");
// Collections with proper typing
export const users = db.collection<{
_id: string;
username: string;
passwordHash: string;
}> ("users");
export const sessions = db.collection<{
_id: string;
userId: string;
expiresAt: number;
}>("sessions");
export { client };
Lucia v3 Authentication Setup
Main Auth Configuration
// src/lib/auth/index.ts
import { lucia } from "lucia";
import { astro } from "lucia/middleware";
import { betterSqlite3 } from "@lucia-auth/adapter-sqlite";
import { db } from "./db";
import { dev } from "app";
// For Astro 5 with modern middleware
const adapter = betterSqlite3(db);
export const auth = lucia({
adapter,
env: dev ? "DEV" : "PROD",
middleware: astro(),
// Generate secure session tokens
sessionCookie: {
attributes: {
secure: !dev,
sameSite: "lax",
},
},
// Get user attributes from database
getUserAttributes: (databaseUser) => {
return {
username: databaseUser.username,
};
},
});
// Type helper for Astro locals
declare module "astro" {
interface Locals {
auth: import("lucia").Auth;
}
}
export type Auth = typeof auth;
Password Hashing Utility
// src/lib/auth/password.ts
import { hash, verify } from "@node-rs/argon2";
// Hash a password
export async function hashPassword(password: string): Promise<string> {
return await hash(password, {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1,
});
}
// Verify a password
export async function verifyPassword(
hash: string,
password: string
): Promise<boolean> {
try {
return await verify(hash, password);
} catch {
return false;
}
}
Astro 5 Middleware Integration
Astro 5 introduces a new middleware system that integrates seamlessly with Lucia:
// src/middleware.ts
import { defineMiddleware } from "astro/middleware";
import { auth } from "./lib/auth";
export const onRequest = defineMiddleware(async (context, next) => {
// Validate session on every request
const session = await auth.validateRequest(
context.request,
// Optional: specify which cookies to check
{
sessionCookieName: "auth_session",
}
);
// Make auth available in locals
context.locals.auth = auth;
context.locals.user = session?.user ?? null;
context.locals.session = session ?? null;
return next();
});
Registration and Login Implementation
Registration API
// src/pages/api/register.ts
import type { APIRoute } from "astro";
import { auth } from "../../lib/auth";
import { db } from "../../lib/auth/db";
import { hashPassword } from "../../lib/auth/password";
import { generateId } from "lucia";
export const POST: APIRoute = async ({ request, redirect }) => {
const formData = await request.formData();
const username = formData.get("username") as string;
const password = formData.get("password") as string;
// Validation
if (
typeof username !== "string" ||
username.length < 3 ||
username.length > 31
) {
return new Response("Invalid username", { status: 400 });
}
if (typeof password !== "string" || password.length < 6) {
return new Response("Invalid password", { status: 400 });
}
// Check if user exists
const existingUser = db
.prepare("SELECT id FROM user WHERE username = ?")
.get(username);
if (existingUser) {
return new Response("Username already taken", { status: 400 });
}
// Create user
const userId = generateId(15);
const passwordHash = await hashPassword(password);
db.prepare(
"INSERT INTO user (id, username, password_hash) VALUES (?, ?, ?)"
).run(userId, username, passwordHash);
// Create session
const session = await auth.createSession(userId, {});
const sessionCookie = auth.createSessionCookie(session);
// Redirect with session cookie
return redirect("/dashboard", {
status: 302,
headers: {
"Set-Cookie": sessionCookie.serialize(),
},
});
};
Login API
// src/pages/api/login.ts
import type { APIRoute } from "astro";
import { auth } from "../../lib/auth";
import { db } from "../../lib/auth/db";
import { verifyPassword } from "../../lib/auth/password";
export const POST: APIRoute = async ({ request, redirect }) => {
const formData = await request.formData();
const username = formData.get("username") as string;
const password = formData.get("password") as string;
// Find user
const user = db
.prepare("SELECT * FROM user WHERE username = ?")
.get(username) as { id: string; password_hash: string } | undefined;
if (!user) {
return new Response("Invalid credentials", { status: 400 });
}
// Verify password
const validPassword = await verifyPassword(user.password_hash, password);
if (!validPassword) {
return new Response("Invalid credentials", { status: 400 });
}
// Create session
const session = await auth.createSession(user.id, {});
const sessionCookie = auth.createSessionCookie(session);
return redirect("/dashboard", {
status: 302,
headers: {
"Set-Cookie": sessionCookie.serialize(),
},
});
};
Logout API
// src/pages/api/logout.ts
import type { APIRoute } from "astro";
import { auth } from "../../lib/auth";
export const POST: APIRoute = async ({ locals, redirect }) => {
const session = locals.session;
if (!session) {
return redirect("/login");
}
// Invalidate session
await auth.invalidateSession(session.id);
// Create empty session cookie (expires immediately)
const sessionCookie = auth.createBlankSessionCookie();
return redirect("/", {
status: 302,
headers: {
"Set-Cookie": sessionCookie.serialize(),
},
});
};
Protected Routes in Astro
Using Middleware for Protection
The cleanest way to protect routes in Astro 5:
// src/middleware.ts (updated)
import { defineMiddleware } from "astro/middleware";
import { auth } from "./lib/auth";
const protectedRoutes = ["/dashboard", "/profile", "/settings"];
const publicRoutes = ["/login", "/register", "/"];
export const onRequest = defineMiddleware(async (context, next) => {
const session = await auth.validateRequest(context.request);
context.locals.auth = auth;
context.locals.user = session?.user ?? null;
context.locals.session = session;
// Check if route requires auth
const isProtectedRoute = protectedRoutes.some((route) =>
context.url.pathname.startsWith(route)
);
const isPublicRoute = publicRoutes.some((route) =>
context.url.pathname === route
);
if (isProtectedRoute && !session) {
return context.redirect("/login?redirect=" + context.url.pathname);
}
// Redirect logged-in users away from public auth pages
if (isPublicRoute && session && context.url.pathname !== "/") {
return context.redirect("/dashboard");
}
return next();
});
Protected Dashboard Page
---
// src/pages/dashboard.astro
import Layout from "../layouts/Layout.astro";
// Access user from locals (set by middleware)
const user = Astro.locals.user;
if (!user) {
return Astro.redirect("/login");
}
---
<Layout title="Dashboard">
<div class="dashboard">
<h1>Welcome back, {user.username}!</h1>
<div class="user-info">
<p>User ID: {user.userId}</p>
<p>Session expires: {new Date(Astro.locals.session?.expiresAt ?? 0).toLocaleString()}</p>
</div>
<form action="/api/logout" method="POST">
<button type="submit" class="btn-logout">Logout</button>
</form>
</div>
</Layout>
Reusable Protected Layout
---
// src/layouts/ProtectedLayout.astro
interface Props {
title: string;
}
const { title } = Astro.props;
const user = Astro.locals.user;
if (!user) {
return Astro.redirect("/login");
}
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
</head>
<body>
<nav>
<a href="/dashboard">Dashboard</a>
<a href="/profile">Profile</a>
<form action="/api/logout" method="POST" style="display: inline">
<button type="submit">Logout</button>
</form>
</nav>
<main>
<slot />
</main>
</body>
</html>
OAuth Integration
Lucia v3 makes OAuth simple with built-in support:
// OAuth example with GitHub
import { github } from "@lucia-auth/oauth/providers";
const oauth = github(auth, {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
});
// In your login page, redirect to:
export const GET: APIRoute = async ({ redirect }) => {
const [url, state] = await oauth.getAuthorizationUrl();
// Store state in cookie for verification
cookies.set("oauth_state", state, {
path: "/",
secure: !import.meta.dev,
httpOnly: true,
maxAge: 60 * 60,
});
return redirect(url.toString());
};
// Callback handler
export const GET: APIRoute = async ({ url, cookies, redirect }) => {
const storedState = cookies.get("oauth_state")?.value;
const state = url.searchParams.get("state");
const code = url.searchParams.get("code");
if (!storedState || !state || storedState !== state || !code) {
return new Response("Invalid state", { status: 400 });
}
// Validate and create session
const { createUser, getUser } = oauth;
const { user } = await validateCallback(code);
// Check if user exists or create new
const existingUser = await getUser(user.id);
if (!existingUser) {
await createUser(user.id, user.name ?? "GitHub User");
}
const session = await auth.createSession(user.id, {});
const sessionCookie = auth.createSessionCookie(session);
return redirect("/dashboard", {
headers: { "Set-Cookie": sessionCookie.serialize() },
});
};
Session Management Best Practices
Session Refresh
Automatically refresh sessions that are still active:
// In your middleware or layout
export const onRequest = defineMiddleware(async (context, next) => {
const session = context.locals.session;
if (session) {
// Refresh session if it expires within 7 days
if (
session.expiresAt.getTime() - Date.now() <
1000 * 60 * 60 * 24 * 7
) {
const newSession = await auth.renewSession(session.id);
const cookie = auth.createSessionCookie(newSession);
context.cookies.set(cookie.name, cookie.value, {
...cookie.attributes,
});
}
}
return next();
});
Remember Me Functionality
// Extended session expiry for "remember me"
export async function createPersistentSession(userId: string): Promise<Session> {
return await auth.createSession(userId, {
expiresIn: 60 * 60 * 24 * 30, // 30 days
attributes: {
persistent: true,
},
});
}
Security Checklist
- Use secure passwords (min 8 characters, hash with Argon2)
- Set secure cookie attributes (
Secure,HttpOnly,SameSite) - Implement CSRF protection for forms
- Use HTTPS in production
- Regularly rotate session keys
- Implement rate limiting on auth endpoints
- Store sessions in database (not localStorage)
- Use parameterized queries (prevents SQL injection)
Conclusion
Implementing robust authorization in Astro 5 with Lucia v3 provides a secure, type-safe foundation for your web applications. The key benefits include:
- Type Safety - Full TypeScript support with auto-completion
- Flexible Adapters - Support for SQLite, PostgreSQL, MongoDB, and more
- OAuth Built-in - Easy integration with GitHub, Google, and other providers
- Modern Middleware - Astro 5’s middleware system integrates seamlessly
- Security First - Secure defaults with room for customization
Start with the basic setup, add OAuth as needed, and scale your authentication system as your application grows.
Questions or tips? Share your experience with Astro authorization in the comments!
Related: Check out our guide on securing Astro API routes for advanced protection techniques.