In parts 1 and 2 covering how to get started with a Laravel 5.5 API project, we covered creating a fresh Laravel project and then starting development with a User Registration API. Up to this point things have been low friction, thanks to the amount of work Laravel does for us when new projects are setup.
Moving forward, in this part we’ll cover creating an API Authentication endpoint, including the beginnings of implementing a JWT authentication model.
Implementing Authentication & JWT
Our API’s login method can be setup with a similar process to that used for the Registration API (route setup, new controller, etc.). But there’s one exception: we’ll need to install a JWT package; it’ll provide needed support to generate and decrypt JWTs during authentication. In this part, we’ll cover installing the JWT package and how to use it to return a JWT with the user’s login response. The JWT will also need to be returned to the API on subsequent requests as a means to authenticate those requests, but we’ll cover that in another post.
For now, let’s focus on installing JWT support and implementing a login method in our Laravel 5.5 project.
First, to install the jwt-auth package, run the following command. Note that at the time of this post, Laravel 5.5 requires the package’s dev branch:
composer require tymon/jwt-auth:dev-develop --prefer-source;
Second, add these entries to the existing providers and aliases arrays in config/app.php:
'providers' => [ Tymon\JWTAuth\Providers\LaravelServiceProvider::class ], 'aliases' => [ 'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class, ],
Third, publish the JWT package assets to our project, which will include creating a config/jwt.php file:
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
Fourth, and at the heart of how JWT operates securely, we need to generate a JWT secret key specific to our project and environment. Run the following command, and be sure to type “yes” when prompted:
php artisan jwt:secret;
It’s important to note that the secret key that was just generated is what’ll be used to securely sign each token upon generation, and to decrypt it when received on subsequent requests. It should not be shared publicly. That also means that while the above step generates a secret key for our particular environment, a different key should be generated and used for other environments (test, production, etc.). Also, be sure these keys don’t land in version control repositories.
Fifth, create app/Http/Middleware/AuthJWT.php (credit to laravelcode.com), w/the following contents:
<?php namespace App\Http\Middleware; use Closure; use JWTAuth; use Exception; class AuthJWT { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { try { $user = JWTAuth::toUser($request->input('token')); } catch (Exception $e) { if ($e instanceof \Tymon\JWTAuth\Exceptions\TokenInvalidException){ return response()->json(['error'=>'Invalid token.']); } else if ($e instanceof \Tymon\JWTAuth\Exceptions\TokenExpiredException){ return response()->json(['error'=>'Expired token.']); } else { return response()->json(['error'=>'Authentication error.']); } } return $next($request); } }
This class will handle decrypting and validating received JWTs.
Sixth, register this new class in app/Http/Kernel.php file by adding the following entry to the protected $routeMiddleware array:
'jwt-auth' => \App\Http\Middleware\AuthJWT::class,
Seventh, we need a controller to field API authentication requests. For that, we’ll create a controller class file at app/Http/Controllers/Auth/ApiAuthController.php with the following contents:
<?php namespace App\Http\Controllers\Auth; use Illuminate\Http\Request; use App\Http\Controllers\Controller; use App\Http\Requests; use JWTAuth; use JWTAuthException; use App\User; class ApiAuthController extends Controller { public function __construct() { $this->user = new User; } public function login(Request $request){ $credentials = $request->only('email', 'password'); $jwt = ''; try { if (!$jwt = JWTAuth::attempt($credentials)) { return response()->json([ 'response' => 'error', 'message' => 'invalid_credentials', ], 401); } } catch (JWTAuthException $e) { return response()->json([ 'response' => 'error', 'message' => 'failed_to_create_token', ], 500); } return response()->json([ 'response' => 'success', 'result' => ['token' => $jwt] ]); } public function getAuthUser(Request $request){ $user = JWTAuth::toUser($request->token); return response()->json(['result' => $user]); } }
Before proceeding, take note of the 401 and 500 HTTP response codes specified above. Other examples of similar login or authentication classes omit those, which I feel is an error. Reason being, returning or indicating an error condition such as a failed login, merely with some error message (e.g. invalid_credentials, as used above), while also returning an HTTP status code of 200 (meaning OK, which is default if one isn’t specified), isn’t in the spirit of REST. Such a response isn’t as accurate or clear as it could be, in part b/c HTTP 401 Unauthorized is arguably a better suited response code for failed login attempts.
One reason adhering to suitable response codes is important is b/c REST API clients can be very status-code-aware; they’ll look primarily to the HTTP status code for indication of success or error vs. some proprietary error text in your API’s response string. And rightfully so – HTTP status codes are standardized and first-class citizens in REST APIs, not to mention your proprietary error text can change at any time. So always prefer HTTP status codes as the primary means to communicate success or error for an API request, and only use error text or messages to supplement the reason behind the status code.
On to the next step …
Eighth, add the API’s login route. In routes/api.php, add an auth/login route as shown below. It’s added to the same middleware closure we added the auth/register route to:
Route::group(['middleware' => ['api','cors']], function () { Route::post('auth/register', 'Auth\RegisterController@create'); Route::post('auth/login', 'Auth\ApiAuthController@login'); });
Ninth, and as noted in the Sep 20 post in this GitHub discussion (I encountered the same problem so wanted to cite credit to that poster), the User Model must now implement the JWTSubject interface. Here’s what the model should look like:
<?php namespace Illuminate\Foundation\Auth; use Illuminate\Auth\Authenticatable; use Illuminate\Database\Eloquent\Model; use Illuminate\Auth\Passwords\CanResetPassword; use Illuminate\Foundation\Auth\Access\Authorizable; use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract; use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; use Tymon\JWTAuth\Contracts\JWTSubject; class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, JWTSubject /** * @return mixed */ public function getJWTIdentifier() { return $this->getKey(); } /** * @return array */ public function getJWTCustomClaims() { return ['user' => ['id' => $this->id]]; } }
Note JWTSubject in the class signature, and the addition of its methods to satisfy its implementation.
Finally, the login endpoint is ready to test. Again in Postman, send a request to http://localhost:8000/auth/login. Send email and password key/value pairs, w/values corresponding to the test user created in Part 2. Here’s what the request looks like in Postman:
Verify that the request completes successfully, and that a JWT is returned in the response as shown above. In subsequent posts, we’ll cover the role of the received JWT in the overall authentication scheme.
Conclusion
This covered an API login example, including the beginnings of implementing JWT authentication in our Laravel 5.5 API. Thus far we’ve covered how to create a fresh Laravel project, then added a User Registration API. With test users introduced into the APIs database, we’ve also implemented a login/authentication API, which includes the beginnings of JWT in the API.
In the next post we’ll cover the JWT in more detail, including how to validate a user’s JWT on subsequent API requests.