Add Authentication to Your AdonisJS Project
Tags: adonisjs, node, postgres
Published: 2021-04-07 12:32:05

---
title: Add Authentication to Your AdonisJS Project
published: true
date: 2021-04-07 00:00:00 UTC
tags: AdonisJS, NodeJS, PostgreSQL
canonical_url: https://warrenwong.org/posts/add-authentication-to-adonisjs
cover_image: https://warrenwong.org/_nuxt/img/chepe-nicoli-UAElX0OkC2g-unsplash-960.jpg
---


> If you haven't created an AdonisJS 5.0 app yet, you can check out my [previous post](https://warrenwong.org/posts/setting-up-vscode-for-adonisjs) or follow the docs [here](https://preview.adonisjs.com/guides/quick-start).

Coming from the JavaScript/NodeJS world, I'm pretty used to having to pick off the npm menu for all the libraries I need, which then becomes a curation problem. With AdonisJS, there is usually a library already there for you to use to add in the common functionality you'd expect from any MVC framework.

First, we should install all the dependencies then run the `invoke` script for each of these libraries:

```bash
# npm
npm i @adonisjs/auth@alpha @adonisjs/session@alpha

# yarn
yarn add @adonisjs/auth@alpha @adonisjs/session@alpha
```

```bash
node ace invoke @adonisjs/auth
node ace invoke @adonisjs/session
```

Select the appropriate options for your project. You can see my choices below.

```
❯ 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/1587988332388_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
```

I don't currently have a `User` model for this project, so running the `invoke` script will create it for me.

If you haven't already done so, install `@adonisjs/lucid` as well:

```bash
# npm
npm i @adonisjs/lucid@alpha

yarn add @adonisjs/lucid@alpha
```

Run the invoke script like you did for the other libraries:

```bash
node ace invoke @adonisjs/lucid
```

You will be prompted to select which database to use. I'll use PostgreSQL for this example, but feel free to use any you'd like. There shouldn't be any differences for this example.

At this point, you'll have to update your `.env` file to reflect your setup:

```
PORT=3333
HOST=0.0.0.0
NODE_ENV=development
APP_KEY=...
SESSION_DRIVER=cookie
CACHE_VIEWS=false
DB_CONNECTION=pg
PG_HOST=localhost
PG_PORT=5432
PG_USER=postgres
PG_PASSWORD=postgres
PG_DB_NAME=example_app_dev
```

On my local machine, my `PG_USER` and `PG_PASSWORD` are the same: "postgres". Make sure to update the `PG_DB_NAME` as well, since the `invoke` script defaults that variable to "lucid". Here I use "example_app_dev".

Now if you haven't already, create a database that matches the `PG_DB_NAME` variable you have in your `.env` file. You can do that with the command line or with something like [pgAdmin 4](https://www.pgadmin.org/):

```bash
psql -U postgres
```

```sql
CREATE DATABASE example_app_dev;
\q
```

Now run the migration created earlier:

```bash
node ace migration:run
```

If you navigate to the newly created `User` model, you can what was created for us:

```ts
// app/Models/User.ts

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 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);
    }
  }
}
```

If you've used any Active Record type ORM before, everything should look familiar. There's a couple things I just wanted to point out though. The `password` column has an option of `{serializeAs: null}`, which makes it simple to deliver the `User` model to JSON using the `serialize` method on the model. You can alias that field in the JSON with `serializeAs`, but if you set it to `null`, that field won't show up, which is what we want with the `password` field.

Another thing I'd like to point out is the [`beforeSave`](https://preview.adonisjs.com/guides/models/hooks) decorator. This hook is invoked before `insert` and `update` queries, which is an obvious place to hash a password. Another interesting thing is the [`$dirty`](https://preview.adonisjs.com/guides/models/hooks#understanding-the-dirty-property) property on the model. Basically, we only want to hash a new or updated password. If that particular property hasn't changed, we should do nothing.

Now comes to something that can be a gotcha with the documentation. We are calling `Hash` from the `@ioc:Adonis/Core/Hash` module. It's nice that this is already a dependency, but what might confuse you is that you'll also have to select your own hashing library. If you run the migration at this point, nothing will be amiss until you try to actually hash a password. So let's avoid that and install the [argon2 dependency](https://www.npmjs.com/package/phc-argon2) for this module:

```bash
# npm
npm install phc-argon2

# yarn
yarn add phc-argon2
```

To add to the confusion, the `phc-argon2` library itself is a fork of `@phc/argon2`, which is no longer maintained. You would have had to find the [release notes](https://preview.adonisjs.com/releases/core/preview-rc-1_7/#hash) to figure this out.

> At this point you can follow the [official docs](https://preview.adonisjs.com/guides/auth/web-guard) because the out of the box experience is already pretty good. But if you want to keep following along for a more customized approach, please do.

If you open up the `app/Middleware/Auth.ts` file, you'll see the user will be redirected to the `/login` route if unauthenticated. I'm pretty partial to using the words "signin", "signout", and "signup" instead of "login", "logout", and "register", so that's what I'm going to do.

```ts
// app/Middleware/Auth.ts

export default class AuthMiddleware {
  /**
   * The URL to redirect to when request is Unauthorized
   */
  protected redirectTo = "/signin";
  ...
```

Now let's open up the `start/routes.ts` file and add our new routes:

```ts
// start/routes.ts

import Route from "@ioc:Adonis/Core/Route";

Route.on("/signin").render("signin");
Route.post("/signin", "AuthController.signin");
Route.on("/signup").render("signup");
Route.post("/signup", "AuthController.signup");
Route.post("/signout", "AuthController.signout");

Route.on("/").render("welcome");
```

As you can see, we'll need to create some views and a controller:

```bash
node ace make:view signin
node ace make:view signup
node ace make:controller Auth
```

If you already have [TailwindCSS](https://tailwindcss.com/) [installed in your project](/posts/add-tailwindcss-to-adonisjs), great! We'll be using Tailwind to design out `signin` and `signup` views. If not, let's bring it in to our project from the CDN. If you don't already have a layout edge template, create one now:

```bash
node ace make:view layouts/default
```

Now open up `default.edge` and add in our default HTML with our TailwindCSS dependency from CDN:

```html
<!-- resources/views/layouts/default.edge -->
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
  <title>Example App</title>
</head>

<body>
  @!section('content')
</body>

</html>
```

Now let's open up `resources/views/signup.edge`. Since I anticipate the `signup` and `signin` views will be rather similar, we can just create the `signup` view then copy it to `signin.edge` and remove the password confirmation section. We can also create a [partial](https://preview.adonisjs.com/guides/http/views-and-templates#using-partials) or [component](https://preview.adonisjs.com/guides/http/views-and-templates#using-components), but seeing as we're only using this particular form twice, I don't feel too bad about the duplication. There's going to be enough of a difference between the two templates that doesn't seem to make the work of generalizing the template worth it. You can, of course, do this on your own.

```html
<!-- resources/views/signup.edge -->

@layout('layouts/default')

@section('content')
<div class="min-h-screen flex flex-col justify-center bg-gray-200 p-8">
  <div class="mx-auto bg-white px-8 py-6 shadow rounded-lg">
    <form action="{{ route('AuthController.signup') }}" method="post" class="space-y-8">
      <div>
        <label for="email" class="block text-gray-600">Email</label>
        <input type="text" name="email" value="{{ flashMessages.get('email') || '' }}"
          class="w-full px-3 py-2 border border-gray-300 rounded" />
        <p>{{ flashMessages.get('errors.email') || '' }}</p>
      </div>

      <div>
        <label for="password" class="block text-gray-600">Password</label>
        <input type="password" name="password" class="w-full px-3 py-2 border border-gray-300 rounded" />
        <p>{{ flashMessages.get('errors.password') || '' }}</p>
      </div>

      <div>
        <label for="password_confirmation" class="block text-gray-600">Re-Enter Password</label>
        <input type="password" name="password_confirmation" class="w-full px-3 py-2 border border-gray-300 rounded" />
        <p>{{ flashMessages.get('errors.password_confirmation') || '' }}</p>
      </div>

      <div>
        <button type="submit" class="w-full flex justify-center px-3 py-2 text-white bg-blue-600 rounded">Create
          Account</button>
      </div>
    </form>
  </div>
</div>
@endsection
```

![Figure 1](https://warrenwong.org/_nuxt/img/add-authentication-to-adonisjs-fig-01-550.png)

Now let's copy this file to `resources/views/signin.edge`, but there's one change we need to make. We need to remove the `password_confirmation` field and replace it with a `remember_me` checkbox.

```html
<!-- resources/views/signin.edge -->

@layout('layouts/default')

@section('content')
<div class="min-h-screen flex flex-col justify-center bg-gray-200 p-8">
  <div class="mx-auto bg-white px-8 py-6 shadow rounded-lg">
    <form action="{{ route('AuthController.signup') }}" method="post" class="space-y-8">
      <div>
        <label for="email" class="block text-gray-600">Email</label>
        <input type="text" name="email" value="{{ flashMessages.get('email') || '' }}"
          class="w-full px-3 py-2 border border-gray-300 rounded" />
        <p>{{ flashMessages.get('errors.email') || '' }}</p>
      </div>

      <div>
        <label for="password" class="block text-gray-600">Password</label>
        <input type="password" name="password" class="w-full px-3 py-2 border border-gray-300 rounded" />
        <p>{{ flashMessages.get('errors.password') || '' }}</p>
      </div>

      <div class="flex items-center">
        <input type="checkbox" name="remember_me" class="h-4 w-4 border-gray-300 rounded">
        <label for="remember_me" class="ml-2 text-sm text-gray-600">
          Remember me
        </label>
      </div>

      <div>
        <button type="submit" class="w-full flex justify-center px-3 py-2 text-white bg-blue-600 rounded">Create
          Account</button>
      </div>
    </form>
  </div>
</div>
@endsection
```

Now that we have our views, let's open the `AuthController` and add the "post" methods `signin`, `signup`, and `signout`. Let's just redirect to the index route for the moment.

```ts
// app/Controllers/Http/AuthController.ts

import { HttpContextContract } from "@ioc:Adonis/Core/HttpContext";

export default class AuthController {
  public signup({ response }: HttpContextContract) {
    // 1. Validate request

    return response.redirect("/");
  }

  public signin({ response }: HttpContextContract) {
    return response.redirect("/");
  }

  public signout({ response }: HttpContextContract) {
    return response.redirect("/");
  }
}
```

Now, I already know something about my app. I want to validate my form data before I persist my user in the database. I can do all that from the controller, but that just doesn't seem like the right place for all that logic. For a simple app like ours (so far), there's no problem writing everything in the controller. I could place all this logic in the model by adding some extra helper methods, but that's probably not a great approach either since we want to keep our [models lean](https://preview.adonisjs.com/guides/models/introduction#creating-your-first-model). I want to put all my "account" management business logic in a single place from which I can access my `User` model. I'll create a file at `app/Account/index.ts`.

```ts
// app/Account/index.ts

import User from "App/Models/User";
import { rules, schema } from "@ioc:Adonis/Core/Validator";

export const validationSchema = schema.create({
  email: schema.string({ trim: true }, [
    rules.email(),
    rules.unique({ table: "users", column: "email" }),
  ]),
  password: schema.string({ trim: true }, [rules.confirmed()]),
});

export const createUser = async (email: string, password: string) => {
  const user = new User();
  user.email = email;
  user.password = password;

  return await user.save();
};
```

Here we created a `validationSchema` to be used by the `request.validate` method in the `AuthController`. It simply checked to see if the string passed in to the "email" input is in the form of an email and that it does not already exist in the `users` table. The password is checked to have a matching value in the "password_confirmation" input with the `rules.confirmed()` rule.

If there's any errors, they will show in the paragraph element under the form inputs.

```ts
// app/Controllers/Http/AuthController.ts

...
export default class AuthController {
  public async signup({ request, response }: HttpContextContract) {
    const userDetails = await request.validate({ schema: validationSchema });
    const user = await createUser(userDetails.email, userDetails.password);
    return response.json(user.serialize());
  }
  ...
}
```

You can test this out by running our local server and navigating to [http://localhost:3333](http://localhost:3333):

You should see the serialized JSON output if the signup was successful:

```json
{
  "email": "me@warrenwong.org",
  "created_at": "2021-04-07T15:02:51.730+08:00",
  "updated_at": "2021-04-07T15:02:51.754+08:00",
  "id": 1
}
```

Now that we can sign up, let's create a protected route that will redirect to the `/signin` page if unauthenticated. Let's go back to our routes and create a route at `/dashboard` that shows all our users.

```ts
// start/routes.ts

...
import { getUsers } from "App/Account";

Route.get("/dashboard", async ({ view }) => {
  const users = await getUsers();
  return view.render("dashboard", { users });
}).middleware("auth");
```

This assumes we have an exported function in our `App/Account` module that returns the users. The users are then passed into the Edge template as `users`.

```ts
// app/Account/index.ts

...
export const getUsers = async () => await User.all();
```

Create the `dashboard` view:

```bash
node ace make:view dashboard
```

Now we need to open `start/kernel.ts` and register our `Auth` middleware. You can name this anything, but "auth" seems like a decent default for this. Just make sure that the name you pick also matches the parameter passed to the `middleware` method in your routes.

```ts
// start/kernel.ts

...
Server.middleware.registerNamed({
  auth: "App/Middleware/Auth",
});
```

Now let's work on our dashboard before we get any further. We don't have too much data to display, but we may in the future, so let's anticipate that by displaying a nice table:

```html
<!-- resources/views/dashboard.edge -->

@layout('layouts/default')

@section('content')
<div class="min-h-screen flex flex-col">
  <div class="m-16">
    <div class="shadow border-b border-gray-300 rounded-lg">
      <table class="min-w-full divide-y divide-gray-300">
        <thead class="bg-gray-50 text-left">
          <tr>
            <th class="px-6 py-4 text-gray-700">
              ID
            </th>
            <th class="px-6 py-4 text-gray-700">
              Email
            </th>
            <th class="px-6 py-4 text-gray-700">
              Created
            </th>
            <th class="px-6 py-4 text-gray-700">
              Updated
            </th>
          </tr>
        </thead>
        <tbody class="bg-white">
          @each(user in users)            
          <tr>
            <td class="px-6 py-4 text-gray-600">
              {{ user.id }}
            </td>
            <td class="px-6 py-4 text-gray-600">
              {{ user.email }}
            </td>
            <td class="px-6 py-4 text-gray-600">
              {{ user.createdAt }}
            </td>
            <td class="px-6 py-4 text-gray-600">
              {{ user.updatedAt }}
            </td>
          </tr>
          @endeach
        </tbody>
      </table>
    </div>
  </div>
</div>
@endsection
```

Now let's update the `Auth` controller to redirect to the dashboard whenever we've authenticated.

```ts
export default class AuthController {
  public async signup({ auth, request, response }: HttpContextContract) {
    const userDetails = await request.validate({ schema: validationSchema });
    const user = await createUser(userDetails.email, userDetails.password);
    await auth.login(user);
    return response.redirect("/dashboard");
  }
  ...
```

There are a few details left, but they are pretty straight forward so I'll leave them for you to do.

- Our "signin" method on the `Auth` controller has yet to be implemented. For the most part, it's similar to the our "signup" flow. I would recommend validating the data that comes from the form, but this time you do not need to have a "password_confirmation". What you do have is a "remember_me" input field that's a boolean. You'd want to update the `User` model with that information and persist it in the database before redirecting the user to our dashboard.
- We don't have anyway to "signout" yet. It would be nice if you could only "signout" after you've authenticated.