Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Authentication

Authentication is checking who came to the site: the user enters an email and a password, and the application remembers them in the session. In AdonisJS the @adonisjs/auth package handles this. If you chose session login when creating a Web kit project, everything is already set up: the User model, the users table migration and config/auth.ts with the web guard.

The user model

// app/models/user.ts
import { DateTime } from 'luxon'
import hash from '@adonisjs/core/services/hash'
import { compose } from '@adonisjs/core/helpers'
import { BaseModel, column } from '@adonisjs/lucid/orm'
import { withAuthFinder } from '@adonisjs/auth/mixins/lucid'
 
const AuthFinder = withAuthFinder(() => hash.use('scrypt'), {
  uids: ['email'],
  passwordColumnName: 'password',
})
 
export default class User extends compose(BaseModel, AuthFinder) {
  @column({ isPrimary: true })
  declare id: number
 
  @column()
  declare fullName: string | null
 
  @column()
  declare email: string
 
  @column({ serializeAs: null })
  declare password: string
 
  @column.dateTime({ autoCreate: true })
  declare createdAt: DateTime
}

The withAuthFinder mixin adds two things to the model: the password is hashed automatically before saving, and the verifyCredentials method checks the email and password. serializeAs: null keeps the password out of JSON.

Signing up

// app/controllers/register_controller.ts
import type { HttpContext } from '@adonisjs/core/http'
import User from '#models/user'
import { registerValidator } from '#validators/auth'
 
export default class RegisterController {
  async store({ request, auth, response }: HttpContext) {
    const payload = await request.validateUsing(registerValidator)
    const user = await User.create(payload)
    await auth.use('web').login(user)
    return response.redirect('/posts')
  }
}

The validator lives in app/validators/auth.ts:

// app/validators/auth.ts
import vine from '@vinejs/vine'
 
export const registerValidator = vine.compile(
  vine.object({
    fullName: vine.string().trim().minLength(2),
    email: vine.string().email().unique({ table: 'users', column: 'email' }),
    password: vine.string().minLength(8),
  })
)

The unique rule prevents a second account with the same email. After creation the user is logged in right away.

Login and logout

// app/controllers/session_controller.ts
import type { HttpContext } from '@adonisjs/core/http'
import User from '#models/user'
 
export default class SessionController {
  async store({ request, auth, response }: HttpContext) {
    const { email, password } = request.only(['email', 'password'])
    const user = await User.verifyCredentials(email, password)
    await auth.use('web').login(user)
    return response.redirect('/posts')
  }
 
  async destroy({ auth, response }: HttpContext) {
    await auth.use('web').logout()
    return response.redirect('/login')
  }
}

If the password is wrong, verifyCredentials throws an error, and the form comes back with a message. Show it with the @error('E_INVALID_CREDENTIALS') tag with the $message variable inside.

Routes

// start/routes.ts
import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'
 
const SessionController = () => import('#controllers/session_controller')
 
router.on('/login').render('pages/auth/login').use(middleware.guest())
router.post('/login', [SessionController, 'store']).use(middleware.guest())
router.post('/logout', [SessionController, 'destroy']).use(middleware.auth())

middleware.guest() lets in only guests, middleware.auth() only logged-in users.

The current user

On routes with middleware.auth() the user is available as auth.user — in the controller and in the template:

{{-- resources/views/components/layout.edge --}}
@if(auth.user)
  <span>{{ auth.user.fullName }}</span>
  <form method="POST" action="/logout">
    {{ csrfField() }}
    <button type="submit">Log out</button>
  </form>
@else
  <a href="/login">Log in</a>
@end

On public pages without this middleware auth.user is empty until await auth.check() is called. The middleware lesson shows how to do that for all pages at once.

Common mistake

The password is hashed by hand before User.create, and the withAuthFinder mixin hashes it once more. The database gets a hash of a hash, and logging in becomes impossible. With withAuthFinder, pass the plain password to create.

Thanks for reading!

Donate to the site — we work hard for you.