Social login and authentication in Adonis JS

Node JS and typescript

·

4 min read

Social login and authentication in Adonis JS

In this tutorial we will be going through user authentication in Node JS specifically Adonis JS. We will be using social login methods example: sign in with Facebook, Google and GitHub, using an Adonis JS package called Ally.

Let’s get into it.

I will assume you have an Adonis project already set up with lucid or your preferred method for storing information, we will then need to install the following packages: • Ally • Auth Ally is a social login tool for Adonis JS, it has to be installed and configured separately.

Run the following commands to install and configure Ally:

npm i @adonisjs/ally

node ace configure @adonisjs/ally.

You will have to update your “clientId”, “clientSecret” and “callbackUrl” in the configuration file stored in the config/ally.ts directory. ClientId and clientSecret are gotten from which platform you choose to use ie facebook, Google, Github, while the callbackUrl is the url which you will define to handle the response gotten from the provider. For this tutorial, i will be using the google provider.

Step 1: make user model and migration.

Using the cli command:

node ace make:model User –m

The “-m” flag creates a migration along with the model. Add other fields you want to store on the table.

The user migration file:

import BaseSchema from '@ioc:Adonis/Lucid/Schema'

export default class Users extends BaseSchema {
  protected tableName = 'users'

  public async up () {
    this.schema.createTable(this.tableName, (table) => {
      table.increments('id');
      table.string('name').notNullable();
      table.string('avatar_url');
      table.string('email').notNullable();
      table.string('provider');
      table.string('provider_id');


      /**
       * Uses timestamptz for PostgreSQL and DATETIME2 for MSSQL
       */
      table.timestamp('created_at', { useTz: true })
      table.timestamp('updated_at', { useTz: true })
    })
  }

  public async down () {
    this.schema.dropTable(this.tableName)
  }
}

This is pretty self-explanatory, we are creating a table with column: • Id: auto incrementing counter for the rows • Name: the name of the user which we will get from the provider • Avatar_url: user profile picture url, stored as a string • Email: user email • Provider: the driver the user used to sign up for our app • Provider id: a unique id gotten from the provider The created_at and updated_at are auto generated and will be updated automatically on creation and update of rows ie user. The user model:

import { DateTime } from 'luxon'
import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'

export default class User extends BaseModel {
  @column()
  public id: number

  @column()
  public name: string;

  @column()
  public avatar_url: string;

  @column({isPrimary: true})
  public email: string;

  @column()   
  public providerId: string;

  @column()
  public provider: string;

  @column.dateTime({ autoCreate: true })
  public createdAt: DateTime

  @column.dateTime({ autoCreate: true, autoUpdate: true })
  public updatedAt: DateTime
}

Note that the content of your model should always match your migration.

Step 2: Create signup controller

Use the cli command: node ace make: controller GoogleSignup A file will be created in the app/controllers directory. Paste the following code in the file:

import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import User from 'App/Models/User';

export default class GoogleSignInsController {
    public async redirect({ally}: HttpContextContract) {
        return ally.use('google').redirect() 
    }

}

We are creating a method that redirects the user to the OAuth providers website for authentication.

Step 3: Handle callback

Paste the following code in the same file, it includes the method created above.

import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import User from 'App/Models/User';

export default class GoogleSignInsController {
    public async redirect({ally}: HttpContextContract) {
        return ally.use('google').redirect() 
    }

    public async handleCallback ({ally, auth, response}: HttpContextContract) {
        const googleUser = ally.use('google');

        /**
         * User has explicitly denied the login request
         */
        if (googleUser.accessDenied()) {
            return 'Access was denied'
        }

        /**
         * Unable to verify the CSRF state
         */
        if (googleUser.stateMisMatch()) {
            return 'Request expired. try again'
        }

        /**
         * There was an unknown error during the redirect
         */
        if (googleUser.hasError()) {
            return googleUser.getError()
        }

        /**
         * Finally, access the user
         */
        const user = await googleUser.user();

        const findUser = {
            email: user.email as string
        }

        const userDetails = {
            name: user.name as string,
            email: user.email as string,
            avatar_url: user.avatarUrl as string,
            provider_id: user.id as string,
            provider: 'google'
        } 

        const newUser =await User.firstOrCreate(findUser, userDetails);

        await auth.use('web').login(newUser)
        response.status(200);

        return newUser;
    }

}

Here we are handling all the use cases if the sign in failed before we store the user in the database

const user = await googleUser.user(); //stores the user information object gotten back from google
 const newUser =await User.firstOrCreate(findUser, userDetails);

Here, we first query the database using the email of the user which is stored in the findUser object, if the email exists in the database, return the first one, otherwise create a new user with the userDetails object.

await auth.use('web').login(newUser)

Above, we use the built in adonis auth package to log the user in and create a session.

Step 4: Attach controllers to route

In the routes file, we will create a route for calling and initializing the OAuth provider and another route for handling the callback

// SIGN IN ROUTES
Route.get('/google-signin', 'GoogleSignInsController.redirect');

//OAuth CALLBACK
Route.get('/google-signin-callback', 'GoogleSignInsController.handleCallback');

Note that the route above will be the route you input as your callbackUrl in the ally configuration file.