Using Facebook/Twitter Authentication in Adonis 4.0

Ayeni Olusegun

In this tutorial, we are building an authentication system using Adonis-ally. I know the question on your mind, "what's adonis-ally?". Adonis-ally package makes it easier to authenticate your user via Facebook, Github, Google, LinkedIn, Twitter, Instagram, Foursquare and Bitbucket(Coming Soon). For this particular blog, we will be talking about authentication via Facebook and Twitter. I will talk about authentication via Google, Github, Instagram e.t.c in another tutorial.

Setting Up Adonis

Let's generate a new AdonisJS Application. For this tutorial, I'm using AdonisJS 4.0. Open your terminal and type this command:

# if you don't have AdonisJS CLI installed on your machine. 
$ npm i -g @adonisjs/cli

$ adonis new adonis-social

Then change directory into the application directory, start your server and test if it's working fine.

$ cd adonis-social

$ adonis serve --dev
2017-09-29T17:05:29.154Z - info: serving app on http://127.0.0.1:3333

Open your browser and make a request to http://127.0.0.1:3333. You should see Below UI.

Let's install adonis-ally package using adonis CLI

adonis install @adonisjs/ally

Then register the provider inside start/app.js file.

const providers = [
  //...
  '@adonisjs/ally/providers/AllyProvider'
  //...
]

Database Setup

For this tutorial, I will be using mysql. Create a database called adonis-social. Get your database's username and password and add it to the .env file in the project's root directory.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=adonis-social
DB_USERNAME=root
DB_PASSWORD=adonisjs

Migrations and Models

We are not going to create any migration or model but we will modify the existing user model and migration file before we migrate it.
Go to "database/migrations" directory, delete <TIMESTAMP>_token.js file then edit <TIMESTAMP>_user.js

TIMESTAMP_user.js
'use strict'

const Schema = use('Schema')

class UserSchema extends Schema {
  up () {
    this.create('users', table => {
      table.increments()
      table.string('name').nullable()
      table.string('avatar').nullable()
      table.string('username', 80).nullable()
      table.string('email', 254).nullable()
      table.string('provider_id').nullable()
      table.string('provider').nullable()
      table.string('password', 60).nullable()
      table.timestamps()
    })
  }

  down () {
    this.drop('users')
  }
}

module.exports = UserSchema

Install mysql module before we migrate our table.

$ npm install --save mysql

Let's go ahead with migrating this table.

adonis migration:run

Check your database and see that users table is created.

Let's move to app/Models directory, delete the Token.js file and edit the User.js file

'use strict'

const Model = use('Model')

class User extends Model {
  static boot () {
    super.boot()

    /**
     * A hook to bash the user password before saving
     * it to the database.
     *
     * Look at `app/Models/Hooks/User.js` file to
     * check the hashPassword method
     */
    this.addHook('beforeCreate', 'User.hashPassword')
  }

  static get table () {
    return 'users'
  }

  static get primaryKey () {
    return 'id'
  }
}

module.exports = User

Application Registration

In this section, we are going to obtain Facebook/Twitter client id and secret.

Let's start with Facebook -- click this link and add a new app. When done registering the application, click on settings on the sidenav. From the basic settings you can get your APP ID (client id) and APP Secret (client secret). Futher more, click the Add Platform button below the settings configuration. Select Website in the platform dialog box then enter the website URL, in our case it is http://localhost:8000. Set the App Domains to localhost then save the settings.

Update config\services.js file, under ally object, add the following key, if it does not exist.

[...]
facebook: {
      clientId: Env.get('FB_CLIENT_ID'),
      clientSecret: Env.get('FB_CLIENT_SECRET'),
      redirectUri: `${Env.get('APP_URL')}/authenticated/facebook`
  }
[...]

Set the keys obtained for the app in your .env file

FB_CLIENT_ID=<APP ID>
FB_CLIENT_SECRET=<APP Secret>

One more thing to do for Facebook -- on the sidenav, select Products and Add Product, then select Facbook Login. Fill Valid OAuth redirect URIs which is http://localhost:3333/authenticated/facebook in our case

Next, head to twitter, log into your twitter account and register an application here. Set callback URL to http://localhost:3333/authenticated/twitter.

When the application is created, click on the keys and access token tab to obtain your Consumer API Key and Consumer API secret

Once the app has been created click on the keys and access tokens tab to get your Consumer API Key(client id) and Consumer API secret(client secret).

Update config\services.js file, under ally object, add the following key, if it does not exist.

[...]
twitter: {
      clientId: Env.get('TWITTER_CLIENT_ID'),
      clientSecret: Env.get('TWITTER_CLIENT_SECRET'),
      redirectUri: `${Env.get('APP_URL')}/authenticated/twitter`
    }
[...]

Set the keys obtained for the app in your .env file

TWITTER_CLIENT_ID=<Consumer API Key>
TWITTER_CLIENT_SECRET=<Consumer API secret>

Application Views

It's time to create some views for our application. Go to "resources/views" directory then we replace the content of welcome.edge file

welcome.edge
@layout('master')

@section('content')
<div class="container" style="margin-top: 160px">
    <div class="row">
        <div class="col-md-1"></div>
        <div class="col-md-10">
            <div class="card">
                @loggedIn
                <div class="card-header">User Information</div>
                <div class="card-body">
                    <div class="container">
                        <div class="row justify-content-md-center">
                            <div class="col col-md-12">
                                <img src="{{ auth.user.avatar }}">
                                <h2>{{ auth.user.name }}</h2>
                                <h5>{{ auth.user.provider }}</h5>
                            </div>
                        </div>
                    </div>
                    <br>
                </div>
                @else
                <div class="card-header">Authentication</div>
                <div class="card-body">
                    <div class="container">
                        <div class="row justify-content-md-center">
                            <div class="col col-md-6">
                                <a href="{{ route('social.login', {provider: 'facebook'}) }}"
                                   class="btn btn-block btn-facebook btn-social" role="button">
                                    <i class="fa fa-facebook"></i>
                                    Login With Facebook
                                </a>
                                <a href="{{ route('social.login', {provider: 'twitter'}) }}"
                                   class="btn btn-block btn-twitter btn-social">
                                    <i class="fa fa-twitter"></i>
                                    Sign in with Twitter
                                </a>
                            </div>
                        </div>
                    </div>
                    <br>
                </div>
                @endloggedIn
        </div>
    </div>
</div>

@endsection

Also, create a file called master.edge file

master.edge

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="description" content="AdonisJs Social">
    <meta name="author" content="">
    <title>AdonisJs Social</title>

    <!-- Fonts -->
    {{ css('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css') }}
    {{ css('https://fonts.googleapis.com/css?family=Lato:100,300,400,700') }}

    <!-- Styles -->
    {{ css('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-beta/css/bootstrap.min.css') }}
    {{ css('https://cdnjs.cloudflare.com/ajax/libs/bootstrap-social/5.1.1/bootstrap-social.min.css') }}

    {{ css('style.css') }}
</head>

<body id="app-layout">

<nav class="navbar navbar-expand-md navbar-dark fixed-top">
    <a class="navbar-brand" href="{{ route('welcomePage') }}"><i class="fa fa-cube"></i> AdonisJS</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarCollapse">
        <ul class="navbar-nav mr-auto">
            <li class="nav-item">
                <a class="nav-link {{ url == route('welcomePage') ? 'active' : '' }}" href="{{ route('welcomePage') }}">HOME</a>
            </li>
        </ul>
        <!-- Right Side Of Navbar -->
        <ul class="navbar-nav navbar-right">
            <!-- Authentication Links -->
            @loggedIn
            <li class="nav-item dropdown">
                <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                    <img src="{{ auth.user.toJSON().avatar }}" style="width: 1.9rem; height: 1.9rem; margin-right: 0.5rem" class="rounded-circle">
                    {{ auth.user.name }} <span class="caret"></span>
                </a>
                <div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
                    <a class="dropdown-item" href="{{ route('logout') }}"><i class="fa fa-btn fa-sign-out"></i> Logout</a>
                </div>
            </li>
            @endloggedIn
        </ul>
    </div>
</nav>

@!section('content')

<!-- JavaScripts -->
{{ script('https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.slim.min.js') }}
{{ script('https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js') }}
{{ script('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-beta/js/bootstrap.min.js') }}

</body>
</html>

Refresh your browser, your home page should look like this.

Creating Adonis Routes

Let's take care of our application's route in this section. Go to "start/routes.js" file and replace the content with:

start/routes.js

'use strict'

const Route = use('Route')

Route.on('/').render('welcome')
Route.get('/auth/:provider', 'AuthController.redirectToProvider').as('social.login')
Route.get('/authenticated/:provider', 'AuthController.handleProviderCallback').as('social.login.callback')
Route.get('/logout', 'AuthController.logout').as('logout')

If you noticed, I added 3 more routes to the existing ones. One for redirecting the user to the OAuth provider(facebook and twitter in our case), another one for receiving the callback from the provider after authentication and last one for logout.

Controllers

We are going to create a controller for our application. In our case,we will call it AuthController. Run the below command

adonis make:controller AuthController

It will ask you "Generating a controller for?". Select Http Request,

Check your app/Controllers/Http/ directory, you should see a controller called AuthController.js has been created.

AuthController.js
'use strict'

const User = use('App/Models/User')

class AuthController {

  async redirectToProvider ({ally, params}) {
    await ally.driver(params.provider).redirect()
  }

  async handleProviderCallback ({params, ally, auth, response}) {
    const provider = params.provider
    try {
      const userData = await ally.driver(params.provider).getUser()

      const authUser = await User.query().where({
        'provider': provider,
        'provider_id': userData.getId()
      }).first()
      if (!(authUser === null)) {
        await auth.loginViaId(authUser.id)
        return response.redirect('/')
      }

      const user = new User()
      user.name = userData.getName()
      user.username = userData.getNickname()
      user.email = userData.getEmail()
      user.provider_id = userData.getId()
      user.avatar = userData.getAvatar()
      user.provider = provider

      await user.save()

      await auth.loginViaId(user.id)
      return response.redirect('/')
    } catch (e) {
      console.log(e)
      response.redirect('/auth/' + provider)
    }
  }

  async logout ({auth, response}) {
    await auth.logout()
    response.redirect('/')

  }
}
module.exports = AuthController

Let's talk about the functions in the controller;

  • redirectToProvider handles redirecting the user to the OAuth provider(Facebook and Twitter).
  • handleProviderCallback handles retrieve the user's information from the provider(Facebook and Twitter). In this method, we checked if the user already exist in the database. If so, we return the user's information. Otherwise, create a new user. This concept prevents a user account from being created twice.

Below is view when you login via facebook and twitter

Conclusion

Now, you can authenticate user via facebook and twitter in your new AdonisJS Application. In my next post, we will work on authentication via google and github.

Let me mention this, I created an Hackathon Starter in AdonisJS those of you that want to explore AdonisJS. The source code is Here

If you have any questions or observations, feel free to drop it in the comments section below. I would be happy to respond to you.