How to use Own Auth with AdonisJS
Own Auth and AdonisJS fit together without replacing each other's responsibilities. AdonisJS handles HTTP requests, validation, cookies, middleware, and application structure. Own Auth handles users, password hashing, tokens, sessions, rate limits, and audit events in Postgres.
This guide builds email and password authentication for an AdonisJS application. The same Own Auth instance can later add magic links, email verification, phone login, organisations, and API keys.
What you will build
- Validated sign-up and sign-in endpoints
- An encrypted HttpOnly session cookie
- A current-session endpoint
- Immediate session revocation on sign-out
- Named middleware for protected routes
- Safe JSON responses for Own Auth errors
Prerequisites
- An AdonisJS application running on Node.js 18 or later
- A Postgres database
- The default AdonisJS body parser and VineJS validation setup
Install Own Auth
Install the package in the AdonisJS project and run the Own Auth migration against your application database.
npm install own-auth
npx own-auth migrate
npx own-auth statusThe migration creates only own_auth_ prefixed tables. Do not recreate those tables as Lucid models or AdonisJS migrations. Own Auth owns its auth schema and uses the same Postgres database without taking over your application tables.
Validate the environment
Add the database URL, token pepper, and public application URL to the AdonisJS environment schema. Startup should fail when a required secret is missing.
import { Env } from "@adonisjs/core/env";
export default await Env.create(new URL("../", import.meta.url), {
// Keep your existing AdonisJS variables here.
DATABASE_URL: Env.schema.string(),
OWN_AUTH_TOKEN_PEPPER: Env.schema.string(),
APP_URL: Env.schema.string({ format: "url" }),
});DATABASE_URL=postgres://user:password@localhost:5432/myapp
OWN_AUTH_TOKEN_PEPPER=replace-with-a-long-random-secret
APP_URL=http://localhost:3333Create one Own Auth instance
Export a single Own Auth instance from an AdonisJS service module. Own Auth reads DATABASE_URL automatically and uses the validated values supplied by AdonisJS.
import { createOwnAuth } from "own-auth";
import env from "#start/env";
const auth = createOwnAuth({
tokenPepper: env.get("OWN_AUTH_TOKEN_PEPPER"),
baseUrl: env.get("APP_URL"),
redirectAllowlist: [env.get("APP_URL")],
});
export default auth;A regular service module is enough here. You do not need a custom AdonisJS service provider unless your application has a specific dependency-injection requirement.
Validate credentials with VineJS
Keep HTTP input validation in AdonisJS. Own Auth still enforces its own authentication rules, but the controller should reject malformed request bodies before calling it.
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(8),
});
export const signInValidator = vine.create({
email: vine.string().trim().email(),
password: vine.string(),
});Create the session cookie helpers
Own Auth returns an opaque session token. For a server-rendered or same-origin AdonisJS application, keep that token in an encrypted HttpOnly cookie. The browser should never read it with JavaScript.
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 authentication controller
The controller validates each request, calls Own Auth, and returns only safe user and session data. It never returns the raw session token in JSON because the encrypted cookie already carries it.
import type { HttpContext } from "@adonisjs/core/http";
import auth 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);
return response.status(201).send({ user: result.user });
}
async signIn({ request, response }: HttpContext) {
const payload = await request.validateUsing(signInValidator);
const result = await auth.signInEmailPassword(payload);
setSessionCookie(response, result);
return response.send({ user: result.user });
}
async current({ request, response }: HttpContext) {
const sessionToken = request.encryptedCookie(sessionCookieName);
const current = sessionToken
? await auth.getCurrentSession(sessionToken)
: null;
if (!current) return response.unauthorized({ error: "Unauthorized" });
return response.send(current);
}
async signOut({ request, response }: HttpContext) {
const sessionToken = request.encryptedCookie(sessionCookieName);
if (sessionToken) await auth.signOut(sessionToken);
clearSessionCookie(response);
return response.noContent();
}
}Register the routes
Group the authentication endpoints under one prefix. AdonisJS lazy-loads the controller when a matching route is requested.
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
For protected endpoints, read the encrypted cookie and verify the session against Postgres. Do not trust a user ID from a form field, route parameter, or unsigned cookie.
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 sessionToken = ctx.request.encryptedCookie(sessionCookieName);
const current = sessionToken
? await auth.getCurrentSession(sessionToken)
: null;
if (!current) {
return ctx.response.unauthorized({ error: "Unauthorized" });
}
ctx.containerResolver.bindValue("ownAuth", current);
return next();
}
}Declare the request-scoped binding so controllers can resolve the verified user and session with type safety.
import type { CurrentSession } from "own-auth";
declare module "@adonisjs/core/types" {
interface ContainerBindings {
ownAuth: CurrentSession;
}
}Register the named middleware and apply it only to routes that require a signed-in user.
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 { middleware } from "#start/kernel";
router
.get("/account", async ({ containerResolver }) => {
const current = await containerResolver.make("ownAuth");
return { user: current.user, session: current.session };
})
.use(middleware.ownAuth());Return safe authentication errors
Own Auth throws AuthError for expected failures such as invalid credentials, weak passwords, duplicate email addresses, and rate limits. Convert those errors in the global AdonisJS exception handler and let unexpected errors use the normal server-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);
}
}Keep invalid_credentials deliberately vague. Never change it into separate email-not-found and wrong-password responses, because that would let attackers discover registered accounts.
Add magic links later
Once password authentication works, connect an email provider and call requestMagicLink from an AdonisJS controller. The verification controller reads the token from the request, calls verifyMagicLink, and sets the returned session with the same cookie helper.
await auth.requestMagicLink({
email: payload.email,
redirectUrl: "/dashboard",
});
const result = await auth.verifyMagicLink({ token: payload.token });
setSessionCookie(response, result);Production checklist
- Use HTTPS and Secure cookies in production
- Keep DATABASE_URL and OWN_AUTH_TOKEN_PEPPER server-only
- Run Own Auth migrations before starting the new release
- Verify the Own Auth session on every protected request
- Use CSRF protection for cookie-authenticated mutation routes
- Keep invalid-credential responses generic
- Never log passwords, session tokens, magic-link URLs, or raw request bodies containing secrets
- Let Own Auth own its prefixed tables instead of duplicating auth records in Lucid
FAQ
Does Own Auth replace AdonisJS middleware or VineJS?
No. VineJS validates the HTTP payload and AdonisJS middleware controls route access. Own Auth performs authentication and session verification after that framework-level work.
Do I need an AdonisJS User model?
Not for Own Auth itself. Own Auth stores users in its own tables and returns typed user objects. Your application may keep a separate profile table keyed by the Own Auth user ID, but it should not duplicate password or session data.
Can I use bearer tokens instead of cookies?
Yes. Read Authorization with request.header, extract the Bearer token, and call getCurrentSession. Cookies are convenient for same-origin browser applications. Bearer tokens are often easier for mobile clients and separate API consumers.
Can I use Lucid and Own Auth in the same database?
Yes. Lucid can own your application tables while Own Auth owns the own_auth_ prefixed tables. Run each migration system for the tables it owns.
Conclusion
A clean AdonisJS integration keeps framework concerns and authentication concerns separate. AdonisJS validates requests, routes controllers, encrypts cookies, and runs middleware. Own Auth hashes passwords, creates users, verifies sessions, applies auth rate limits, and keeps the auth data in your Postgres database.