Guides
Protect a route
A session token, one middleware call, and role checks. This is the pattern to copy into a new project.
Resolve the session
auth.getSession takes the raw token and returns the user, or null when it is missing or expired. Tokens arrive either in a cookie or as a bearer header.
import type { AuthSession } from "yatta/auth";import { auth } from "./auth"; /** Reads the session from the Authorization header, or the cookie. */export async function getSession(req: Request): Promise<AuthSession | null> { const bearer = req.headers.get("Authorization")?.replace(/^Bearer\s+/i, ""); const cookie = readCookie(req.headers.get("Cookie"), "session"); return auth.getSession(bearer ?? cookie);} function readCookie(header: string | null, name: string): string | null { if (!header) return null; for (const part of header.split(";")) { const [k, ...rest] = part.trim().split("="); if (k === name) return rest.join("="); } return null;}Require auth on one route
import { API, createAPI } from "yatta/api";import { getSession } from "../func/session"; const api = createAPI(); api.get(async (ctx) => { const session = await getSession(ctx.req); if (!session) { return API.json({ error: "Unauthorized" }, { status: 401 }); } return API.json({ user: session.user });}); export default api;Require auth everywhere
Middleware runs before every handler in the same file. Return early to short-circuit.
import { API, createAPI } from "yatta/api";import { getSession } from "../../func/session"; const api = createAPI(); // Applies to every route in this file.api.use(async (ctx, next) => { const session = await getSession(ctx.req); if (!session) { // Send browsers to a login page; APIs get a 401. if (ctx.req.headers.get("accept")?.includes("text/html")) { return Response.redirect("/login", 302); } return API.json({ error: "Unauthorized" }, { status: 401 }); } // Shared with handlers below. ctx.state.session = session; return next();}); api.get(async (ctx) => { return API.json({ user: ctx.state.session.user });}); export default api;Note
ctx.state is a plain bag for middleware to pass values downstream. It is typed loosely, so give ctx.state.session a known shape at the top of the file.Check roles
Users carry a roles array. The permission helper in Authentication checks a role or a full permission string.
const { user } = ctx.state.session; // simple role checkif (!user.roles.includes("admin")) { return API.json({ error: "Forbidden" }, { status: 403 });} // permission check, if you configured rolesconst allowed = auth.permissions.check(user.roles, "delete", "posts"); // ownership: only the author may edit their own postif (post.authorId !== user.id && !user.roles.includes("admin")) { return API.json({ error: "Forbidden" }, { status: 403 });}Put the check in middleware when every route needs the same role:
api.use(async (ctx, next) => { const session = await getSession(ctx.req); if (!session) return API.json({ error: "Unauthorized" }, { status: 401 }); if (!session.user.roles.includes("admin")) { return API.json({ error: "Forbidden" }, { status: 403 }); } ctx.state.user = session.user; return next();});Setting the session cookie
Login and OAuth both return cookies and a toResponse helper that sets them for you.
import { API, createAPI } from "yatta/api";import { auth } from "../func/auth"; const api = createAPI(); api.post(async (ctx) => { const { email, password } = await ctx.json(); const result = await auth.signIn({ email, password, req: ctx.req }); // Sets the HttpOnly session cookie and returns the user. return result.toResponse();}); export default api;CSRF
The session cookie is HttpOnly, so JavaScript cannot read it. For cookie-authenticated state-changing requests, check the Origin header as well.
const SAFE = new Set(["https://yourdomain.com", "http://localhost:4000"]); api.post(async (ctx) => { const origin = ctx.req.headers.get("Origin"); if (origin && !SAFE.has(origin)) { return API.json({ error: "Forbidden origin" }, { status: 403 }); } // …});Note
SameSite=Lax (the default on issued cookies) already blocks cross-site POSTs from other origins. The Origin check is cheap insurance, not a replacement.