Updated 8 August 2026
Own Auth with AdonisJS
Add authentication to AdonisJS with controllers, VineJS validation, encrypted HttpOnly session cookies, and route middleware. Own Auth verifies credentials and stores users and sessions in Postgres while AdonisJS owns the HTTP boundary.
Install Own Auth
npm install own-authApply the database schema
DATABASE_URL=postgres://user:password@localhost:5432/myappnpx own-auth migrateOwn Auth creates own_auth_ prefixed tables. Keep those tables out of Lucid models and AdonisJS migrations.
Create the auth instance
Add a stable token pepper to .env and the AdonisJS environment schema, then create the shared Own Auth instance.
OWN_AUTH_TOKEN_PEPPER=replace-with-a-long-random-secretimport { Env } from "@adonisjs/core/env";
export default await Env.create(new URL("../", import.meta.url), {
DATABASE_URL: Env.schema.string(),
OWN_AUTH_TOKEN_PEPPER: Env.schema.string(),
});import { createOwnAuth, type User } from "own-auth";
import env from "#start/env";
const auth = createOwnAuth({
tokenPepper: env.get("OWN_AUTH_TOKEN_PEPPER"),
});
export function toPublicUser({ id, email, name, imageUrl }: User) {
return { id, email, name, imageUrl };
}
export default auth;toPublicUser limits AdonisJS responses to id, email, name, and imageUrl instead of serializing the stored user record.
Validate credentials
VineJS checks the HTTP payload shape. Own Auth keeps control of password policy, duplicate-account handling, credential checks, and auth rate limits.
import vine from "@vinejs/vine";
export const signUpValidator = vine.create({
name: vine.string().trim().minLength(1).optional(),
email: vine.string().trim().email(),
password: vine.string().minLength(1),
});
export const signInValidator = vine.create({
email: vine.string().trim().email(),
password: vine.string().minLength(1),
});Store the session cookie
import type { HttpContext } from "@adonisjs/core/http";
import app from "@adonisjs/core/services/app";
import type { SessionResult } from "own-auth";
export const sessionCookieName = "own_auth_session";
const cookieOptions = {
httpOnly: true,
path: "/",
sameSite: "lax" as const,
secure: app.inProduction,
};
export function setSessionCookie(
response: HttpContext["response"],
result: SessionResult,
) {
response.encryptedCookie(sessionCookieName, result.sessionToken, {
...cookieOptions,
maxAge: Math.max(0, result.session.expiresAt.getTime() - Date.now()),
});
}
export function clearSessionCookie(response: HttpContext["response"]) {
response.encryptedCookie(sessionCookieName, "", {
...cookieOptions,
maxAge: 0,
});
}Add the auth controller
Set the session cookie only after a complete sign-in. An MFA result returns its challenge token, methods, and expiry as a successful first-factor response.
import type { HttpContext } from "@adonisjs/core/http";
import auth, { toPublicUser } from "#services/own_auth";
import {
clearSessionCookie,
sessionCookieName,
setSessionCookie,
} from "#services/own_auth_cookie";
import { signInValidator, signUpValidator } from "#validators/auth";
export default class AuthController {
async signUp({ request, response }: HttpContext) {
const payload = await request.validateUsing(signUpValidator);
const result = await auth.signUpEmailPassword(payload);
setSessionCookie(response, result);
const user = toPublicUser(result.user);
return response.status(201).send({ user });
}
async signIn({ request, response }: HttpContext) {
const payload = await request.validateUsing(signInValidator);
const result = await auth.signInEmailPassword(payload);
if (result.status === "mfa_required") {
return response.send({
status: result.status,
challengeToken: result.challengeToken,
methods: result.methods,
expiresAt: result.expiresAt,
});
}
setSessionCookie(response, result);
const user = toPublicUser(result.user);
return response.send({ user });
}
async current({ request, response }: HttpContext) {
const token = request.encryptedCookie(sessionCookieName);
const current = token ? await auth.getCurrentSession(token) : null;
if (!current) return response.unauthorized({ error: "Unauthorized" });
const user = toPublicUser(current.user);
return response.send({ user });
}
async signOut({ request, response }: HttpContext) {
const token = request.encryptedCookie(sessionCookieName);
if (token) await auth.signOut(token);
clearSessionCookie(response);
return response.noContent();
}
}auth.signUpEmailPassword creates the Own Auth user and password account together. Do not write a second password record through Lucid.
Register the routes
import router from "@adonisjs/core/services/router";
import { controllers } from "#generated/controllers";
router
.group(() => {
router.post("/signup", [controllers.Auth, "signUp"]);
router.post("/signin", [controllers.Auth, "signIn"]);
router.get("/session", [controllers.Auth, "current"]);
router.post("/signout", [controllers.Auth, "signOut"]);
})
.prefix("/auth");Protect routes with middleware
Verify the cookie with auth.getCurrentSession, then bind the result to the current request.
import type { HttpContext } from "@adonisjs/core/http";
import type { NextFn } from "@adonisjs/core/types/http";
import auth from "#services/own_auth";
import { sessionCookieName } from "#services/own_auth_cookie";
export default class OwnAuthMiddleware {
async handle(ctx: HttpContext, next: NextFn) {
const token = ctx.request.encryptedCookie(sessionCookieName);
const current = token ? await auth.getCurrentSession(token) : null;
if (!current) {
return ctx.response.unauthorized({ error: "Unauthorized" });
}
ctx.containerResolver.bindValue("ownAuth", current);
return next();
}
}import type { CurrentSession } from "own-auth";
declare module "@adonisjs/core/types" {
interface ContainerBindings {
ownAuth: CurrentSession;
}
}import router from "@adonisjs/core/services/router";
export const middleware = router.named({
ownAuth: () => import("#middleware/own_auth_middleware"),
});import router from "@adonisjs/core/services/router";
import { toPublicUser } from "#services/own_auth";
import { middleware } from "#start/kernel";
router
.get("/account", async ({ containerResolver }) => {
const current = await containerResolver.make("ownAuth");
const user = toPublicUser(current.user);
return { user };
})
.use(middleware.ownAuth());Use current.user.id for route-specific ownership and permission checks.
Return safe auth errors
Map expected AuthError values in the global exception handler. Unknown errors continue through the normal AdonisJS error path.
import { ExceptionHandler, type HttpContext } from "@adonisjs/core/http";
import { AuthError } from "own-auth";
export default class HttpExceptionHandler extends ExceptionHandler {
async handle(error: unknown, ctx: HttpContext) {
if (error instanceof AuthError) {
return ctx.response.status(error.statusCode).send({
error: {
code: error.code,
message: error.safeMessage,
},
});
}
return super.handle(error, ctx);
}
async report(error: unknown, ctx: HttpContext) {
if (error instanceof AuthError) return;
return super.report(error, ctx);
}
}Add magic links
Magic-link generation needs the application origin. Add APP_URL to the environment schema and .env, then replace the auth initialization in app/services/own_auth.ts. Keep this origin explicit instead of deriving it from request.completeUrl() or the inbound Host header. Request values may include a development port and depend on proxy configuration; a fixed origin keeps emailed links stable and prevents caller-controlled hosts from entering authentication URLs.
import { Env } from "@adonisjs/core/env";
export default await Env.create(new URL("../", import.meta.url), {
DATABASE_URL: Env.schema.string(),
OWN_AUTH_TOKEN_PEPPER: Env.schema.string(),
APP_URL: Env.schema.string({ format: "url" }),
});APP_URL=http://localhost:3333const auth = createOwnAuth({
tokenPepper: env.get("OWN_AUTH_TOKEN_PEPPER"),
baseUrl: env.get("APP_URL"),
});After configuring an email provider, request and verify magic links through the same auth instance. Complete verification sets the session cookie; MFA returns its challenge payload.
import type { HttpContext } from "@adonisjs/core/http";
import auth, { toPublicUser } from "#services/own_auth";
import { setSessionCookie } from "#services/own_auth_cookie";
export function requestMagicLink(email: string) {
return auth.requestMagicLink({ email });
}
export async function verifyMagicLink(
token: string,
response: HttpContext["response"],
) {
const result = await auth.verifyMagicLink({ token });
if (result.status === "mfa_required") {
return {
status: result.status,
challengeToken: result.challengeToken,
methods: result.methods,
expiresAt: result.expiresAt,
};
}
setSessionCookie(response, result);
const user = toPublicUser(result.user);
return {
status: result.status,
user,
};
}