Wednesday, 20 December, 2017 UTC


Summary

The importance of security and efficient authentication in modern web apps cannot be overly emphasized. From my previous post, Using Facebook/Twitter Authentication in Adonis 4.0. Effective user authentication via Facebook and twitter was discussed and implemented. In this article, we will be talking about authentication using Google, Github and Instagram accounts. An Adonis package Adonis-ally will be employed to achieve this seamlessly.

Adonis-ally

The Adonis-ally package is an awesome package that makes it easier to authenticate user via Facebook, Twitter, Google, Linkedin, Instagram, Foursquare and Github. It makes it seamless to implement social authentication and also, more secure.

Prerequisites

To get started, you need knowledge of Node.js and JavaScript. The following must be installed on your machine:
  • Node,JS
  • NPM(Bundled with Node.js installer)

Setup an Adonis Project

Open your terminal and type this command.
# if you don't have Adonis CLI intalled on your machine.
$ npm i -g @adonisjs/cli

# Create a new adonis app and move into the app directory
$ adonis new adonis-social-ii && cd adonis-social-ii
Start the server and test if it's working:
$ adonis serve --dev
2017-10-18T09:09:16.649Z - 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 the following.
Let's install adonis-ally package via adonis CLI
adonis install @adonisjs/ally
Register adonis-ally provider to the application inside start/app.js file.
const providers = [
  //...
    '@adonisjs/ally/providers/AllyProvider'
  //...
]

Database Setup

I will be using MySQL for this tutorial. 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

Since Adonis installation comes with pre-installed migration and model files for user, We will modify the existing user model and migration file before we migrate it.
Go to "database/migrations" directory, delete _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
To migrate our table,let's install mysql module
$ npm install --save mysql
Let's go ahead with the migration.
adonis migration:run
Check your database, users table is created.
Let's go to app/Models directory, delete Token.js file then edit 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('beforeSave', 'User.hashPassword')
  }

    static get table () {
        return 'users'
    }

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

module.exports = User

Obtaining Client id and secret.

We are going obtain Google/Github/Instagram client id and secret in this section.

Google

  • Visit Google Cloud Console and create new project there
  • Select APIs and services on the sidebar
  • Click on Enable APIs and service
  • Select Google+ API under social tab and enable it
  • Next, Click on Credentials tab by the left
  • Select OAuth client id from create Credentials dropdown
  • Select web application, click on configure consent screen
  • Fill out the required fields then click on Save
  • Select web application, Fill out the required fields then click on Save
  • Name : Adonis login
  • Authorized Javascript origins: http://localhost:3333
  • Authorized redirect URI: http://localhost:3333/authenticated/google
  • copy the client id and secret into .env
GOOGLE_CLIENT_ID=xxxxxxxx
GOOGLE_CLIENT_SECRET=xxxxxxxx

Github

  • Visit Developer Setting
  • click on New OAuth App
  • Enter Application Name and Homepage URL
  • For Authorization callback URL: http://localhost:3333/authenticated/github.
  • Click Register application
  • Copy the client id and secret into .env
GITHUB_CLIENT_ID=xxxxxxxx
GITHUB_CLIENT_SECRET=xxxxxxxx

Instagram

  • Visit Manage Clients
  • click on Register a New Client.
  • Enter Application Name, description, Contact Email and website URL.
  • For Valid redirect URIs: http://localhost:3333/authenticated/instagram
  • Don't forget to complete captcha challenge.
  • Copy the client id and secret into .env
INSTAGRAM_CLIENT_ID=xxxxxx
INSTAGRAM_CLIENT_SECRET=xxxxxx
Now,we have our client id and secret. Let's update config/service.js file, under ally object, add the following keys if it does not exist.
[...]
google: {
  clientId: Env.get('GOOGLE_CLIENT_ID'),
  clientSecret: Env.get('GOOGLE_CLIENT_SECRET'),
  redirectUri: `${Env.get('APP_URL')}/authenticated/google`
},
github: {
  clientId: Env.get('GITHUB_CLIENT_ID'),
  clientSecret: Env.get('GITHUB_CLIENT_SECRET'),
  redirectUri: `${Env.get('APP_URL')}/authenticated/github`
},
instagram: {
  clientId: Env.get('INSTAGRAM_CLIENT_ID'),
  clientSecret: Env.get('INSTAGRAM_CLIENT_SECRET'),
  redirectUri: `${Env.get('APP_URL')}/authenticated/instagram`
}
[...]

Application Views

In this section, we will be creating some views for our application. Go to "resource/views" directory then create file called master.edge

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> Adonis</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>
Also, 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: 'google'}) }}"
                                   class="btn btn-block btn-google btn-social">
                                    <i class="fa fa-google-plus"></i>Login with Google
                                </a>
                                <a href="{{ route('social.login', { provider: 'github' }) }}"
                                   class="btn btn-block btn-github btn-social">
                                    <i class="fa fa-github"></i>Login with GitHub
                                </a>
                                <a href="{{ route('social.login', {provider: 'instagram'}) }}"
                                   class="btn btn-block btn-instagram btn-social">
                                    <i class="fa fa-instagram"></i>Login with Instagram
                                </a>
                            </div>
                        </div>
                    </div>
                    <br>
                </div>
                @endloggedIn
            </div>
        </div>
    </div>
</div>
@endsection
Refresh your browser, your home page should look like this.

Routes

Time to take care of our application's route. Go to "start/routes.js" and replace the content with:

routes.js

'use strict'

const Route = use('Route')

Route.on('/').render('welcome')
Route.get('/logout', 'AuthController.logout').as('logout')
Route.get('/auth/:provider', 'AuthController.redirectToProvider').as('social.login')
Route.get('/authenticated/:provider', 'AuthController.handleProviderCallback').as('social.login.callback')
Three routes was added to the existing ones. One for redirecting the user to the OAuth provider(Google, Github and Instagram in our case), another one for receiving the callback from the provider after authentication and last one for logout.

Controllers

Let's create a controller for the application. we are going to call it AuthController. Run the below command to create this controller.
adonis make:controller AuthController
It will ask you Generating a controller for ? select Http Request
Go to app/Controllers/Http/ directory, you will discover 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
Before we test our code, let's talk about the functions in the controller.
  • redirectToProvider handles redirecting of the user to the OAuth provider(Google, Github and Instagram).
  • handleProviderCallback handles retrieve the user's information from the provider(Google, Github and Instagram). 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 prevent user account to be created twice.
    Note: when you test the application with google chrome,it might not log you in because of many re-direct that occurs in the background.To fix this, you have to set sameSite: false inside config/session.js. But for other browsers, you should be fine.
The view when i login via google, github and instagram.
Github Google Instagram

Conclusion

Your application now provide users the option of using different social login providers to securely gain access to your service. No restrictions. No boundaries.
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.