I’ve had several people ask me how to go about authentication within AdonisJS using Lucid using multiple models, specifically a User
and Admin
model. So, today we’ll take some time to walk through how to do exactly that, and I think you’ll find it a rather easy feat.
Starting Point
If you’d like to follow along I have a repository on GitHub called to serve as a starting point for this lesson, you can find it here, or use the checkout command below. You can also find the completed code here if that’s what you’re after.
git clone https://github.com/adocasts/lucid-multi-model-authentication.git
Copied!
If you clone down the above, be sure to checkout the 01_StartingPoint
branch to follow along.
Within this application, you’ll find an /auth/admin
and /auth/user
page. The user page will contain a login and registration form. The admin page will contain a login form, though later in this lesson we’ll add the registration form to keep things simple (knowing you likely won’t want any ol’ person to be able to register as an admin).
Installing & Configuring Authentication
First, let’s get the AdonisJS Auth package installed and configured. While we’re installing this, let's also get our hashing package installed, called phc-argon2
.
npm i @adonisjs/auth phc-argon2
Copied!
With those installed, let’s next configure the AdonisJS Auth package within our project.
node ace configure @adonisjs/auth # ❯ Select provider for finding user · lucid # ❯ Select which guard you need for authentication · web # ❯ Enter model name to be used for authentication · User # ❯ Create migration for the users table? · true
Copied!
Creating Our Admin Model & Migration
So, we created our user model and migration above with our auth configuration. Next, we’ll want to get the same structure created for Admin. You can copy/paste both your User
model and users
migration as your Admin
model and admins
migration. But, to keep things easier to follow along with in written format, we’ll go ahead and create these using the Ace CLI.
node ace make:model Admin -m
Copied!
The above will get our Admin
model created, but thanks to the addition of -m
it’ll also create a migration for this model as well.
Admin Migration
Let’s go ahead and duplicate the columns from our users
migration for our admins
migration. If you’re copying/pasting from your users
migration, remember to update the table name to admins
.
import BaseSchema from '@ioc:Adonis/Lucid/Schema' export default class UsersSchema extends BaseSchema { protected tableName = 'admins' public async up() { this.schema.createTable(this.tableName, (table) => { table.increments('id').primary() table.string('email', 255).notNullable() table.string('password', 180).notNullable() table.string('remember_me_token').nullable() /** * Uses timestampz for PostgreSQL and DATETIME2 for MSSQL */ table.timestamp('created_at', { useTz: true }).notNullable() table.timestamp('updated_at', { useTz: true }).notNullable() }) } public async down() { this.schema.dropTable(this.tableName) } }
Copied!
- database
- migrations
- TIMESTAMP_admins.ts
Admin Model
We’ll also want to do the same for our Admin
model. If you’re copying/pasting from your User
model, remember to update the class name to Admin
and to update the hashPassword
type to Admin
as well.
import { DateTime } from 'luxon' import Hash from '@ioc:Adonis/Core/Hash' import { column, beforeSave, BaseModel } from '@ioc:Adonis/Lucid/Orm' export default class Admin extends BaseModel { @column({ isPrimary: true }) public id: number @column() public email: string @column({ serializeAs: null }) public password: string @column() public rememberMeToken?: string @column.dateTime({ autoCreate: true }) public createdAt: DateTime @column.dateTime({ autoCreate: true, autoUpdate: true }) public updatedAt: DateTime @beforeSave() public static async hashPassword (admin: Admin) { if (admin.$dirty.password) { admin.password = await Hash.make(admin.password) } } }
Copied!
- app
- Models
- Admin.ts
Run Migrations
Our migrations are now good to go, so let's go ahead and run them.
node ace migration:run # ❯ migrated database/migrations/TIMESTAMP_users # ❯ migrated database/migrations/TIMESTAMP_admins
Copied!
Creating Our Authentication Guards
With our database now set up, let's now focus on our authentication system by first getting our guards defined.
Auth Configuration
First, let’s get our auth configuration file setup. This file is what defines the default authentication guard (the guard
property) and specific configurations for each guard our application uses. Most notably, this is where we’ll define what model each guard should use.
So, our default guard
is web
, which is what we defined when we configured authentication within our project. To help make things a tad easier to understand, let's rename this to user
. So, with this change, when we want to login using the User
model we’ll use the user
guard. This will also be our default guard as well.
const authConfig: AuthConfig = { guard: 'user', // 👈 change from 'web' to 'user' guards: { /* |-------------------------------------------------------------------------- | Web Guard |-------------------------------------------------------------------------- | | Web guard uses classic old school sessions for authenticating users. | If you are building a standard web application, it is recommended to | use web guard with session driver | */ user: { // 👈 change from web to user driver: 'session', provider: { /* |-------------------------------------------------------------------------- | Driver |-------------------------------------------------------------------------- | | Name of the driver | */ driver: 'lucid', /* |-------------------------------------------------------------------------- | Identifier key |-------------------------------------------------------------------------- | | The identifier key is the unique key on the model. In most cases specifying | the primary key is the right choice. | */ identifierKey: 'id', /* |-------------------------------------------------------------------------- | Uids |-------------------------------------------------------------------------- | | Uids are used to search a user against one of the mentioned columns. During | login, the auth module will search the user mentioned value against one | of the mentioned columns to find their user record. | */ uids: ['email'], /* |-------------------------------------------------------------------------- | Model |-------------------------------------------------------------------------- | | The model to use for fetching or finding users. The model is imported | lazily since the config files are read way earlier in the lifecycle | of booting the app and the models may not be in a usable state at | that time. | */ model: () => import('App/Models/User'), }, } }
Copied!
- config
- auth.ts
You’ll notice TypeScript will begin to complain about this whole section, don’t worry we’ll fix that in a moment.
Next, let’s copy/paste the user
key and its object value and remap this for our Admin
model by changing the pasted key name to admin
and the model return value import to App/Models/Admin
. To help prevent confusion, below is the entire auth configuration file with the changes noted.
/** * Config source: <https://git.io/JY0mp> * * Feel free to let us know via PR, if you find something broken in this config * file. */ import { AuthConfig } from '@ioc:Adonis/Addons/Auth' /* |-------------------------------------------------------------------------- | Authentication Mapping |-------------------------------------------------------------------------- | | List of available authentication mapping. You must first define them | inside the `contracts/auth.ts` file before mentioning them here. | */ const authConfig: AuthConfig = { guard: 'user', guards: { /* |-------------------------------------------------------------------------- | Web Guard |-------------------------------------------------------------------------- | | Web guard uses classic old school sessions for authenticating users. | If you are building a standard web application, it is recommended to | use web guard with session driver | */ user: { driver: 'session', provider: { /* |-------------------------------------------------------------------------- | Driver |-------------------------------------------------------------------------- | | Name of the driver | */ driver: 'lucid', /* |-------------------------------------------------------------------------- | Identifier key |-------------------------------------------------------------------------- | | The identifier key is the unique key on the model. In most cases specifying | the primary key is the right choice. | */ identifierKey: 'id', /* |-------------------------------------------------------------------------- | Uids |-------------------------------------------------------------------------- | | Uids are used to search a user against one of the mentioned columns. During | login, the auth module will search the user mentioned value against one | of the mentioned columns to find their user record. | */ uids: ['email'], /* |-------------------------------------------------------------------------- | Model |-------------------------------------------------------------------------- | | The model to use for fetching or finding users. The model is imported | lazily since the config files are read way earlier in the lifecycle | of booting the app and the models may not be in a usable state at | that time. | */ model: () => import('App/Models/User'), }, }, admin: { // 👈 update pasted key to admin driver: 'session', provider: { /* |-------------------------------------------------------------------------- | Driver |-------------------------------------------------------------------------- | | Name of the driver | */ driver: 'lucid', /* |-------------------------------------------------------------------------- | Identifier key |-------------------------------------------------------------------------- | | The identifier key is the unique key on the model. In most cases specifying | the primary key is the right choice. | */ identifierKey: 'id', /* |-------------------------------------------------------------------------- | Uids |-------------------------------------------------------------------------- | | Uids are used to search a user against one of the mentioned columns. During | login, the auth module will search the user mentioned value against one | of the mentioned columns to find their user record. | */ uids: ['email'], /* |-------------------------------------------------------------------------- | Model |-------------------------------------------------------------------------- | | The model to use for fetching or finding users. The model is imported | lazily since the config files are read way earlier in the lifecycle | of booting the app and the models may not be in a usable state at | that time. | */ model: () => import('App/Models/Admin'), // 👈 update pasted import to Admin }, }, }, } export default authConfig
Copied!
- config
- auth.ts
With that, our auth configuration is ready! Next, let’s get these TypeScript errors fixed up by updating the type definitions.
Auth Contract
The auth contract module contains type definitions for our authentication providers and our guards. We made these definitions out-of-date with our prior changes, so all we need to do is update web
to user
and duplicate user
for our admin
. Essentially the same process we went through for our auth configuration.
Guards List
First, let's fix up our GuardsList
, this is at the bottom of our auth contract file. Here we’ll just need to update two web
usages to user
.
declare module '@ioc:Adonis/Addons/Auth' { interface ProvidersList { // ... } /* |-------------------------------------------------------------------------- | Guards |-------------------------------------------------------------------------- | | The guards are used for authenticating users using different drivers. | The auth module comes with 3 different guards. | | - SessionGuardContract | - BasicAuthGuardContract | - OATGuardContract ( Opaque access token ) | | Every guard needs a provider for looking up users from the database. | */ interface GuardsList { /* |-------------------------------------------------------------------------- | Web Guard |-------------------------------------------------------------------------- | | The web guard uses sessions for maintaining user login state. It uses | the `user` provider for fetching user details. | */ web: { user: { implementation: SessionGuardContract<'user', 'web'> implementation: SessionGuardContract<'user', 'user'> config: SessionGuardConfig<'user'> } } }
Copied!
- contracts
- auth.ts
The first argument to the SessionGuard
types are for the provider to use within the ProvidersList
, by default this is user
, so we’ll leave that as-is. However, the second argument is the guard key name to use and that we will want to update from web
to user
.
Let’s go ahead and duplicate our user
guard for our admin
guard, replacing user
with admin
everywhere within the duplicated contents. Below will be our GuardsList
after this change is made.
interface GuardsList { user: { implementation: SessionGuardContract<'user', 'user'> config: SessionGuardConfig<'user'> }, admin: { implementation: SessionGuardContract<'admin', 'admin'> config: SessionGuardConfig<'admin'> } }
Copied!
- contracts
- auth.ts
Providers List
Next, we’ll get our ProvidersList
updated and all we need to do here is duplicate the user
provider definition for our admin
. To make things easy to understand, below is our complete auth contract file after this change has been made.
/** * Contract source: <https://git.io/JOdz5> * * Feel free to let us know via PR, if you find something broken in this * file. */ import User from 'App/Models/User' import Admin from 'App/Models/Admin' declare module '@ioc:Adonis/Addons/Auth' { /* |-------------------------------------------------------------------------- | Providers |-------------------------------------------------------------------------- | | The providers are used to fetch users. The Auth module comes pre-bundled | with two providers that are `Lucid` and `Database`. Both uses database | to fetch user details. | | You can also create and register your own custom providers. | */ interface ProvidersList { /* |-------------------------------------------------------------------------- | User Provider |-------------------------------------------------------------------------- | | The following provider uses Lucid models as a driver for fetching user | details from the database for authentication. | | You can create multiple providers using the same underlying driver with | different Lucid models. | */ user: { implementation: LucidProviderContract<typeof User> config: LucidProviderConfig<typeof User> }, admin: { implementation: LucidProviderContract<typeof Admin> config: LucidProviderConfig<typeof Admin> } } /* |-------------------------------------------------------------------------- | Guards |-------------------------------------------------------------------------- | | The guards are used for authenticating users using different drivers. | The auth module comes with 3 different guards. | | - SessionGuardContract | - BasicAuthGuardContract | - OATGuardContract ( Opaque access token ) | | Every guard needs a provider for looking up users from the database. | */ interface GuardsList { /* |-------------------------------------------------------------------------- | User Guard |-------------------------------------------------------------------------- | | The web guard uses sessions for maintaining user login state. It uses | the `user` provider for fetching user details. | */ user: { implementation: SessionGuardContract<'user', 'user'> config: SessionGuardConfig<'user'> }, admin: { implementation: SessionGuardContract<'admin', 'admin'> config: SessionGuardConfig<'admin'> } } }
Copied!
- contracts
- auth.ts
With this in place, you should now be able to go back into your auth configuration and see all the TypeScript errors are fixed up.
Below is the gist of what we just set up:
To login/register with the
User
model, use theuser
guardTo login/register with the
Admin
model, use theadmin
guard
Auth Validator
Let's take a brief aside to make our user and admin authentication a tad easier by creating a generic AuthValidator
.
node ace make:validator Auth
Copied!
All we need to do is add a simple check for an email
and a password
.
public schema = schema.create({ email: schema.string([rules.email()]), password: schema.string() })
Copied!
- app
- Validators
- AuthValidator.ts
Registration
Next, let's get our registrations set up for both our User
and Admin
. I’ll show the controller method for both below, then discuss the difference.
// User Registration // app/Controllers/Http/AuthUserController.ts public async register({ request, response, auth }: HttpContextContract) { const data = await request.validate(AuthValidator) const user = await User.create(data) await auth.login(user) return response.redirect('/') }
Copied!
// Admin Registration // app/Controllers/Http/AuthAdminController.ts public async register({ request, response, auth }: HttpContextContract) { const data = await request.validate(AuthValidator) const admin = await Admin.create(data) await auth.use('admin').login(admin) return response.redirect('/') }
Copied!
The first thing you’ll notice is that our user registration uses the User
model to create the user record within our database while our admin registration uses the Admin
model.
Next, you’ll notice our admin registration calls an additional use('admin')
method prior to calling login
. This use
method is how we specify which authentication guard we want the auth
module to use. Since the user
guard is set up as our default we can omit this for our user registration, but we need to specify to use the admin
guard for our admin registration.
Login Routes & FOrms
Let’s add route definitions for both of these and rig them up to the forms.
Route.post('/auth/user/register', 'AuthUserController.register').as('auth.user.register') Route.post('/auth/admin/register', 'AuthAdminController.register').as('auth.admin.register')
Copied!
- start
- routes.ts
{{-- resources/views/auth/user.edge --}} @layout('layouts/main') @section('content') <main class="flex flex-wrap justify-center mt-24"> @!form.login({ title: 'User Login' }) {{-- 👇 add the action: route('auth.user.register') here --}} @!form.register({ title: 'User Registration', action: route('auth.user.register') }) <div class="w-full text-center mt-8"> <a href="{{ route('auth.admin.index') }}" class="text-blue-600"> Admin? Login here instead. </a> </div> </main> @endsection
Copied!
{{-- resources/views/auth/admin.edge --}} @layout('layouts/main') @section('content') <main class="flex flex-wrap justify-center mt-24"> @!form.login({ title: 'Admin Login' }) {{-- 👇 add the register form here along with the action --}} @!form.register({ title: 'Admin Registration (for simplicity)', action: route('auth.admin.register') }) <div class="w-full text-center mt-8"> <a href="{{ route('auth.user.index') }}" class="text-blue-600"> User? Login or register here instead. </a> </div> </main> @endsection
Copied!
Silent Auth Middleware
The SilentAuth
middleware will perform a check to see if there is a currently authenticated user or not using your default auth guard. Let’s go ahead and add this guard globally for our project so that our auth.user
will auto-populate for us.
Server.middleware.register([ () => import('@ioc:Adonis/Core/BodyParser'), () => import('App/Middleware/SilentAuth') // 👈 add SilentAuth here ])
Copied!
- start
- kernel.ts
Next, let's update the SilentAuth
middleware to account for our admin
guard.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' /** * Silent auth middleware can be used as a global middleware to silent check * if the user is logged-in or not. * * The request continues as usual, even when the user is not logged-in. */ export default class SilentAuthMiddleware { /** * Handle request */ public async handle({ auth }: HttpContextContract, next: () => Promise<void>) { /** * Check if user is logged-in or not. If yes, then `ctx.auth.user` will be * set to the instance of the currently logged in user. */ // check to see if admin is login if (await auth.use('admin').check()) { // admin is logged in // lets set the requests default guard to 'admin' auth.defaultGuard = 'admin' } await auth.check() await next() } }
Copied!
- app
- Middleware
- SilentAuth.ts
Here, we’ll perform and if check on our admin
guard to see if an administrator is currently authenticated. If this returns truthy, we know an admin is logged in for this request. So, we’re safe to set this request’s default guard to admin
.
Otherwise, the request will continue onward as usual for our default user
guard.
By updating the default guard when an admin is logged in, we’re simplifying the logic needed to perform a logout, as you see next.
Logout
Since we’re updating the defaultGuard
to admin
whenever an administrator is authenticated, we’re clear to use auth.logout()
as-is without needing to worry about applying it to a specific guard. We’ve offloaded that check globally to our SilentAuth
middleware.
So, since logout will be a generic call for both our auth guards, let’s create a generic route within our routes.ts
file.
Route.get('/auth/logout', async ({ response, auth }) => { await auth.logout() return response.redirect('/') }).as('auth.logout')
Copied!
- start
- routes.ts
On the Edge side, hop into the welcome.edge
file and replace the auth check within the if statement and the logout button with the below.
{{-- resources/views/welcome.edge --}} @layout('layouts/main') @section('content') <main class="flex flex-wrap justify-center items-center h-full"> @if (auth.user) <h3>{{ auth.user.email }}</h3> {{-- Logged In --}} <a href="{{ route('auth.logout') }}"> Logout </a> @else {{-- Logged Out --}} <div class="flex flex-col space-y-3 items-center"> <a href="{{ route('auth.user.index') }}" class="text-blue-600">User Authentication</a> <a href="{{ route('auth.admin.index') }}" class="text-blue-600">Admin Authentication</a> </div> @endif </main> @endsection
Copied!
Logging In
Lastly, is logging in. This will be fairly similar to our registration changes. Again, I’ll put both the user and admin login controller methods below, then describe the differences.
// User Login // app/Controllers/Http/AuthUserController.ts public async login({ request, response, auth }: HttpContextContract) { const { email, password } = await request.validate(AuthValidator) await auth.attempt(email, password) return response.redirect('/') }
Copied!
// Admin Login // app/Controllers/Http/AuthAdminController.ts public async login({ request, response, auth }: HttpContextContract) { const { email, password } = await request.validate(AuthValidator) await auth.use('admin').attempt(email, password) return response.redirect('/') }
Copied!
As you can see, the only difference here is the specification on which guard to use when attempting to login. Note again, our user
guard is our default guard so we don’t need to call the use('user')
there, though you absolutely could if you prefer the specificity!
Login Routes & Forms
Next, let’s get the routes setup
Route.post('/auth/user/login', 'AuthUserController.login').as('auth.user.login') Route.post('/auth/admin/login', 'AuthAdminController.login').as('auth.admin.login')
Copied!
- start
- routes.ts
Then, let’s get them added to our user and admin auth pages.
{{-- resources/views/auth/user.edge --}} @layout('layouts/main') @section('content') <main class="flex flex-wrap justify-center mt-24"> @!form.login({ title: 'User Login', action: route('auth.user.login') }) @!form.register({ title: 'User Registration', action: route('auth.user.register') }) <div class="w-full text-center mt-8"> <a href="{{ route('auth.admin.index') }}" class="text-blue-600"> Admin? Login here instead. </a> </div> </main> @endsection
Copied!
{{-- resources/views/auth/admin.edge --}} @layout('layouts/main') @section('content') <main class="flex flex-wrap justify-center mt-24"> @!form.login({ title: 'Admin Login', action: route('auth.admin.login') }) @!form.register({ title: 'Admin Registration (for simplicity)', action: route('auth.admin.register') }) <div class="w-full text-center mt-8"> <a href="{{ route('auth.user.index') }}" class="text-blue-600"> User? Login or register here instead. </a> </div> </main> @endsection
Copied!
Wrapping Up
If you’ve followed all of the above, you should now have a working system allowing you to register, logout, and login using either the User
model or Admin
model within your AdonisJS application. Test it out, and alter what we have above to fit your needs!
Join The Discussion! (2 Comments)
Please sign in or sign up for free to join in on the dicussion.
ramiro-francisco-nzau
How can i make it with adonisjs 6?
Please sign in or sign up for free to reply
tomgobich
The premise would be the same as shown in this lesson, just using the updated code for AdonisJS 6 authentication. You'd want to configure your admin and user guards in the auth config, pointing to the correct model in each guard. Include the
AuthFinder
mixin in both models. Then, it should essentially be the same login flow as any AdonisJS 6 app, just using the correct guard.Please sign in or sign up for free to reply