How Do We Implement Custom API-Only Authentication in Laravel

How do we implement custom API-only authentication in Laravel

The solution involves seven PHP files

  • app/Http/Controllers/HomeController.php - homepage controller; the destination for an authenticated user
  • app/Providers/ApiUserProvider.php - a custom provider to bootstrap and register the logged-in user, and implements the interface Illuminate\Contracts\Auth\UserProvider
  • app/CoreExtensions/SessionGuardExtended.php - custom guard-controller to log-in the user and receives the authentication values and stores them in session array; extends class Illuminate\Auth\SessionGuard
  • app/ApiUser - if you're using OAuth2 (Laravel's Passport); custom user class that exposes the OAuth access_token; extends Illuminate\Auth\GenericUser and implements the interface Illuminate\Contracts\Auth\Authenticatable
  • config/auth.php - the auth config which instructs the Auth() facade to return the custom session guard
  • app/Providers/AuthServiceProvider.php - the auth bootstrap
  • app/Providers/AppServiceProvider.php - the main application bootstrap

Source research/investigation material are cited for you to investigate for yourself and comprehend the background context to their existence. I make no claims to be a genius who created the solution from scratch through my own mojo, but rather that - like all innovators - I build on the efforts of others. The unique selling point of my article is that I provide a complete packaged solution, whereas the cited sources provide solutions to niche parts of the overall answer. Together, after much trial and error, they helped me to form a complete solution.

A really useful article to understands how config/auth.php affects execution in AuthManager.php is https://www.2hatslogic.com/blog/laravel-custom-authentication/

No code modifications are made to the following, but they're included to acknowledge the role they play and their importance in the process:

  • vendor/laravel/framework/src/Illuminate/Auth/AuthManager.php - main authorization factory manager
  • Auth() facade - returns the shrink-wrapped Illuminate\Auth\SessionGuard class instance by default, unless it's instructed to do otherwise through the config/auth.php file - Auth() is used ubiquitously throughout Laravel code to retrieve the session guard

The Code

app/Http/Controllers/HomeController.php

<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;

/**
* Handles and manages the home-page
*
* @category controllers
*/
class HomeController extends Controller
{
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth');
}

public function index()
{
blah
}

... other methods ...

}

app/Providers/ApiUserProvider.php

Sources:

  • Using Laravel 5.8 authentication with external JSON API (Creating own ServiceProvider)
  • https://laracasts.com/discuss/channels/laravel/replacing-the-laravel-authentication-with-a-custom-authentication
  • Custom user authentication base on the response of an API call
<?php
namespace App\Providers;

use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;
use App\ApiUser;

/**
* Delegates API user login and authentication
*
* @category providers
*/
class ApiUserProvider implements UserProvider
{

/**
* Custom API Handler
* Used to request API and capture responses
*
* @var \Path\To\Your\Internal\Api\Handler
*/
private $_oApi = null;

/**
* POST request to API
*
* @param string $p_url Endpoint URL
* @param array $p_arrParam Parameters
* @param boolean $p_isOAuth2 Is OAuth2 authenticated request? [Optional, Default=True]
*
* @return array
*/
private function _post(string $p_url, array $p_arrParam, bool $p_isOAuth2=true)
{
if (!$this->_oApi) {
$this->_oApi = new \Path\To\Your\Internal\Api\Handler();
}
$arrResponse = $this->_oApi->post($p_url, $p_arrParam, $p_isOAuth2);
return $arrResponse;
}

/**
* GET request to API
*
* @param string $p_url Endpoint URL
* @param array $p_arrParam Parameters [Optional, Default = array()]
*
* @return array
*/
private function _get(string $p_url, array $p_arrParam=[], bool $p_isOAuth2=true)
{
if (!$this->_oApi) {
$this->_oApi = new \Path\To\Your\Internal\Api\Handler();
}
$arrResponse = $this->_oApi->get($p_url, $p_arrParam);
return $arrResponse;
}

/**
* Retrieve a user by the given credentials.
*
* @param array $p_arrCredentials
*
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByCredentials(array $p_arrCredentials)
{
$arrResponse = $this->_post('/login', $p_arrCredentials, false);
if ( $arrResponse['result'] ) {
$arrPayload = array_merge(
$arrResponse['data'],
$p_arrCredentials
);
return $this->getApiUser($arrPayload);
}
}

/**
* Retrieve a user by their unique identifier.
*
* @param mixed $p_id
*
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveById($p_id)
{
$arrResponse = $this->_get("user/id/{$p_id}");
if ( $arrResponse['result'] ) {
return $this->getApiUser($arrResponse['data']);
}
}

/**
* Validate a user against the given credentials.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $p_oUser
* @param array $p_arrCredentials
*
* @return bool
*/
public function validateCredentials(UserContract $p_oUser, array $p_arrCredentials)
{
return $p_oUser->getAuthPassword() == $p_arrCredentials['password'];
}

/**
* Get the api user.
*
* @param mixed $p_user
*
* @return \App\Auth\ApiUser|null
*/
protected function getApiUser($p_user)
{
if ($p_user !== null) {
return new ApiUser($p_user);
}
return null;
}

protected function getUserById($id)
{
$user = [];

foreach ($this->getUsers() as $item) {
if ($item['account_id'] == $id) {
$user = $item;

break;
}
}

return $user ?: null;
}

protected function getUserByUsername($username)
{
$user = [];

foreach ($this->getUsers() as $item) {
if ($item['email_address'] == $username) {
$user = $item;

break;
}
}

return $user ?: null;
}


/**
* The methods below need to be defined because of the Authenticatable contract
* but need no implementation for 'Auth::attempt' to work and can be implemented
* if you need their functionality
*/
public function retrieveByToken($identifier, $token) { }
public function updateRememberToken(UserContract $user, $token) { }

}

app/CoreExtensions/SessionGuardExtended.php

Sources:

  • Extending Laravel 5.2 SessionGuard
  • Using Laravel 5.8 authentication with external JSON API (Creating own ServiceProvider)
<?php
namespace App\CoreExtensions;

use Illuminate\Auth\SessionGuard;
use Illuminate\Contracts\Auth\Authenticatable;

/**
* Extended SessionGuard() functionality
* Provides added functionality to store the OAuth tokens in the session for later use
*
* @category guards
*
* @see https://stackoverflow.com/questions/36087061/extending-laravel-5-2-sessionguard
*/
class SessionGuardExtended extends SessionGuard
{

/**
* Log a user into the application.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $p_oUser
* @param bool $p_remember
* @return void
*/
public function login(Authenticatable $p_oUser, $p_remember = false)
{

parent::login($p_oUser, $p_remember);

/**
* Writing the OAuth tokens to the session
*/
$key = 'authtokens';
$this->session->put(
$key,
[
'access_token' => $p_oUser->getAccessToken(),
'refresh_token' => $p_oUser->getRefreshToken(),
]
);
}

/**
* Log the user out of the application.
*
* @return void
*/
public function logout()
{
parent::logout();

/**
* Deleting the OAuth tokens from the session
*/
$this->session->forget('authtokens');
}

}

app/ApiUser

Sources:

  • Using Laravel 5.8 authentication with external JSON API (Creating own ServiceProvider)
    *https://laracasts.com/discuss/channels/laravel/replacing-the-laravel-authentication-with-a-custom-authentication
  • Custom user authentication base on the response of an API call
<?php
namespace App;

use Illuminate\Auth\GenericUser;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;

class ApiUser extends GenericUser implements UserContract
{

/**
* Returns the OAuth access_token
*
* @return mixed
*/
public function getAccessToken()
{
return $this->attributes['access_token'];
}


public function getRefreshToken()
{
return $this->attributes['refresh_token'];
}

}

app/Providers/AuthServiceProvider.php

<?php
namespace App\Providers;

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

class AuthServiceProvider extends ServiceProvider
{

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

Auth::provider('frank_sinatra', function ($app, array $config) {
// Return an instance of Illuminate\Contracts\Auth\UserProvider...

return new ApiUserProvider();
});

}
}

app/Providers/AppServiceProvider.php

Sources:

  • Extending Laravel 5.2 SessionGuard

Note:

There is a couple of nuanced issues regarding the change to coding in this PHP file.
If you want to understand more, look at vendor/laravel/framework/src/Illuminate/Auth/AuthManager.php, AuthManager::resolve() in particular.

  1. References to config/auth.php 'session' and 'token' are served by hard-coded methods AuthManager::createSessionDriver() and AuthManager::createTokenDriver()
    (Tell me please if you know of a way to extend AuthManager.php in the app)
  2. AppServiceProvider.php to the rescue! Custom guards can be registered in AppServiceProvider::boot() and intercepted before the default code can be executed.
  3. I'm OK with point 2 above, but couldn't we do something clever like return the custom session-guard name or instance from AppServiceProvider, have setCookieJar(), setDispatcher(), setRequest() in a specialized public method in AuthManager.php, which can be hooked into AppServiceProvider.php or driven by config/auth.php to execute after creating the custom session-guard in AuthManager.php?
  4. Without the cookies or sessions, the user's identity isn't preserved through the redirect. The only way to resolve this is to include the setCookieJar(), setDispatcher() and setRequest() in AppServiceProvider within our current solution.
<?php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Auth;
use App\CoreExtensions\SessionGuardExtended;

class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}

/**
* Bootstrap any application services.
*
* @see https://stackoverflow.com/questions/36087061/extending-laravel-5-2-sessionguard
*
* @return void
*/
public function boot()
{

/**
* Extending Illuminate\Auth\SessionGuard()
* This is so we can store the OAuth tokens in the session
*/
Auth::extend(
'sessionExtended',
function ($app) {

$guard = new SessionGuardExtended(
'sessionExtended',
new ApiUserProvider(),
app()->make('session.store'),
request()
);

// When using the remember me functionality of the authentication services we
// will need to be set the encryption instance of the guard, which allows
// secure, encrypted cookie values to get generated for those cookies.
if (method_exists($guard, 'setCookieJar')) {
$guard->setCookieJar($this->app['cookie']);
}

if (method_exists($guard, 'setDispatcher')) {
$guard->setDispatcher($this->app['events']);
}

if (method_exists($guard, 'setRequest')) {
$guard->setRequest($this->app->refresh('request', $guard, 'setRequest'));
}

return $guard;
}
);
}
}

config/auth.php

Sources:

  • https://www.2hatslogic.com/blog/laravel-custom-authentication/
<?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' => 'web', /** This refers to the settings under ['guards']['web'] */
'guard' => 'webextended', /** This refers to the settings under ['guards']['webextended'] */
'passwords' => 'users', /** This refers to the settings under ['passwords']['users'] */
],

/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| here which uses session storage and the Eloquent user provider.
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| Supported: "session", "token"
|
*/

'guards' => [
'web' => [
'driver' => 'session', /** This refers to Illuminate/Auth/SessionGuard */
'provider' => 'users', /** This refers to the settings under ['providers']['users'] */
],

'webextended' => [
'driver' => 'sessionExtended', /** @see app/Providers/AppServiceProvider::boot() */
'provider' => 'users', /** This refers to the settings under ['providers']['users'] */
],

'api' => [
'driver' => 'token', /** This refers to Illuminate/Auth/TokenGuard */
'provider' => 'users',
'hash' => false,
],
],

/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| If you have multiple user tables or models you may configure multiple
| sources which represent each model / table. These sources may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/

'providers' => [
'users' => [
'driver' => 'frank_sinatra', /** @see app/Providers/AuthServiceProvider::boot() */
//'model' => App\User::class,
],

// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],

[
blah
],

[
other settings
],

];

How To Use This Solution

Very simple. There's no change in the overall approach. In other words, we use the Auth() facade.

When logging in with your custom API /login?username=<username>&password=<password>

request()->flash();
$arrData = request()->all();

if ( Auth::attempt($arrData, true) ) {
return redirect('home');
} else {
return back()->withErrors(
[
'username' => "Those credentials can't be found",
'password' => "Those credentials can't be found",
]
);
}

When logging out with your custom API /logout

Auth::logout();
return redirect('home');

Custom user authentication base on the response of an API call

By following the steps below, you can setup your own authentication driver that handles fetching and validating the user credentials using your API call:

1. Create your own custom user provider in app/Auth/ApiUserProvider.php with the following contents:

namespace App\Auth;

use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;

class ApiUserProvider implements UserProvider
{
/**
* Retrieve a user by the given credentials.
*
* @param array $credentials
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByCredentials(array $credentials)
{
$user = $this->getUserByUsername($credentials['username']);

return $this->getApiUser($user);
}

/**
* Retrieve a user by their unique identifier.
*
* @param mixed $identifier
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveById($identifier)
{
$user = $this->getUserById($identifier);

return $this->getApiUser($user);
}

/**
* Validate a user against the given credentials.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param array $credentials
* @return bool
*/
public function validateCredentials(UserContract $user, array $credentials)
{
return $user->getAuthPassword() == $credentials['password'];
}

/**
* Get the api user.
*
* @param mixed $user
* @return \App\Auth\ApiUser|null
*/
protected function getApiUser($user)
{
if ($user !== null) {
return new ApiUser($user);
}
}

/**
* Get the use details from your API.
*
* @param string $username
* @return array|null
*/
protected function getUsers()
{
$ch = curl_init();

curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_URL, env('API_HOST') . 'vse/accounts');

$response = curl_exec($ch);
$response = json_decode($response, true);

curl_close($ch);

return $response['data'];
}

protected function getUserById($id)
{
$user = [];

foreach ($this->getUsers() as $item) {
if ($item['account_id'] == $id) {
$user = $item;

break;
}
}

return $user ?: null;
}

protected function getUserByUsername($username)
{
$user = [];

foreach ($this->getUsers() as $item) {
if ($item['email_address'] == $username) {
$user = $item;

break;
}
}

return $user ?: null;
}

// The methods below need to be defined because of the Authenticatable contract
// but need no implementation for 'Auth::attempt' to work and can be implemented
// if you need their functionality
public function retrieveByToken($identifier, $token) { }
public function updateRememberToken(UserContract $user, $token) { }
}

2. Also create a user class that extends the default GenericUser offered by the authentication system in app/Auth/ApiUser.php with the following contents:

namespace App\Auth;

use Illuminate\Auth\GenericUser;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;

class ApiUser extends GenericUser implements UserContract
{
public function getAuthIdentifier()
{
return $this->attributes['account_id'];
}
}

3. In your app/Providers/AuthServiceProvider.php file's boot method, register the new driver user provider:

public function boot(GateContract $gate)
{
$this->registerPolicies($gate);

// The code below sets up the 'api' driver
$this->app['auth']->extend('api', function() {
return new \App\Auth\ApiUserProvider();
});
}

4. Finally in your config/auth.php file set the driver to your custom one:

    'driver' => 'api',

You can now do the following in your controller action:

public function postSignIn()
{
$username = strtolower(Input::get('username'));
$password = Input::get('password');

if (Auth::attempt(['username' => $username, 'password' => $password])) {
return Redirect::to('/dashboard')->with('success', 'Hi '. $username .'! You have been successfully logged in.');
} else {
return Redirect::to('/')->with('error', 'Username/Password Wrong')->withInput(Request::except('password'))->with('username', $username);
}
}

Calling Auth::user() to get user details after a successful login, will return an ApiUser instance containing the attributes fetched from the remote API and would look something like this:

ApiUser {#143 ▼
#attributes: array:10 [▼
"DBA" => ""
"account_id" => 111
"account_type" => "admin"
"display_name" => "BobJ"
"email_address" => "bob@xyzcorp.com"
"first_name" => "Bob"
"last_name" => "Jones"
"password" => "abc"
"message" => "Success"
"status" => 200
]
}

Since you haven't posted a sample of the response that you get when there's no match in the API for the user email, I setup the condition in the getUserDetails method, to determine that there's no match and return null if the response doesn't contain a data property or if the data property is empty. You can change that condition according to your needs.


The code above was tested using a mocked response that returns the data structure you posted in your question and it works very well.

As a final note: you should strongly consider modifying the API to handle the user authentication sooner rather than later (perhaps using a Oauth implementation), because having the password sent over (and even more worryingly as plain text) is not something you want to postpone doing.

Is it possible to allow access to Laravel API only with one API key?

Yes, it is absolutely possible. We do not know your application requirements, however if you wish only to return some stuff and therefore you have no need for identifying which user did what, there is no reason to go with key per user as another answer suggested.

For example, you can put your key in .env

API_KEY=yourkey

And then, simply add middleware to api routes, which will first check, if there is a proper ApiKey passed within the request.

You can pass your env variable to any config file and then call config('yourConfigFile.apiKey') anywhere in your app.

For some simple scenarios this approach is 100% sufficient. Take into consideration who will have access to this key and how it will be passed. If there will be one global key, and your api consumer will simply use ajax without proxing it via server, your api key will be exposed and everyone will be able to grab it and use it anywhere.

If that is the case, you can still avoid creating key per user – just specify list of allowed domains which can call your API and then check key. It all can be checked within middleware.

Like I said, know your requirements/use cases and then make a decision :-)

Two Custom authentication api laravel with JWT

i have to edit config/auth to

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

'user' => [
'driver' => 'jwt',
'provider' => 'users',
],
'hospitalization' => [
'driver' => 'jwt',
'provider' => 'hospitalizations',
]
],

and use Auth::guard('hospitalization') when i need to separate



Related Topics



Leave a reply



Submit