How To Authenticate APIs With Laravel Sanctum
Given Ncube
When building web applications it is often a good idea to build an external API for other frontend clients like mobile apps or something. In this article, I'm going to show you how to secure your Laravel APIs with Sanctum. I assume you have a basic knowledge of Laravel and REST APIs. You will need :
-
PHP 7+, MySQL
-
Laravel UI
-
Postman or curl
-
and of course composer
Create a new Laravel project and set up a database connection in your .env
file. You'll also need to install laravel/ui
composer require laravel/ui
Then run the auth scaffold
php artisan ui bootstrap --auth
After this, you should have a working auth scaffold. For this example, I'm creating a simple todos API.
Step 1: Models and Controllers
Let's create our models
php artisan make:model Todo -a
This will create the Todos model, a migration file a seeder, a controller, and a factory. Open the migration file and add the following code.
public function up()
{
Schema::create('todos', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->boolean('complete')->default(false);
$table->timestamps();
});
}
Step 2: setup sanctum
Our models and controller are now in place let's set up our authentication with sanctum. Install Laravel Sanctum.
composer require laravel/sanctum
Now run the migration
php artisan migrate
To begin issuing tokens for users, your User model should use the HasApiTokens trait from sanctum:
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasFactory, Notifiable, HasApiTokens;
Now we can start protecting our routes with sanctum which leads us to the next step
Step 4: Routes
Open the RouteServiceProvider
and uncomment the $namespace
property like this:
protected $namespace = 'App\\Http\\Controllers';
Now we're set let's add our API routes. In the api routes
file add the following routes.
Route::post('/login', 'API\UserController@login');
Route::middleware('auth:sanctum')->group(function () {
Route::delete('/logout', 'API\UserController@logout');
Route::resource('todos', 'TodosController');
});
As you can see we don't have a UserController
in the API
namespace, let's create it
php artisan make:controller API\\UserController
We're almost done
Step 4: Issuing Tokens
In the API\Usercontroller
add the following code
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Hash;
use Illuminate\Http\Request;
use App\Models\User;
class UserController extends Controller
{
/**
* Issues an auth token to the user
*
* @return \Illuminate\Http\Response
*/
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
'device_name' => 'required',
]);
$user = User::where('email', $request->email)->first();
if (! $user || ! Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
]);
}
return response()->json([
'token' => $user->createToken($request->device_name)->plainTextToken,
]);
}
/**
* Revokes the user's access token
*
* @return \Illuminate\Http\Response
*/
public function logout(Request $request)
{
$request->user()->currentAccessToken()->delete();
return response()->noContent();
}
}
The way this API works is that your users create accounts from the main website then login with the API client to get a token for that specific client. The login
method takes an email, password, and device name. If the user's credentials are verified the API returns the token as JSON to the client which is specific to that device. The logout
method simply revokes the current token nothing fancy there. Our API auth is done, let's add some JSON resources for our todos.
php artisan make:resource TodoResource
In the todo controller let's return some JSON. We're only going to implement the index action.
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
$todos = Todo::all();
return new TodoResource($todos);
}
Create a factory and seeder for dummy data and seed the database
Testing our auth
I'm using curl to test it you can use Postman or Insomnia or whatever
┌──(given㉿flixtechs)-[~/Documents/PHP/api]
└─$ curl "http://127.0.0.1:8000/api/todos" -H 'Accept: application/json'
{"message":"Unauthenticated."}
As you can see I'm not authenticated I can't access the resource. Let's try to log in. I've already created a user via the main web UI.
┌──(given㉿flixtechs)-[~/Documents/PHP/api]
└─$ curl 'http://127.0.0.1:8000/api/login' -H 'Accept: application\json' -X POST -d email='giveny12@yahoo.com' -d password='fY@!4U@MYWcf.cz' -d device_name='curl-client'
{"token":"9|XArCVpkPTpGoax4aF5KDQnlyFNWD8SJ0pkUTdDxD"}
As you can see, I've logged in and received a token in your client you can save this token somewhere for future requests. To authenticate with this token we have to use it as Bearer
in the Authorization
header:
┌──(given㉿flixtechs)-[~/Documents/PHP/api]
└─$ curl http://127.0.0.1:8000/api/todos \
> -H 'Accept: application/json' \
> -H 'Authorization: Bearer 9|XArCVpkPTpGoax4aF5KDQnlyFNWD8SJ0pkUTdDxD'
{"data":[{"id":1,"name":"Kelley Gottlieb","complete":0,"created_at":"2020-11-29T12:47:09.000000Z","updated_at":"2020-11-29T12:47:09.000000Z"},{"id":2,"name":"Lazaro Goldner","complete":0,"created_at":"2020-11-29T12:47:09.000000Z","updated_at":"2020-11-29T12:47:09.000000Z"},{"id":3,"name":"Mr. Aaron Bruen","complete":0,"created_at":"2020-11-29T12:47:09.000000Z","updated_at":"2020-11-29T12:47:09.000000Z"},{"id":4,"name":"Kyla Marquardt","complete":0,"created_at":"2020-11-29T12:47:10.000000Z","updated_at":"2020-11-29T12:47:10.000000Z"},{"id":5,"name":"Lia Bergnaum","complete":0,"created_at":"2020-11-29T12:47:10.000000Z","updated_at":"2020-11-29T12:47:10.000000Z"},{"id":6,"name":"Robbie Brekke","complete":0,"created_at":"2020-11-29T12:47:10.000000Z","updated_at":"2020-11-29T12:47:10.000000Z"},{"id":7,"name":"Miss Margarita Ratke","complete":0,"created_at":"2020-11-29T12:47:10.000000Z","updated_at":"2020-11-29T12:47:10.000000Z"},{"id":8,"name":"Jessyca Stokes","complete":0,"created_at":"2020-11-29T12:47:10.000000Z","updated_at":"2020-11-29T12:47:10.000000Z"},{"id":9,"name":"Dr. Mylene Doyle","complete":0,"created_at":"2020-11-29T12:47:10.000000Z","updated_at":"2020-11-29T12:47:10.000000Z"},{"id":10,"name":"Vilma Erdman","complete":0,"created_at":"2020-11-29T12:47:10.000000Z","updated_at":"2020-11-29T12:47:10.000000Z"},{"id":11,"name":"Dr. Keagan O'Conner PhD","complete":0,"created_at":"2020-11-29T12:47:10.000000Z","updated_at":"2020-11-29T12:47:10.000000Z"},{"id":12,"name":"Lou Heaney","complete":0,"created_at":"2020-11-29T12:47:10.000000Z","updated_at":"2020-11-29T12:47:10.000000Z"},{"id":13,"name":"Prof. Larissa Carroll","complete":0,"created_at":"2020-11-29T12:47:10.000000Z","updated_at":"2020-11-29T12:47:10.000000Z"},{"id":14,"name":"Mrs. Dejah Hane V","complete":0,"created_at":"2020-11-29T12:47:10.000000Z","updated_at":"2020-11-29T12:47:10.000000Z"},{"id":15,"name":"Doug Herman","complete":0,"created_at":"2020-11-29T12:47:10.000000Z","updated_at":"2020-11-29T12:47:10.000000Z"},{"id":16,"name":"Isabella Mills Jr.","complete":0,"created_at":"2020-11-29T12:47:10.000000Z","updated_at":"2020-11-29T12:47:10.000000Z"},{"id":17,"name":"Dr. Elwin Ullrich","complete":0,"created_at":"2020-11-29T12:47:10.000000Z","updated_at":"2020-11-29T12:47:10.000000Z"},{"id":18,"name":"Humberto Stehr","complete":0,"created_at":"2020-11-29T12:47:10.000000Z","updated_at":"2020-11-29T12:47:10.000000Z"},{"id":19,"name":"Frank Thiel","complete":0,"created_at":"2020-11-29T12:47:11.000000Z","updated_at":"2020-11-29T12:47:11.000000Z"},{"id":20,"name":"Maurice Runolfsson","complete":0,"created_at":"2020-11-29T12:47:11.000000Z","updated_at":"2020-11-29T12:47:11.000000Z"}]}
This was one way of doing it don't forget the official docs. Happy coding.
[convertkit=2908863]