Creation & Setup
First, we'll want to create our project. I'll be naming my project adonisjs-auth. If you’d like you can view the finished repository here.
npm init adonis-ts-app@latest adonisjs-auth # ❯ Select the project structure · web # ❯ Enter the project name · adonisjs-auth # ❯ Setup eslint? (y/N) · false # ❯ Configure webpack encore for compiling frontend assets? (y/N) · false
Copied!
Next, let's move into the directory.
cd adonisjs-auth
Copied!
Then, we'll want to install both Lucid and Auth. We'll need Lucid for Auth because the Auth package relies on Lucid to work.
npm i @adonisjs/lucid @adonisjs/auth
Copied!
Once those are installed, we'll want to configure them. This will configure types, commands, and providers, and more for these packages within our project. We'll specifically want to configure Lucid first since Auth needs Lucid to work.
node ace configure @adonisjs/lucid # ❯ Select the database driver you want to use · pg # CREATE: config/database.ts # UPDATE: .env,.env.example # UPDATE: tsconfig.json { types += "@adonisjs/lucid" } # UPDATE: .adonisrc.json { commands += "@adonisjs/lucid/build/commands" } # UPDATE: .adonisrc.json { providers += "@adonisjs/lucid" }
Copied!
Next, we'll do the same for Auth.
node ace configure @adonisjs/auth # ❯ Select provider for finding users · lucid # ❯ Select which guard you need for authentication (select using space) · web # ❯ Enter model name to be used for authentication · User # ❯ Create migration for the users table? (y/N) · true # CREATE: app/Models/User.ts # CREATE: database/migrations/1639232007772_users.ts # CREATE: contracts/auth.ts # CREATE: config/auth.ts # CREATE: app/Middleware/Auth.ts # CREATE: app/Middleware/SilentAuth.ts # UPDATE: tsconfig.json { types += "@adonisjs/auth" } # UPDATE: .adonisrc.json { providers += "@adonisjs/auth" } # CREATE: ace-manifest.json file
Copied!
With the selections I made when configuring Auth, it's also going to stub a migration file to create the users table for me. Before we run this migration, I'm going to add a username column to it and enforce my username and email to be unique.
import BaseSchema from '@ioc:Adonis/Lucid/Schema' export default class UsersSchema extends BaseSchema { protected tableName = 'users' public async up() { this.schema.createTable(this.tableName, (table) => { table.increments('id').primary() table.string('username', 50).notNullable().unique() // ++ table.string('email', 255).notNullable().unique() // add unique 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
- 1639232007772_users.ts
We'll also want to add the username column to our user's model.
import { DateTime } from 'luxon' import Hash from '@ioc:Adonis/Core/Hash' import { column, beforeSave, BaseModel } from '@ioc:Adonis/Lucid/Orm' export default class User extends BaseModel { @column({ isPrimary: true }) public id: number @column() // ++ public username: string // ++ @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 (user: User) { if (user.$dirty.password) { user.password = await Hash.make(user.password) } } }
Copied!
- app
- Models
- User.ts
Lastly, be sure to configure your environment variables for your database connection. Since I'm using Postgres, below are my variables & configuration. Yours is likely going to be at least a little different.
DB_CONNECTION=pg
PG_HOST=localhost
PG_PORT=5432
PG_USER=postgres
PG_PASSWORD=password
PG_DB_NAME=adonisjs-blog
Now that we have our migration and database connection setup, we can run our user migration.
node ace migration:run # ❯ migrated database/migrations/1639232007772_users
Copied!
Installing Hashing
As you may have noticed, our User model hashing the user's password before saving the user record. In order for this to work, we're going to need to install the hashing package we wish to use. I typically use phc-argon2
, so let's go ahead and install that.
npm i phc-argon2
Copied!
Auto-Populating Auth User
When it comes to populating the authenticated user record for a request, we can either have AdonisJS automatically do it for us via the SilentAuth middleware, or we can manually call await auth.check()
. SilentAuth allows us to set it and forget it, so we'll go with that approach here.
So open up the kernel.ts
file within the start directory. Then, under the global middleware, add the SilentAuth middleware. This was added to our project when we configured the auth package.
Server.middleware.register([ () => import('@ioc:Adonis/Core/BodyParser'), () => import('App/Middleware/SilentAuth') // ++ ])
Copied!
- start
- kernel.ts
Login With Username or Email
Since we've added a username to our user, let's go ahead and configure it so the Auth package will allow the user to login with either the username or the email. Within the Auth Config, is an array called uids
. All we need to do is add username to this array.
const authConfig: AuthConfig = { guard: 'web', guards: { web: { driver: 'session', provider: { // ... uids: ['username', 'email'], // ... } } } }
Copied!
- config
- auth.ts
Implementing Authentication
First, let's create a controller to house our authentication logic.
node ace make:controller Auth # CREATE: app/Controllers/Http/AuthController.ts
Copied!
Let's jump into that file and wire up our show pages for registration and login.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' export default class AuthController { public async registerShow({ view }: HttpContextContract) { return view.render('auth/register') } public async loginShow({ view }: HttpContextContract) { return view.render('auth/login') } }
Copied!
- app
- Controllers
- Http
- AuthController.ts
Then, we'll register these controller methods to routes.
Route.get('register', 'AuthController.registerShow').as('auth.register.show') Route.get('login', 'AuthController.loginShow').as('auth.login.show')
Copied!
- start
- routes.ts
Next, let's create the register and login pages.
node ace make:view auth/register # CREATE: resources/views/auth/register.edge node ace make:view auth/login # CREATE: resources/views/auth/login.edge
Copied!
Let’s first focus on the register page.
{{-- resources/views/auth/register.edge --}} <form action="{{ route('auth.register') }}" method="POST"> <label> Username <input type="text" name="username" value="{{ flashMessages.get('username') ?? '' }}" /> @if (flashMessages.has('errors.username')) <small style="color: red;"> {{ flashMessages.get('errors.username') }} </small> @endif </label> <label> Email <input type="email" name="email" value="{{ flashMessages.get('email') ?? '' }}" /> @if (flashMessages.has('errors.email')) <small style="color: red;"> {{ flashMessages.get('errors.email') }} </small> @endif </label> <label> Password <input type="password" name="password" /> @if (flashMessages.has('errors.password')) <small style="color: red;"> {{ flashMessages.get('errors.password') }} </small> @endif </label> <button type="submit">Register</button> </form>
Copied!
Here we have a form posting to a route with the name auth.register. We'll be creating this route shortly. The flashMessages
are there in case the user submits and our validation fails. The user will be redirected back to this form.
value="{{ flashMessages.get('username') ?? '' }}"
Copied!
The above will populate the field's value with the previously submitted value, so the user doesn't need to retype the whole form and can actually see what they attempted to submit with.
@if (flashMessages.has('errors.username')) <small style="color: red;"> {{ flashMessages.get('errors.username') }} </small> @endif
Copied!
This if block will then check our flashMessages
for errors for specific fields. If it finds one for the requested field we then display it onto our form to notify the user.
Next, let's do the same for our login page. Remember, users can login with either their username or email, so we'll only use one field for that called uid.
{{-- resources/views/auth/login.edge --}} <form action="{{ route('auth.login') }}" method="POST"> @if (flashMessages.has('form')) <div role="alert"> {{ flashMessages.get('form') }} </div> @endif <label> Username or Email <input type="text" name="uid" /> </label> <label> Password <input type="password" name="password" /> </label> <button type="submit">Login</button> </form>
Copied!
With the login page, we don't want to explicitly tell the user whether they got the username/email or password wrong for account security reasons. So, we'll instead be vague and provide a single flash message for the form, called form.
Next, let's create the login and register methods within our auth controller. While we're here let's also go ahead and add the logout functionality.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' import { schema, rules } from '@ioc:Adonis/Core/Validator' import User from "App/Models/User"; export default class AuthController { // ... show methods excluded for brevity public async register({ request, response, auth }: HttpContextContract) { // create validation schema for expected user form data const userSchema = schema.create({ username: schema.string({ trim: true }, [rules.unique({ table: 'users', column: 'username', caseInsensitive: true })]), email: schema.string({ trim: true }, [rules.email(), rules.unique({ table: 'users', column: 'email', caseInsensitive: true })]), password: schema.string({}, [rules.minLength(8)]) }) // get validated data by validating our userSchema // if validation fails the request will be automatically redirected back to the form const data = await request.validate({ schema: userSchema }) // create a user record with the validated data const user = await User.create(data) // login the user using the user model record await auth.login(user) // redirect to the login page return response.redirect('/') } public async login({ request, response, auth, session }: HttpContextContract) { // grab uid and password values off request body const { uid, password } = request.only(['uid', 'password']) try { // attempt to login await auth.attempt(uid, password) } catch (error) { // if login fails, return vague form message and redirect back session.flash('form', 'Your username, email, or password is incorrect') return response.redirect().back() } // otherwise, redirect to home page return response.redirect('/') } public async logout({ response, auth }: HttpContextContract) { // logout the user await auth.logout() // redirect to login page return response.redirect().toRoute('auth.login.show') } }
Copied!
- app
- Controllers
- Http
- AuthController.ts
For our register method, we’re first creating a validation schema for our user. This will validate that our username and email are unique, that our email is a valid email, and that our password is at least 8 characters long. Then, we validate using our user schema, which returns back the validated data. We then use that validated data to create our user’s record. Then, we use that user’s new record to log the user in.
For login, all we’re doing is grabbing the uid and password off the request body, no need to validate here the attempt call will suffice. We then provide the uid and password values into the auth.attempt
call to attempt to login. If it fails, we capture that error and return back a vague flash error on the session and kick the user back to the form page.
For logout, we’re simply calling auth.logout
, which will take care of everything for us.
Now that we have these methods, let's go ahead and create the routes for these methods.
Route.get('register', 'AuthController.registerShow').as('auth.register.show') Route.post('register', 'AuthController.register').as('auth.register') // ++ Route.get('login', 'AuthController.loginShow').as('auth.login.show') Route.post('login', 'AuthController.login').as('auth.login') // ++ Route.get('logout', 'AuthController.logout').as('auth.logout') // ++
Copied!
- start
- routes.ts
Lastly, let's add a logout button onto our welcome page and display it only if the user is authenticated.
{{-- resources/views/welcome.edge --}} {{-- ... --}} <body> @if (auth.user) <a href="{{ route('auth.logout') }}">Logout</a> @endif <main> {{-- ... --}} </main> </body>
Copied!
Test It Out!
That should do it, all that's left to do now is to test out our authentication! So, start up your server, head to [<http://localhost:3333/register>](<http://localhost:3333/register>)
and test your registration. Logout, then test your login at http://localhost:3333/login
.
You can start your server by running:
npm run dev
Copied!
Join The Discussion! (2 Comments)
Please sign in or sign up for free to join in on the dicussion.
Anonymous (JaguarMadonna833)
the script doesn't find loginShow method and registerShow method that are called in route.ts
Please sign in or sign up for free to reply
Anonymous (JaguarMadonna833)
fixed
Please sign in or sign up for free to reply