Laravel & JWT authentication & Creating custom authentication contracts

Lokendra Lodha
9 min readNov 7, 2020

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object.

Its’’s most preferred way to exchange the authentication information between mobile apps and server side.

Recently we have migrated a CakePHP project to Laravel. This migration also required to integrate the custom authentication as we already have custom users table and data.
Also we have an ionic app that requires the JWT authentication.

- In order to achieve these we have written the custom authentication providers based on Laravel authentication contracts.
- and we have used the "tymon/jwt-auth": "^1.0" package for JWT implementation

This tutorial explains:
- How to create a custom authentication provider for Laravel
- JWT authentication overview and integration
- A Sample curl commands , dart and ionic integration to call REST api with JWT tokens.


Laravel has a concept of user provider and authentication guard.
https://laravel.com/docs/8.x/authentication

1) We define a custom user class FrontendUser, Custom Security Guard and a Custom Authentication provider
2) and then we inject the guard and provider inside:
/config/auth.php
3) inject the auth middleware in your routes that requires authentications
4) and finally test login and few api!!

## Create Auth Provider
Add the following authentication provider in app/Providers directory


<?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;

use Illuminate\Support\Facades\Auth;


use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use App\Extensions\WizapetUserProvider;
use App\Extensions\WizapetFrontendUserProvider;
use App\Services\Auth\WizapetGuard;
use App\Models\Auth\User;
use App\Models\Auth\FrontendUser;

class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* @var array
*/
protected $policies = [
// 'App\Model' => 'App\Policies\ModelPolicy',
];

/**
* Register any authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();

$this->app->bind('App\Models\Auth\FrontendUser', function ($app) {
return new FrontendUser();
});

// add custom guard provider
Auth::provider('myorg_frontend_user_provider', function ($app, array $config) {
return new MyOrgFrontendUserProvider($app->make('App\Models\Auth\FrontendUser'));
});



// add custom guard
Auth::extend('my_guard', function ($app, $name, array $config) {
return new MyOrgGuard(Auth::createUserProvider($config['provider']), $app->make('request'));
});
}
}

## Create Auth Guard

Laravel will call the Guard method for authentications, and this guard than uses our custom auth provider and custom user model for authentication checks.

/app/Services/Auth/MyOrgGuard.php

<?php

namespace App\Services\Auth;

use Illuminate\Http\Request;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\UserProvider;
use GuzzleHttp\json_decode;
use phpDocumentor\Reflection\Types\Array_;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Auth\SessionGuard;
use Illuminate\Contracts\Session\Session;

class MyOrgGuard implements Guard{

protected $request;
protected $provider;
protected $user;

/**
* Create a new authentication guard.
*
* @param \Illuminate\Contracts\Auth\UserProvider $provider
* @param \Illuminate\Http\Request $request
* @return void
*/
public function __construct(UserProvider $provider, Request $request)
{

$this->request = $request;
$this->provider = $provider;
$this->user = NULL;
}


/**
* Determine if the user matches the credentials.
*
* @param mixed $user
* @param array $credentials
* @return bool
*/
protected function hasValidCredentials($user, $credentials)
{
$validated = ! is_null($user) && $this->provider->validateCredentials($user, $credentials);

if ($validated) {
//$this->fireValidatedEvent($user);
}

return $validated;
}

/**
* Attempt to authenticate a user using the given credentials.
*
* @param array $credentials
* @param bool $remember
* @return bool
*/
public function attempt(array $credentials = [], $remember = false)
{

\Illuminate\Support\Facades\Log::debug('MyOrgGuard:: attempt:');

$this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);


// If an implementation of UserInterface was returned, we'll ask the provider
// to validate the user against the given credentials, and if they are in
// fact valid we'll log the users into the application and return true.
if ($user && $this->hasValidCredentials($user, $credentials)) {
$this->user = $user;
return true;
}

// If the authentication attempt fails we will fire an event so that the user
// may be notified of any suspicious attempts to access their account from
// an unrecognized user. A developer may listen to this event as needed.
//$this->fireFailedEvent($user, $credentials);

return false;
}


/**
* Determine if the current user is authenticated.
*
* @return bool
*/
public function check()
{
return ! is_null($this->user());
}

/**
* Determine if the current user is a guest.
*
* @return bool
*/
public function guest()
{
return ! $this->check();
}

/**
* Get the currently authenticated user.
*
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function user()
{
if (! is_null($this->user)) {
return $this->user;
}
}

/**
* Get the JSON params from the current request
*
* @return string
*/
public function getJsonParams()
{
$jsondata = $this->request->query('jsondata');

return (!empty($jsondata) ? json_decode($jsondata, TRUE) : NULL);
}

/**
* Get the ID for the currently authenticated user.
*
* @return string|null
*/
public function id()
{
if ($user = $this->user()) {
return $this->user()->getAuthIdentifier();
}
}

/**
* Validate a user's credentials.
*
* @return bool
*/
public function validate(Array $credentials=[])
{


$user = $this->provider->retrieveByCredentials($credentials);

if (! is_null($user) && $this->provider->validateCredentials($user, $credentials)) {
$this->setUser($user);

return true;
} else {
return false;
}
}

/**
* Set the current user.
*
* @param Array $user User info
* @return void
*/
public function setUser(Authenticatable $user)
{
$this->user = $user;
return $this;
}
}

## Create custom user model based on Laravel contract: AuthenticatableContract

app/Models/Auth/FrontendUser.php

<?php
namespace App\Models\Auth;

use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use App\Users as UserModel;
use Tymon\JWTAuth\Contracts\JWTSubject;

class FrontendUser implements AuthenticatableContract, JWTSubject
{
private $conn;

public $id;
public $name;
public $username;
public $password;
public $remember_token;
protected $rememberTokenName = 'remember_token';

public function __construct()
{

}



/**
* Create a hash from string using given method or fallback on next available method.
*
* #### Using Blowfish
*
* - Creating Hashes: *Do not supply a salt*. Cake handles salt creation for
* you ensuring that each hashed password will have a *unique* salt.
* - Comparing Hashes: Simply pass the originally hashed password as the salt.
* The salt is prepended to the hash and php handles the parsing automagically.
* For convenience the `BlowfishPasswordHasher` class is available for use with
* the AuthComponent.
* - Do NOT use a constant salt for blowfish!
*
* Creating a blowfish/bcrypt hash:
*
* ```
* $hash = Security::hash($password, 'blowfish');
* ```
*
* @param string $string String to hash
* @param string $type Method to use (sha1/sha256/md5/blowfish)
* @param mixed $salt If true, automatically prepends the application's salt
* value to $string (Security.salt). If you are using blowfish the salt
* must be false or a previously generated salt.
* @return string Hash
* @link http://book.cakephp.org/2.0/en/core-utility-libraries/security.html#Security::hash
*

*/
public static function hash($string, $type = null, $salt = false) {
if (empty($type)) {
//$type = static::$hashType;
}
$type = strtolower($type);

if ($type === 'blowfish') {
return static::_crypt($string, $salt);
}
if ($salt) {
if (!is_string($salt)) {
$salt = env('SECURITY_SALT');//Configure::read('Security.salt');
}
$string = $salt . $string;
}

if (!$type || $type === 'sha1') {
if (function_exists('sha1')) {
return sha1($string);
}
$type = 'sha256';
}

if ($type === 'sha256' && function_exists('mhash')) {
return bin2hex(mhash(MHASH_SHA256, $string));
}

if (function_exists('hash')) {
return hash($type, $string);
}
return md5($string);
}

/**
* Sets the default hash method for the Security object. This affects all objects using
* Security::hash().
*
* @param string $hash Method to use (sha1/sha256/md5/blowfish)
* @return void
* @see Security::hash()
*/
public static function setHash($hash) {
static::$hashType = $hash;
}


/**
* Fetch user by Credentials
*
* @param array $credentials
* @return Illuminate\Contracts\Auth\Authenticatable
*/
public function fetchUserByCredentials(Array $credentials)
{


$user = UserModel::where(['username' => $credentials['username']])->first();
if ($user) {
$this->id = $user->id;
$this->name = $user->name;
$this->username = $user->username;
$this->password = $user->password;


}else{
\Illuminate\Support\Facades\Log::info('WU::fetchUserByCredentials:: user not found..',[$credentials['username']]);
}



return $this;
}

/**
* {@inheritDoc}
* @see \Illuminate\Contracts\Auth\Authenticatable::getAuthIdentifierName()
*/
public function getAuthIdentifierName()
{
return "username";
}

/**
* {@inheritDoc}
* @see \Illuminate\Contracts\Auth\Authenticatable::getAuthIdentifier()
*/
public function getAuthIdentifier()
{
//\Illuminate\Support\Facades\Log::debug('WU:: getAuthIdentifier$this->username'.$this->username);
return $this->{$this->getAuthIdentifierName()};
}

/**
* {@inheritDoc}
* @see \Illuminate\Contracts\Auth\Authenticatable::getAuthPassword()
*/
public function getAuthPassword()
{
return $this->password;
}

/**
* {@inheritDoc}
* @see \Illuminate\Contracts\Auth\Authenticatable::getRememberToken()
*/
public function getRememberToken()
{
if (! empty($this->getRememberTokenName())) {
return $this->{$this->getRememberTokenName()};
}
}

/**
* {@inheritDoc}
* @see \Illuminate\Contracts\Auth\Authenticatable::setRememberToken()
*/
public function setRememberToken($value)
{
if (! empty($this->getRememberTokenName())) {
$this->{$this->getRememberTokenName()} = $value;
}
}

/**
* {@inheritDoc}
* @see \Illuminate\Contracts\Auth\Authenticatable::getRememberTokenName()
*/
public function getRememberTokenName()
{
return $this->rememberTokenName;
}

public function getKey(){
return $this->username;
}

/**
* Get the identifier that will be stored in the subject claim of the JWT.
*
* @return mixed
*/
public function getJWTIdentifier()
{
return $this->getKey();
}

/**
* Return a key value array, containing any custom claims to be added to the JWT.
*
* @return array
*/
public function getJWTCustomClaims()
{
return [];
}
}

### define the auth settings for our classes
/config/auth.php

<?php

return [

/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option controls the default authentication "guard" and password
| reset options for your application. You may change these defaults
| as required, but they're a perfect start for most applications.
|
*/

'defaults' => [
'guard' => 'myorg_guard',
'passwords' => 'users',
],

'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],

'api2' => [
'driver' => 'token',
'provider' => 'users',
'hash' => false,
],

'myorg_guard' => [
'driver' => 'session',
'provider' => 'myorg_user_provider',
],

'api' => [
'driver' => 'jwt',
'provider' => 'myorg_frontend_user_provider',
],

],

## Install the "tymon/jwt-auth": "^1.0" package for JWT implementation

## set in your .env file
JWT_SECRET=*******

A helper command is provided for this:
| `php artisan jwt:secret`

## Laravel API routes to enable JWT authentication

Now inject the 'api' middleware (that we have defined in auth.php) for any routes that requires custom security:

/routes/api.php

example:


Route::group([

'middleware' => 'api',
'namespace' => '\App\Http\Controllers',
'prefix' => '1.0'

], function ($router) {
Route::any('/sample/mail.json', 'AuthController@sample_mail')->name('sample/mail');

Route::any('/users/login.json', 'AuthController@api_login')->name('api_login');
Route::any('/users/register_classic.json', 'AuthController@api_register_classic')->name('register_classic.json');
Route::any('/users/recovery_password.json', 'AuthController@api_recovery_password')->name('recovery_password.json');
Route::any('/users/logout.json', 'AuthController@logout')->name('logout.json');
Route::any('/users/get_notifications.json', 'NotificationsApiController@api_get_notifications')->name('get_notifications.json');
Route::any('/notifications/remove.json', 'NotificationsApiController@remove')->name('notificationsremove.json');

## Testing the authentication and authorization for API calls

Let's define login api
Route::any('/users/login.json', 'AuthController@api_login')->name('api_login');


The sample AuthController::api_login is the login implementation
Client app shall send a POST request with username/password
The api_login shall validate against the 'api' middleware and if login is success
we return back the user and JWT 'token'

Client app will than need to send this token as the HTTP Authorization header for every api calls that requires authentication
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Validator;
use App\Users;
use App\HtmlHelper;


use App\Notifications;
use App\Chats;
use App\Adverts;
use App\Payments;
use App\Countries;
use App\DeviceUsers;


class AuthController extends UsersController
{
/**
* Create a new AuthController instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth:api', ['except' => ['sample_mail','login','api_login','api_register_classic','api_recovery_password']]);
}

/**
* Get a JWT token via given credentials.
*
* @param \Illuminate\Http\Request $request
*
* @return \Illuminate\Http\JsonResponse
*/
public function login(Request $request)
{
$credentials = $request->only('username', 'password');

if ($token = $this->guard()->attempt($credentials)) {
return $this->respondWithToken($token);
}

return response()->json(['error' => 'Unauthorized'], 401);
}

/**
* Get the authenticated User
*
* @return \Illuminate\Http\JsonResponse
*/
public function me()
{
return response()->json($this->guard()->user());
}

/**
* Log the user out (Invalidate the token)
*
* @return \Illuminate\Http\JsonResponse
*/
public function logout()
{
$this->guard()->logout();

return response()->json(['message' => 'Successfully logged out']);
}

/**
* Refresh a token.
*
* @return \Illuminate\Http\JsonResponse
*/
public function refresh()
{
return $this->respondWithToken($this->guard()->refresh());
}

/**
* Get the token array structure.
*
* @param string $token
*
* @return \Illuminate\Http\JsonResponse
*/
protected function respondWithToken($token)
{
return response()->json([
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => $this->guard()->factory()->getTTL() * 60
]);
}

/**
* Get the guard to be used during authentication.
*
* @return \Illuminate\Contracts\Auth\Guard
*/
public function guard()
{
return Auth::guard('api');
}

public function api_login(Request $request) {

$requestdata = $request->all();
try {

if (isset($requestdata['password']) && $requestdata['password'] != '') {
if (filter_var($requestdata['username'], FILTER_VALIDATE_EMAIL)) {
$usernameFromEmailUsername = Users::where(
array('email' => $requestdata['username']))->first();
if ($usernameFromEmailUsername) {
$usernameFromEmailDB = $usernameFromEmailUsername->username;
} else {
$usernameFromEmailDB = '';
}
$requestdata['username'] = $usernameFromEmailDB;
}


if ($token = $this->guard()->attempt($requestdata)) {
$userSession = $this->guard()->user();
if ($userSession ) {
//$this->Session->destroy();
//$token = JWT::encode($user, env('SECURITY_SALT'));

$user = Users::where(['username'=>$userSession->getAuthIdentifier()])->first();

return response()->json(array(
'user' => $user,
'token'=>$token,
));
} else {
return response()->json(array(
'result' => 403,
));

}
} else {
return response()->json(array(
'result' => 400,
));
}
}

## Sample testcases

## Testcase 1: Client app Logins and obtain JWT token

sample valid test users::

2 user accounts added at via mobile app
adv2 / opac
adv5 / opac

#### successful login response
NOTE: the 'token' is returned as part of json response.
The client app must store this at there end and use this token for any api calls.

curl -X POST http://<server ip>/api/1.0/users/login.json \
> -H "Content-Type: application/json" \
> -d '{
> "username": "adv2",
> "password": "opac"
> }'
{"user":{"id":3,"name":"adv2","phone":"+919958202825","last_name":"adv2","email":"adv2@gmail.com","username":"adv2"
,"password":"8866a9ab0a7ca3c580be286c0e9072bd17702ec8","active":1,"facebook_id":null,"google_id":null,"twitter_id":null
.......}]
,"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9naXZlcnMub3BhY2xhYnMuY29tXC9hcGlcLzEuMFwvdXNlcnNcL2xvZ2luLmpzb24iLCJpYXQiOjE2MDQ3NTkzNTksIm5iZiI6MTYwNDc1OTM1OSwianRpIjoiNllVMzM1VDVNdml4UGU3RyIsInN1YiI6ImFkdjIiLCJwcnYiOiI1N2Y2YmUzZjg4ZmI5N2YzYmIzZDQyNTQ2OWJhY2ZjOWIzZTdiYmRhIn0.eJQxG1IsGQpsfd3JCwhLO_U0J5qIvDY2mppIbXFWmDo"}

## Testcase 2: Client app calls an api with JWT token
JWT token is now passed as per Authorization header:
'Authorization' : 'Bearer '+window.localStorage.getItem('token')


curl http://<server ip>/api/1.0/adverts/index.json \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9naXZlcnMub3BhY2xhYnMuY29tXC9hcGlcLzEuMFwvdXNlcnNcL2xvZ2luLmpzb24iLCJpYXQiOjE2MDQ3NTkzNTksIm5iZiI6MTYwNDc1OTM1OSwianRpIjoiNllVMzM1VDVNdml4UGU3RyIsInN1YiI6ImFkdjIiLCJwcnYiOiI1N2Y2YmUzZjg4ZmI5N2YzYmIzZDQyNTQ2OWJhY2ZjOWIzZTdiYmRhIn0.eJQxG1IsGQpsfd3JCwhLO_U0J5qIvDY2mppIbXFWmDo" \
-H "Content-Type: application/json"

Successful response:
{"result":0,"adverts":[{"id":224,"user_id":51,"advert_category_id":3,"title":"qq5 cat lost 5",......

--

--