Laravel 5 and Angular Auth using JSON Web Token (JWT) - Local Dev Env
We'll use this repo : laravel5-angular-jwt which I modified the original repo: https://github.com/ttkalec/laravel5-angular-jwt.
Result first:
Let's go to our Laravel folder:
$ cd ~/Code/Laravel
Then, clone:
$ git clone --depth=1 https://github.com/epic-math/laravel5-angular-jwt.git
In our project directory, run the following commands to get the latest Composer version (we can get the commands from Download Composer):
$ cd laravel5-angular-jwt $ php -r "readfile('https://getcomposer.org/installer');" > composer-setup.php $ php -r "if (hash('SHA384', file_get_contents('composer-setup.php')) === 'fd26ce67e3b237fffd5e5544b45b0d92c41a4afe3e3f778e942e43ce6be197b9cdc7c251dcde6e2a52297ea269370680') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); }" $ php composer-setup.php $ php -r "unlink('composer-setup.php');"
Next, we need to run php composer install to install the dependencies:
$ composer updateOr:
$ composer install
Run php artisan vendor:publish to publish JWT and CORS packages config files:
$ php artisan vendor:publish Publishing Complete!
Before we migrate the database, we need to create a MySQL user, "homestead":
$ mysql -u root -p mysql> CREATE DATABASE forge; mysql> CREATE USER 'forge'@'localhost' IDENTIFIED BY 'password';
Run php artisan migrate to migrate the database:
$ php artisan migrate
Actually, I had an error saying "PDOException] SQLSTATE[28000] [1045] Access denied for user âhomesteadâ@âlocalhostâ (using password: YES) looks like same error different is not showing any file line number.
So, I had to do the following things:
- Match the username, db, pass of the mysql conf. both in .env and config/database.php.
To do that, we may want to copy .env.example to .env:$ cp .env.example .env
Actually, I left the password in config/database.php as blank. - Clear cache
$ php artisan config:clear
The following two sections are about dependency update using "composer update" and quite straight-forward. Though there are not a lot we should do at this point because everything is already into the repo we got earlier. But one thing we need to do is to set jwt secret key which is separate key from the app key.
Now JWT thing (JSON Web Token Authentication for Laravel & Lumen).
Check if jwt is in composer.json:
"require": { "tymon/jwt-auth": "0.5.*" }
To pull it in, run composer update:
$ composer update
Got "Call to undefined method Illuminate\Foundation\Application::getCachedCompilePath()" error during "composer update".
Did the following:
$ rm storage/framework/compiled.php
After that composer update was successful:
$ composer update Loading composer repositories with package information Updating dependencies (including require-dev) Nothing to install or update Generating autoload files > php artisan clear-compiled > php artisan optimize Generating optimized class loader
Add the following to config/app.php:
'Tymon\JWTAuth\Providers\JWTAuthServiceProvider' 'JWTAuth' => 'Tymon\JWTAuth\Facades\JWTAuth' 'JWTFactory' => 'Tymon\JWTAuth\Facades\JWTFactory'
Publish the config using the following command:
$ php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\JWTAuthServiceProvider"
Set a secret key in the config file using this command to generate a key:
$ php artisan jwt:generate
The key generated will be put into config/jwt.php.
The laravel-cors package allows us to send Cross-Origin Resource Sharing headers with ACL-style per-url configuration (Cross-Origin Resource Sharing).
The defaults are set in config/cors.php. Copy this file to config directory to modify the values. We can publish the config using this command:
$ php artisan vendor:publish --provider="Barryvdh\Cors\ServiceProvider" Nothing to publish
Weird. But decided to move on from this.
Check if the following is in composer.json:
"require": { "barryvdh/laravel-cors": "0.4.x@dev", },
Update dependencies:
$ composer install Loading composer repositories with package information Installing dependencies (including require-dev) from lock file Nothing to install or update Generating autoload files > php artisan clear-compiled > php artisan optimize Generating optimized class loader
Since I worked on my home folder, we need to move the files to /var/www:
k@laptop:~/Code/Laravel$ sudo cp -r laravel5-angular-jwt /var/www
Ownership change:
k@laptop:/var/www$ sudo chown -R www-data:www-data laravel5-angular-jwt
I moved the code to /var/www/, and the nginx configuration looks like this:
server { listen 8001 default_server; listen [::]:8001 default_server ipv6only=on; #root /var/www/todo/public; root /var/www/angular-laravel5-jwt/public; index index.php index.html index.htm; server_name laravel.example.com; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { try_files $uri /index.php =404; fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass unix:/var/run/php5-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location ~ /\.ht { deny all; } }
The setup is almost identical to my previous setup for todo app. So, for more information about the configuration, please visit ToDo list running on Nginx.
Though I showed the result earlier, here again our new page:
However, unfortunately, I got the following error at SignUp:
XMLHttpRequest cannot load http://jwt.dev:8000/signup. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://laravel.example.com:8001' is therefore not allowed access. The response had HTTP status code 404.
So, I looked for the "jwt.dev":
$ grep -irn "jwt.dev" ./ ./public/scripts/app.js:10: BASE: 'http://jwt.dev:8000', ./public/scripts/app.js:11: BASE_API: 'http://api.jwt.dev:8000/v1' ./app/Http/routes.php:65:Route::group(['domain' => 'api.jwt.dev', 'prefix' => 'v1'], function () {
After replacing the "jwt.dev" with "laravel.example.com" and "8000" with "8001", we may want to clear cache:
$ sudo php artisan cache:clear
Now, we can try to Signup & SignIn.
Now I am able to signup and signin:
Just an illustration, we'll see only the part of "signin" process.
public/partials/home.html:
<div class="row"> <div class="col-lg-12"> <div class="jumbotron"> <h1>Welcome to JWT example!</h1> <div class="row"> <div data-ng-hide="token" class="col-md-6"> <a href="#/signup" class="btn btn-default btn-primary margin-right" role="button">Sign up</a> or <a href="#/signin" class="btn btn-default btn-primary margin-left" role="button">Sign in</a> </div> <div data-ng-show="token" class="col-md-6"> <a href="#/restricted" class="btn btn-default btn-primary" role="button">Restricted area</a> </div> </div> </div> </div> </div> <div data-ng-show="token" class="row"> <div class="col-lg-4"> <p>You are now logged in and have received JWT from the backend.</p> <p>Here are the claims data:</p> <table class="table table-bordered table-striped"> <tr data-ng-repeat="(key, value) in tokenClaims"> <td> {{ key }} </td> <td> {{ value }} </td> </tr> </table> </div> </div>
public/scripts/app.js:
(function () { 'use strict'; angular.module('app', [ 'ngStorage', 'ngRoute', 'angular-loading-bar' ]) .constant('urls', { /* BASE: 'http://jwt.dev:8001', BASE_API: 'http://api.jwt.dev:8000/v1' */ BASE: 'http://laravel.example.com:8001', BASE_API: 'http://laravel.example.com:8001/v1' }) .config(['$routeProvider', '$httpProvider', function ($routeProvider, $httpProvider) { $routeProvider. when('/', { templateUrl: 'partials/home.html', controller: 'HomeController' }). when('/signin', { templateUrl: 'partials/signin.html', controller: 'HomeController' }). when('/signup', { templateUrl: 'partials/signup.html', controller: 'HomeController' }). when('/restricted', { templateUrl: 'partials/restricted.html', controller: 'RestrictedController' }). otherwise({ redirectTo: '/' }); ...
public/partials/signin.html:
<div class="row"> <div class="col-lg-6 col-lg-offset-3"> <div class="panel panel-primary"> <div class="panel-heading"> <strong>Sign in</strong> </div> <div class="panel-body"> <form class="form-horizontal" role="form" ng-submit="signin()"> <div class="form-group"> <label for="email" class="col-sm-2 control-label">Email</label> <div class="col-sm-10"> <input type="email" class="form-control" id="email" placeholder="Email" ng-model="email"> </div> </div> <div class="form-group"> <label for="password" class="col-sm-2 control-label">Password</label> <div class="col-sm-10"> <input type="password" class="form-control" id="password" placeholder="Password" ng-model="password"> </div> </div> <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> <button type="submit" class="btn btn-primary pull-right">Sign in</button> </div> </div> <p data-ng-show="error" class="error">{{ error }}</p> </form> </div> </div> </div> </div>
public/scripts/controller.js:
(function () { 'use strict'; angular.module('app') .controller('HomeController', ['$rootScope', '$scope', '$location', '$localStorage', 'Auth', function ($rootScope, $scope, $location, $localStorage, Auth) { function successAuth(res) { $localStorage.token = res.token; window.location = "/"; } $scope.signin = function () { var formData = { email: $scope.email, password: $scope.password }; Auth.signin(formData, successAuth, function () { $rootScope.error = 'Invalid credentials.'; }) };
app/Http/routes.php:
<?php use App\User; use Illuminate\Http\Response as HttpResponse; /** * Displays Angular SPA application */ Route::get('/', function () { return view('spa'); }); /** * Registers a new user and returns a auth token */ Route::post('/signup', function () { $credentials = Input::only('email', 'password'); try { $user = User::create($credentials); } catch (Exception $e) { return Response::json(['error' => 'User already exists.'], HttpResponse::HTTP_CONFLICT); } $token = JWTAuth::fromUser($user); return Response::json(compact('token')); }); /** * Signs in a user using JWT */ Route::post('/signin', function () { $credentials = Input::only('email', 'password'); if (!$token = JWTAuth::attempt($credentials)) { return Response::json(false, HttpResponse::HTTP_UNAUTHORIZED); } return Response::json(compact('token')); }); /** * Fetches a restricted resource from the same domain used for user authentication */ Route::get('/restricted', [ 'before' => 'jwt-auth', function () { $token = JWTAuth::getToken(); $user = JWTAuth::toUser($token); return Response::json([ 'data' => [ 'email' => $user->email, 'registered_at' => $user->created_at->toDateTimeString() ] ]); } ]); /** * Fetches a restricted resource from API subdomain using CORS */ /* Route::group(['domain' => 'api.jwt.dev', 'prefix' => 'v1'], function () { */ Route::group(['domain' => 'api.laravel.example.com', 'prefix' => 'v1'], function () { Route::get('/restricted', function () { try { JWTAuth::parseToken()->toUser(); } catch (Exception $e) { return Response::json(['error' => $e->getMessage()], HttpResponse::HTTP_UNAUTHORIZED); } return ['data' => 'This has come from a dedicated API subdomain with restricted access.']; }); });
The spa page, resources/views/spa.blade.php:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Laravel 5 / AngularJS JWT example</title> <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"> <link rel="stylesheet" href="/css/bootstrap.superhero.min.css"> <link rel="stylesheet" href="/lib/loading-bar.css"> <link rel="stylesheet" href="{{ assetVersioned('/css/app.css') }}"> <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> <!--[if lt IE 9]> <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script> <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script> <![endif]--> </head> <body ng-app="app"> <div class="navbar navbar-inverse navbar-fixed-top" role="navigation" data-ng-controller="HomeController"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target=".navbar-collapse"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="#/">JWT Angular example</a> </div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav navbar-right"> <li data-ng-show="token"><a ng-href="#/restricted">Restricted area</a></li> <li data-ng-hide="token"><a ng-href="#/signin">Signin</a></li> <li data-ng-hide="token"><a ng-href="#/signup">Signup</a></li> <li data-ng-show="token"><a ng-href="#/" ng-click="logout()">Logout</a></li> </ul> </div> </div> </div> <div class="container" ng-view=""></div> <div class="footer"> <div class="container"> <p class="muted credit">Example by <a href="http://www.toptal.com/resume/tino-tkalec" title="Tino Tkalec">Tino Tkalec</a></p> </div> </div> <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.14/angular.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.14/angular-route.min.js"></script> <script src="/lib/ngStorage.js"></script> <script src="/lib/loading-bar.js"></script> <script src="{{ assetVersioned('/scripts/app.js') }}"></script> <script src="{{ assetVersioned('/scripts/controllers.js') }}"></script> <script src="{{ assetVersioned('/scripts/services.js') }}"></script> </body> </html>
config/jwt.php which contains jwt-auth:
<?php return [ 'secret' => env('JWT_SECRET', 'secret'), 'ttl' => 60, 'refresh_ttl' => 20160, 'algo' => 'HS256', 'user' => 'App\User', 'identifier' => 'id', 'required_claims' => ['iss', 'iat', 'exp', 'nbf', 'sub', 'jti'], 'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true), 'providers' => [ 'user' => 'Tymon\JWTAuth\Providers\User\EloquentUserAdapter', 'jwt' => 'Tymon\JWTAuth\Providers\JWT\NamshiAdapter', 'auth' => 'Tymon\JWTAuth\Providers\Auth\IlluminateAuthAdapter', 'storage' => 'Tymon\JWTAuth\Providers\Storage\IlluminateCacheAdapter' ] ];
Github code:
laravel5-angular-jwtPh.D. / Golden Gate Ave, San Francisco / Seoul National Univ / Carnegie Mellon / UC Berkeley / DevOps / Deep Learning / Visualization