Laravel 5 / AngularJS JWT Token Auth With Refresh
I decided to write a post about this topic because of all the trouble I had with it. There is tonnes of stuff out there about Laravel and Token Auth, but very little about how to go about refreshing the token between requests. So this is a full whack quick methodology about how to do JWT Auth with Laravel, how to tie this in with AngularJS, and then finally how to refresh the token on each request to ensure that the user can have a seamless experience.
If you don’t know what JWT / Token Auth is, then go here: https://jwt.io/introduction/
Before we start, as a disclaimer, I’m a self-taught PHP/JS programmer. With this in mind, if there are any suggestions to improve this module of my projects, then my ears are burning!
Also, I’m not planning on going into much deep detail. If you’ve read around the subject then you should be able to pick this up.
Let’s get started…
Laravel Side
Installing Token Auth
Let’s grab this package for Laravel:
“tymon/jwt-auth”: “0.6.*@dev”
You can find more information about this package here: https://github.com/tymondesigns/jwt-auth. It is worth noting that I am also pulling in the development branch of this module. At the time of writing (10/02/2016) branch 0.6 is still in development.
You will need to pull in this branch of this package however to complete this post.
Configuring Token Auth
- Install the JWT Package
You will have to add the service provider below to pull the package into your project.
Tymon\JWTAuth\Providers\LaravelServiceProvider::class,
2. Configure the JWT Package
You will need to run the following command in your terminal to publish the config file:
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\JWTAuthServiceProvider"
You will now find a file called ‘jwt’ in your config directory.
Important things to note in this file:
‘secret’: This is unique key you need to set in order to encode your tokens.
‘ttl’: This is time to live for your tokens. I.e. how long they are valid for.
‘refresh_ttl’: When your tokens become invalid, this is the time period where it is still allowed to reissue an invalidated token.
‘blacklist_enabled’: When tokens become invalid, or when you refresh a token, the old tokens will be placed on a blacklist. This means they cannot be used again, which is great for security.
‘blacklist_grace_period’: Invalidated tokens can have a grace period. This is really important for this application, when we’re running concurrent requests from AngularJS. Let me explain:
Say we have 3 going to the server for a page load. The first request will hit the server, and cause the token to be refreshed, and thus blacklisted. The 2nd and 3rd requests will then have the blacklisted token in their header and will throw a 401/Unauthorised error. By allowing a grace period, the token will still pass and the 3rd or last request will bring back the latest refreshed/updated token for the next set of requests.
This is how mine looks:
return [ // Other config settings… ‘ttl’ => env(‘JWT_TTL’, 60), ‘refresh_ttl’ => env(‘JWT_REFRESH_TTL’, 20160), ‘blacklist_enabled’ => env(‘JWT_BLACKLIST_ENABLED’, true), 'blacklist_grace_period’ => env(‘JWT_BLACKLIST_GRACE_PERIOD’, 60),]
3. Configure Laravel / Extending Laravel Auth
Go to the auth.php file in your config directory.
In the ‘guards’ section, you will need to either change your current guard (the one that says web), or create a new guard.
For my application, I’m allowing customers to log in via JWT and my admin users to log in via sessions. With this in mind, I have two guards called ‘employees’ and ‘customer_api’. This left me with this set up:
‘guards’ => [ ‘employees’ => [ ‘driver’ => ‘session’, ‘provider’ => ‘users’, ], ‘customer_api’ => [ ‘driver’ => ‘jwt’, ‘provider’ => ‘customers’, ]],
This part is also important becasue you will need to specify whichever guard configured here when accessing the different types of authenticated user. To access any customers, I will need to specify the ‘customer_api’ guard. To access my users, I will need to specify the ‘employees’ guard. We’ll see examples of this later.
Note that I’m also using the ‘jwt’ driver, which is available now we have installed the tymon/jwt-auth package.
Also note the provider, ‘customers’. This does not link directly to database table names. If you look slightly lower down in this same file you should come to a ‘providers’ section. Here is where you can link providers to database tables. Mine looks like this:
‘providers’ => [ ‘users’ => [ ‘driver’ => ‘eloquent’, ‘model’ => App\User::class, ], ‘customers’ => [ ‘driver’ => ‘eloquent’, ‘model’ => App\Customer::class, ],],
We’re now done configuring.
Authenticating Users / Middleware
Because we’re extending the Laravel auth package, and we have a project running with two different types of auth, we need to be particularly crafty with our middleware. As the guard being used is passed into this function, we need to ensure that this function works for the token auth and the session auth.
This is what mine looks like:
<?phpnamespace 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) {
// BEFORE THE REQUEST ENTERS OUR OUR PROJECT // Check if the users is logged
// This returns true if the user is not logged
if (Auth::guard($guard)->guest()) { // If the request is AJAX (if it is coming from AngularJS)
if ($request->ajax() || $request->wantsJson()) { // Return 401 / Unauthorised
return response(‘Unauthorised.’, 401); } else { // If the request was not from AJAX, redirect to login page.
return redirect()->guest(‘login’); } } // If the user was logged in, we can process the request
$response = $next($request);
// AFTER THE REQUEST HAS BEEN THROUGH OUR PROJECT // If the user is logged in, and the method refresh exists with
// this auth package (i.e. we're using JWT), then we want to
// refresh the token for the user's next request.
if (Auth::guard($guard)->check() && method_exists(Auth::guard($guard), ‘refresh’))
{ // Refresh the token, and place it in the headers for the user to pick up at the front end.
$response->headers->set(‘Authorization’, ‘Bearer ‘ . Auth::guard($guard)->refresh()); } return $response; }}
What we’ve done here is:
- Asserted if the user is logged in.
If they are not, then, is the request an AJAX request? If so, throw a 401 response.
If it is a normal request, then redirect the user to the login page.
2. If the user is logged in, then great! Let’s process the request.
3. After the request has passed through our project, we want to attach a new refreshed token to the response headers, but only if this request uses JWT for the auth driver.
This, however, brings up a new problem. The ‘Authorization’ key on the response header will normally be hidden. We need to designate it as an exposed header in order for AngularJS to pick it up.
Exposing The Header / CORS
Installing CORS
If you haven’t already got CORS installed for your package, then I would recommend using this package:
composer require barryvdh/laravel-cors
(See more info here: https://github.com/barryvdh/laravel-cors)
Configuring CORS
You will need to add the following to your Kernel.php to register the CORS middleware:
\Barryvdh\Cors\HandleCors::class
(See more info here: https://github.com/barryvdh/laravel-cors)
You will need to run the following command in your terminal to publish the config file:
php artisan vendor:publish --provider="Barryvdh\Cors\ServiceProvider"
After this, you should see a ‘cors.php’ file in your config directory.
We now need to add ‘Authorization’ to the exposed headers in this file. This is what my config file looks like:
<?php return [ /* | — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — | Laravel CORS | — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — | | allowedOrigins, allowedHeaders and allowedMethods can be set to array(‘*’) | to accept any value, the allowed methods however have to be explicitly listed. | */ ‘supportsCredentials’ => false, ‘allowedOrigins’ => [‘*’], ‘allowedHeaders’ => [‘*’], ‘allowedMethods’ => [‘GET’, ‘POST’, ‘PUT’, ‘DELETE’], ‘exposedHeaders’ => [‘Authorization’], ‘maxAge’ => 0, ‘hosts’ => [], ];
Logging Users In
To clear up final steps, we need to create a method to log the user in:
(I’ve attached inline notes for this)
class CustomerAccessController ...// Validate the form via the LoginRequest Form Request Class
public function login(LoginRequest $request)
{ // Attempt to log in the user with credentials
if(Auth::guard(‘customer_api’)->attempt([‘email’ => $request->get(‘email’), ‘password’ => $request->get(‘password’), ‘app_id’ => $request->session()->get(‘app’)->id]))
{
// If successful grab the user
$customer = Auth::guard(‘customer_api’)->user();
// If the customer has verified his account
if ($customer->verified)
{
// Lets generate the token
$token = Auth::guard(‘customer_api’)->tokenById($customer->id); // Return the user and the token
return response()->json([‘token’ => $token, ‘user_data’ => Auth::guard(‘customer_api’)->user()]); } // If the user isn't verified, tell them
return response()->json([‘error’ => ‘account_not_verified’], 403); } // If the credentials were incorrect, tell the user
return response()->json([‘error’ => ‘invalid_credentials’], 401);}
We also need a method to reissue an expired token, if the token is still valid according to the ‘ttl_refresh’ configuration mentioned at the start.
(I’ve attached inline notes for this)
class CustomerAccessController ...public function reissueToken(Request $request)
{
return response()->json([‘token’ => Auth::guard(‘customer_api’)->parseToken()->refresh()]);
}
Note that for both of these, I’m specifying that the ‘customer_api’ auth guard is used. If this isn’t specified, it won’t issue a token/the right token, or retrieve the right user.
We’re now done with Laravel!!
AngularJS Side
Now comes Angular. In this section, we’ll be writing code to initially store the authorization token in local storage (on the users device). From then onwards our code will take this token and attach it to every request to the server. It will then detect the updated token, and replace the old token in the local storage for the next request.
In my code, I’ve actually compiled it as a totally separate module, so we’ll follow this now.
Declaring the module and the .config(
angular.module(‘auth’, [‘ngStorage’])
.config([‘$httpProvider’, ‘$localStorageProvider’, function($httpProvider, $localStorageProvider) { $localStorageProvider.setKeyPrefix(‘auth’);
$httpProvider.defaults.headers.common[“X-Requested-With”] = “XMLHttpRequest”
$httpProvider.interceptors.push('AuthHttpInterceptor');}]);
So we’ve intiated and declared the module. We also know that we’ll need the ngStorage package for storing and retrieving the Authorization token.
NgStorage can be found here: https://github.com/gsklee/ngStorage
We’ve given the LocalStorage a unique prefix so as to not conflict with other modules that might be part of your project.
We’ve set “X-Requested-With” as “XMLHttpRequest” as this tells Laravel that it’s an AJAX request.
We’ve also notified AngularJS to intercept requests with the ‘AuthHttpInterceptor’. We’ll be creating this interceptor in a second.
Set up methods to retrieve and store the token
This should be easy enough to understand.
angular.module(‘auth’).run([‘$rootScope’, ‘$localStorage’, function($rootScope, $localStorage) {
$rootScope.getAuthToken = function(new_token)
{
return $localStorage.auth_token;
} $rootScope.storeAuthToken = function(new_token)
{
return $localStorage.auth_token = new_token;
} $rootScope.deleteAuthToken = function(new_token)
{
return delete $localStorage.auth_token;
} $rootScope.$on(‘auth-logout’, function() {
return $rootScope.deleteAuthToken();
});}]);
Setting up the $httpInterceptor
This part of the module intercepts requests and responses, allowing us to do our Authorization magic. We’re also going to slip in a function which checks to see if the token can be refreshed if we get a 401 error. If it can be refreshed, we’ll send the request again and hopefully get a successful response with the refreshed token.
I’ve attached inline notes for this.
angular.module(‘auth’).factory(‘AuthHttpInterceptor’, [‘$q’, ‘$rootScope’, ‘$localStorage’, ‘$injector’, function($q, $rootScope, $localStorage, $injector) { return { // When sending a request
request: function(config) {
// Pick up the token from storage and attach to headers
config.headers.Authorization = $localStorage.auth_token; // Send the request
return config; }, // On a successful response
response: function(response) {
// If there is a token in the headers, retrieve it
if (new_token = response.headers(‘Authorization’))
{
// Store the token in storage
$rootScope.storeAuthToken(new_token);
}
// Return the reponse to the app
return response; }, // On a unsuccessful response
responseError: function(rejection) { // If the error is 401 related
if(rejection.status === 401)
{ // We're going to get attempt to refresh the token on the
// server, if we're within the ttl_refresh period.
var deferred = $q.defer(); // We inject $http, otherwise we will get a circular ref
$injector.get(“$http”).post('http://my-api.com/customer/reissue/token’, {}, {
headers: {
Authorization: AuthService.getToken()
}
}).then(function(response) {
// If this request was successful, we will have a new
// token, so let's put it in storage
$rootScope.storeAuthToken(response.token); // Now let's send the original request again
$injector.get(“$http”)(response.config)
.then(function(response) {
// The repeated request was successful! So let's put
// this response back to the original workflow
return deferred.resolve(response); }, function() { // Something went wrong with this request
// So we reject the response and carry on with 401
// error
$rootScope.$broadcast(‘auth-logout’);
return deferred.reject(); }) }, function() { // Refreshing the token failed, so let's carry on with
// 401
$rootScope.$broadcast(‘auth-logout’);
return deferred.reject(); }); // Now we continue with the 401 error if we've reached this
// point
return deferred.promise;
} return $q.reject(rejection); }
};}]);
Setting up the AuthService
This part of the module provides some key functions.
- To get the current token. This is needed occasionally. For example when using an upload module, you might need the token for this module’s headers. In this case, you need a way of getting the token from storage and into this other module.
- To check if there is an authenticated user.
- To login, and then put the token in storage.
I’ve attached inline notes for this.
angular.module(‘auth’).factory(‘AuthService’, [‘$rootScope’, ‘$http’, ‘$localStorage’, function($rootScope, $http, $localStorage) { return {
getToken: function() {
// Get the token from storage
return $rootScope.getAuthToken();
}, isAuthenticated: function() {
// If there is a token, the return true
return ($rootScope.getAuthToken()) ? true : false
}, login: function(url, credentials) {
$http({
method: ‘POST’,
url: url,
data: credentials
}).then(function(response) {
// On success, set the token and fire an event
$rootScope.storeAuthToken(“Bearer “ + response.data.token);
$rootScope.$broadcast(‘auth-login-success’, response.data);
}, function(response) { // On login error, fire an event for the main app to pick
// up
$rootScope.$broadcast(‘auth-login-error’, response); }).finally(function() { // Once the login process is complete (successful or
// unsuccessful), fire this event
$rootScope.$broadcast(‘auth-login-complete’); }); },
logout: function() {
// On request to logout, fire the event to be picked up
// above
$rootScope.$broadcast(‘auth-logout’);
}
}}]);
Finished!
Hopefully you have found this helpful. It was a fast and quick overview, but that’s the way I like it.
Credits to:
Taylor (of Course).
Tymon Designs: https://github.com/tymondesigns
Barry VDH: https://github.com/barryvdh
Chris Clarke: http://engineering.talis.com/articles/elegant-api-auth-angular-js/
Syed Irfaq R: https://github.com/irazasyed
Have a great day!
Ed