How To Add Custom API Authentication To Laravel

add custom api authenticationSometimes the default authentication system is not enough. When that happened to us with Laravel, here’s how we added a custom API authentication:

1. Install The Necessary Packages

These overrides require two packages that are not in the default Laravel installation: a better caching system for login throttling and a PHP HTTP client to send HTTP requests to the desired API.

  1. Cache – Redis: https://laravel.com/docs/5.2/redis
  2. HTTP Client – Guzzle: https://github.com/guzzle/guzzle

Bonus – Set up a helper class to wrap RESTful API calls.

2. Override The Auth Routes

The first step to implementing custom authentication is to override the default routes in the routes file generated by Laravel’s Auth::routes(). The routes generated by the function are:

/* Original Routes */
// Authentication Routes...
Route::get('login', 'Auth\AuthController@showLoginForm');
Route::post('login', 'Auth\AuthController@login');
Route::get('logout', 'Auth\AuthController@logout');

// Registration Routes...
Route::get('register', 'Auth\AuthController@showRegistrationForm');
Route::post('register', 'Auth\AuthController@register');

// Password Reset Routes...
Route::get('password/reset/{token?}', 'Auth\PasswordController@showResetForm');
Route::post('password/email', 'Auth\PasswordController@sendResetLinkEmail');
Route::post('password/reset', 'Auth\PasswordController@reset');

Depending on the extent of the authentication API being used, each of these routes can be overwritten. Generally, if an API has authentication endpoints, it will have registration and password reset endpoints as well. Replace each of the authentication routes to point to custom authentication controllers as seen below. This requires three new controllers: CustomAuthController, CustomRegistrationController, and CustomPasswordController.

 

/* Custom Routes */

// Authentication Routes...
Route::get('login', 'Auth\CustomAuthController@showLoginForm');
Route::post('login', 'Auth\CustomAuthController@login');
Route::get('logout', 'Auth\CustomAuthController@logout');

// Registration Routes...
Route::get('register', 'Auth\CustomRegistrationController@showRegistrationForm');
Route::post('register', 'Auth\CustomRegistrationController@register');

// Password Reset Routes...
Route::get('password/reset/{token?}', 'Auth\CustomPasswordController@showResetForm');
Route::post('password/email', 'Auth\CustomPasswordController@sendResetLinkEmail');
Route::post('password/reset', 'Auth\CustomPasswordController@reset');

3. Create Custom Auth Controllers

CustomAuthController.php

The custom authentication controller needs to have three main components:

  1. An index function that shows the login form
  2. An authenticate function that processes the login form
  3. Login throttling to prevent too many login attempts
<?php
namespace App\Http\Controllers;

use App\Helpers\HttpHelper;
use App\Traits\Throttles;
use Exception;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;

class CustomAuthController extends Controller
{
    //implement App\Traits\Throttles;
    use Throttles;

    private $httpHelper;
    /**
     * CustomAuthController constructor.
     */
    public function __construct() {
        //initialize HttpHelper
        $this->httpHelper = new HttpHelper();
    }

    /**
     * Show the main login page
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
     */
    public function showLoginForm() {
        return view("auth.login");
    }
    /**
     * Authenticate against the  API
     * @param AuthenticationRequest $request
     * @return \Illuminate\Http\RedirectResponse
     */
     public function authenticate(AuthenticationRequest $request) {
        //too many failed login attempts
       if ($this->getThrottleValue("login", $this->generateLoginThrottleHash($request)) > 10) {
            return redirect()->back()->with('error', 'Too many failed login attempts.');
        }
        
        //attempt API authentication
        try {   
            $result = $this->httpHelper->post("authenticate", [
                'username' => $request->email, 
                'password' => $request->password
            ]);

            //create user to store in session
            $user = new User();
            /* Set any  user specific fields returned by the api request*/
            $user->email = $request->email;
            $user->field_1 = $result->field_1;
            //..

            //store authenticated and user in session to be checked by authentication middleware
            $request->session()->put('authenticated',true);
            $request->session()->put('user', $user);
        } catch(\GuzzleHttp\Exception\ClientException $e) {
            //track login attempt
            $this->incrementThrottleValue("login", $this->generateLoginThrottleHash($request));

            //remove user and authenticated from session
            $request->session()->forget('authenticated');
            $request->session()->forget('user');
            //redirect back with error
            return redirect()->back()->with('error', 'The credentials do not match our records');
        }
        //login success - redirect to home page
        $this->resetThrottleValue("login", $this->generateLoginThrottleHash($request));
        return redirect()->action("HomeController@home");
    }
    
    /**
     * Log user out
     * @param Request $request 
     * @return type
     */
    public function logout(Request $request) {
        //remove authenticated from session and redirect to login
        $request->session()->forget('authenticated');
        $request->session()->forget('user');
        return redirect()->action("CustomAuthController@showLoginForm");
    }


    
    // Login throttling functions
     
    /**
     * @param AuthenticationRequest $request
     * @return string
     */
    private function generateLoginThrottleHash(AuthenticationRequest $request) {
        return md5($request->email . "_" . $request->getClientIp());
    }
}

Throttles.php

<?php

namespace App\Traits;

use Illuminate\Support\Facades\Cache;

trait Throttles
{
    /**
     * Get the current throttle value
     * @param $throttleName
     * @param $tag
     * @return mixed
     */
    protected function getThrottleValue($throttleName, $tag)
    {
        return Cache::tags("$throttleName.throttle")->get($tag);
    }

    /**
     * Increment the throttle value
     * @param $throttleName
     * @param $tag
     */
    protected function incrementThrottleValue($throttleName, $tag)
    {
        Cache::tags("$throttleName.throttle")->put(
            $tag,
            (int)Cache::tags("$throttleName.throttle")->get($tag)+1,
            2
        );
    }

    /**
     * Reset the throttle value
     * @param $throttleName
     * @param $tag
     */
    protected function resetThrottleValue($throttleName, $tag)
    {
        Cache::tags("$throttleName.throttle")->forget($tag);
    }
}

 

CustomRegistrationController.php

  1. Show registration form
  2. Process registration form
<?php
namespace App\Http\Controllers;

use App\Helpers\HttpHelper;
use Illuminate\Http\Request;

class CustomRegistrationController extends Controller
{
    private $httpHelper;
    /**
     * CustomRegistrationController constructor.
     */
    public function __construct() {
        //initialize HttpHelper
        $this->httpHelper = new HttpHelper();
    }

    /**
     * Show registration form
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
     */
    public function showRegistrationForm() {
        return view("auth.register");
    }

    //
    public function register(Request $request) {
       try {   
            $result = $this->httpHelper->post("register", [
              //insert required registration fields
            ]);

           
        } catch(\GuzzleHttp\Exception\ClientException $e) {
          //return back with errors
        }

        //return to login page after registration
        return redirect('/login');
    }
    
}

CustomPasswordController.php

  1. Password reset link
  2. Send reset email
  3. Process reset email
  4. Password reset form
<?php
namespace App\Http\Controllers;

use App\Helpers\HttpHelper;
use Illuminate\Http\Request;


class CustomPasswordController extends Controller
{
    private $httpHelper;
    /**
     * CustomRegistrationController constructor.
     */
    public function __construct() {
        //initialize HttpHelper
        $this->httpHelper = new HttpHelper();
    }

    /**
     * Show password reset form
     * @return type
     */
    public function showResetForm() {
        return view('auth/password-reset');
    }

    /**
     * Send reset password email
     * @return type
     */
    public function sendResetLinkEmail(Request $request) {
        //get password reset token from api
       try {   
            $result = $this->httpHelper->post("reset-password-token", [
              //insert required password reset fields
            ]); 
        } catch(\GuzzleHttp\Exception\ClientException $e) {
          //return back with errors
        }

        //send password reset email with token from api
        $data = array( 
            'email' => $request->email,
            'token' => $result->token
        );
        Mail::send('auth/emails.password',$data, function($message) use ($data) {
            $message->from('support@website.com');
            $message->to($data['email']);
            $message->subject('Password Reset');
        });

        $request->session()->forget('authenticated');
        $request->session()->forget('user');

        //return success message
        return redirect()->back()->with('success', 'Please check your email to continue');
    }

    public function reset(Request $request) {
        try {   
            $result = $this->httpHelper->post("reset-password", [
              'token' => $request->token,
              'passowrd' => $request->password
            ]); 
        } catch(\GuzzleHttp\Exception\ClientException $e) {
          //return back with errors
        }

        //redirect to login with success message
        return redirect('/login')->with('success', 'Your password has been reset.');
    }
    
}

AuthenticationRequest.php

<?php

namespace App\Http\Requests;

use App\Http\Requests\Request;

class AuthenticationRequest extends Request
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'email' => 'required|email',
            'password' => 'required|string',
        ];
    }
}

 

4. Create Custom Auth Middleware

The final step in overriding Laravel’s authentication is to set up custom authentication middleware. Create a new file App/Http/Authenticate.php. This file will check every request throughout the application.

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Auth;

class Authenticate
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string|null  $guard
     * @return mixed
     */
    public function handle($request, Closure $next, $guard = null)
    {
       // if the session does not have 'authenticated' forget the user and redirect to login
       if ($request->session()->get('authenticated',false) === true)
        {
            return $next($request);
        }
        $request->session()->forget('authenticated');
        $request->session()->forget('user');
        return redirect()->action("CustomAuthController@showLoginForm")->with('error', 'Your session has expired.');

    }
}

A few things to keep in mind are:

  1. To retrieve the authenticated user, replace Auth::user() with$request>session()>get(‘user’);
  2. Set expiration times on your session
  3. Some APIs may not have a password reset functionality, so reset tokens need to be generated and stored manually.

That’s it! Your app will now use API endpoints for authentication.

15 responses to “How To Add Custom API Authentication To Laravel”

    • Laravel Passport is used to make an Auth server. This article is not about that, it’s about authenticating to a third party server that is not using oAuth.

  1. Hello,
    I’ve searched for days and read so many tutorials and stackoverflow answers but your article is what came the closest to what I’ve been trying to do, so thank you.
    Now, I’m a complete newbie here, so excuse me if the following question is too silly: how should I “mark” the routes that are to be available only through authentication? (I was using ->middleware(‘auth’); but that’s not working with this approach… what am I missing?
    Thanks again 🙂

    • Hi!

      After spending a bit of time thinking about it, I’m not 100% sure I’m reading the question correctly…could you give us a better idea of what you’re working with? What’s above is for authenticating a login against a third-party system…if memory serves, we did this as part of a “customer portal” for a CRM that didn’t do customer logins (only company employee logins) natively.

      If all you need is to authenticate within your system, you wouldn’t need to jump through some of these hoops. Either way, please let me know how we can help?

  2. Thank you for publishing this. It is extremely helpful and much appreciated!
    I am having problems with the throttles … there is no example of the App/Traits/Throttles.php file, where I assume the methods are written out. I have, perhaps, missed something obvious, but, I am stuck. Any help would be greatly appreciated.
    k

  3. Hi, I have followed you tutorial and I love it.
    I encountered an issue with a missing class “AuthenticationRequest” which was passed as an argument to the generateLoginThrottleHash method.
    Kindly provide the class

  4. hi, this is exactly what I’m looking for, yet it’s failing for me and all I need is the CustomAuthController. You have the namespace as Controller yet in the routes you call it ‘Auth\Custom…’
    I noticed AuthController is part of 5.2 and below and new version use LoginController… any advice would help (what threw me off was your reference to 5.4 Redis install docs).

    • Sorry for the confusion. You can change the namespace & make sure the authentication controllers are all in app/Http/Controllers/Auth, or change the routes to match.

  5. Hi, thanks for your article, it gave me an idea of how to implement my system.
    However the @guest directive in app.blade.php doesn’t recognize the session and shows the guest navigation (with the possibility to Login or Register) instead of the Name of the user.

    • Hi Andrea, from this example, you’d want to check the session eg $request->session()->get(‘authenticated’) or write new directives.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.