Note: This post is based on Laravel 5.2 and has not been updated for newer versions!
Sometimes 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.
- Cache – Redis: https://laravel.com/docs/5.2/redis
- 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:
- An index function that shows the login form
- An authenticate function that processes the login form
- 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
- Show registration form
- 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
- Password reset link
- Send reset email
- Process reset email
- 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:
- To retrieve the authenticated user, replace Auth::user() with$request–>session()–>get(‘user’);
- Set expiration times on your session
- 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.