In this post, we are going to build a simple authentication JSON APIs in Laravel. There will be five JSON APIs.
- Send OTP API: /api/account/send-otp/ (for sending OTP for given mobile number)
- Login API: /api/account/login/ (For verifing OTP)
- Access Token API: /api/account/refresh-access-token/ (For requesting access token for authenticated user)
- Update user info: /api/account/update-user/ (for updating user information on the server)
- Logout API: /api/account/logout/ (for making authentication token invalid)
You can find code on GitHub here https://github.com/28harishkumar/laravel_auth_demo

Create a new Project
If you have not installed Composer then install it from https://getcomposer.org/ and install Laravel as mentioned here https://laravel.com/docs/8.x/installation#installing-laravel. Create a new project named `laravel_authentication` by using:
- laravel new laravel_authentication
For Oauth2, we will using Laravel Passport. Install Laravel Passport using:
- composer require laravel/passport
Now setup database by updating laravel_authentication/.env file.
- DB_CONNECTION=mysql
- DB_HOST=127.0.0.1
- DB_PORT=3306
- DB_DATABASE=your_database_name
- DB_USERNAME=your_databasee_username
- DB_PASSWORD=your_mysql_password
Database Tables (Migrations)
For authentication we need two table User and OTP. Some more tables will be created by passport package to handle access tokens. Open laravel_authentication/database/migrations/2014_10_12_000000_create_users_table.php and replace up() function with following code:
- <?php // don't include this line, this is for code decoration
-
- public function up()
- {
- Schema::create('users', function (Blueprint $table) {
- $table->id();
- $table->string('name')->nullable();;
- $table->string('email')->nullable();
- $table->string('mobile_no')->unique();
- $table->timestamp('email_verified_at')->nullable();
- $table->string('password')->nullable();;
- $table->string('photo')->nullable();;
- $table->rememberToken()->default(1);
- $table->timestamps();
- });
- }
In User table we have marked name and email nullable and added mobile_no and photo fields. Now Create OTP table using
- php artisan make:migration create_otp_table
Replace content of laravel_authentication/database/migrations/*_create_otp_table.php with:
- <?php
-
- use Illuminate\Database\Migrations\Migration;
- use Illuminate\Database\Schema\Blueprint;
- use Illuminate\Support\Facades\Schema;
-
- class CreateOtpTable extends Migration
- {
- /**
- * Run the migrations.
- *
- * @return void
- */
- public function up()
- {
- Schema::create('otp', function(Blueprint $table)
- {
- $table->id();
- $table->string('mobile_no');
- $table->string('code');
- $table->boolean('active')->default(1);
- $table->timestamps();
- });
- }
-
- /**
- * Reverse the migrations.
- *
- * @return void
- */
- public function down()
- {
- //
- {
- Schema::drop('otp');
- }
- }
- }
Now migrate the tables to the database using:
- php artisan migrate
Now we need to generate keys for passport. Run the following command in the terminal:
- php artisan passport:install
Models
For using Eloquent ORM, we need to define Model classes for User and OTP tables. Open laravel_authentication/app/Models/User.php and replace the code with:
- <?php
-
- namespace App\Models;
-
- use Laravel\Passport\HasApiTokens;
- use Illuminate\Contracts\Auth\MustVerifyEmail;
- use Illuminate\Database\Eloquent\Factories\HasFactory;
- use Illuminate\Foundation\Auth\User as Authenticatable;
- use Illuminate\Notifications\Notifiable;
-
- class User extends Authenticatable
- {
- use HasApiTokens, HasFactory, Notifiable;
-
- /**
- * The attributes that are mass assignable.
- *
- * @var array
- */
- protected $fillable = [
- 'name',
- 'mobile_no',
- 'password',
- ];
-
- /**
- * The attributes that should be hidden for arrays.
- *
- * @var array
- */
- protected $hidden = [
- 'password',
- 'remember_token',
- ];
-
- /**
- * The attributes that should be cast to native types.
- *
- * @var array
- */
- protected $casts = [
- 'email_verified_at' => 'datetime',
- ];
- }
In line 5 and line 13, I have added HasApiTokens from Laravel Passport for using Oauth2. We will use createToken function from HasApiTokens to generate access token for the user.
Now create a new folder in Models named Account with a file OTP.php in it. Paste the following code in laravel_authentication/app/Models/Account/OTP.php
- <?php
-
- namespace App\Models\Account;
-
- use Carbon\Carbon;
- use Illuminate\Database\Eloquent\Factories\HasFactory;
- use Illuminate\Database\Eloquent\Model;
- use Illuminate\Support\Facades\Http;
-
- class OTP extends Model
- {
- use HasFactory;
-
- protected $table = 'otp';
-
- public function isValidOTPTime() {
- // valid only for 10 minutes
- $timenow = Carbon::now();
- return $timenow->diffInMinutes($this->created_at) < 10;
- }
-
- /**
- * Sends request for OTP
- */
- public function sendToMobile() {
- // MSG19 server request
- $curl = curl_init();
-
- curl_setopt_array($curl, array(
- CURLOPT_URL => "https://api.msg91.com/api/v5/flow/",
- CURLOPT_RETURNTRANSFER => true,
- CURLOPT_ENCODING => "",
- CURLOPT_MAXREDIRS => 10,
- CURLOPT_TIMEOUT => 30,
- CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
- CURLOPT_CUSTOMREQUEST => "POST",
- CURLOPT_POSTFIELDS => "",
- CURLOPT_SSL_VERIFYHOST => 0,
- CURLOPT_SSL_VERIFYPEER => 0,
- CURLOPT_HTTPHEADER => array(
- "authkey: EnterYourAuthKey",
- "content-type: application/json"
- ),
- ));
-
- $response = curl_exec($curl);
- $err = curl_error($curl);
-
- curl_close($curl);
- }
- }
Here I have created a Model for OTP table. I need to define protected $table = 'otp'; because I have named at class in capital letters. Otherwise Laravel will look for the table o_t_p instead of otp. I have also defined a function isValidOTPTime. This function will check if OTP was generated less than 10 minutes ago. There is another function for sending OTP to the mobile number via MSG91.
Controller
Now we will add the controller class that will handle the API requests. Create a new folder Auth in laravel_authentication/app/Http/Controllers with a file named AuthController.php. Update this file with following code:
- <?php
-
- namespace App\Http\Controllers\Auth;
-
- // import User Model
- use App\Models\User;
-
- // import OTP model
- use App\Models\Account\OTP;
-
- // import Carbon of DateTime operations
- use Carbon\Carbon;
-
- use Illuminate\Http\Request;
- use App\Http\Controllers\Controller;
- use Illuminate\Support\Facades\Auth;
-
- // for storing image (user photo)
- use Illuminate\Support\Facades\Storage;
-
- class AuthController extends Controller {
- // for sending otp
- public function sendOTP(Request $request) {
-
- }
-
- // for login
- public function login(Request $request) {
-
- }
-
- // for logout
- public function logout(Request $request) {
-
- }
-
- // update user info (name and photo)
- public function updateUser(Request $request) {
-
- }
-
- // for creating new access token
- public function refreshToken(Request $request) {
-
- }
- }
Here I have imported required files and defined abstract functions. I will map these functions with API routes in the next section.
I imported Carbon\Carbon for datetime operations. Illuminate\Support\Facades\Storage; has been used for creating storage uri for user's photo. We have to complete five functions.
sendOTP()
Update sendOTP() function as:
- <?php // don't include this line, this is for code decoration
-
- public function sendOTP(Request $request) {
- // collect mobile number
- $mobile_no = $request->mobile_no;
-
- // validate mobile number
- if(!preg_match('/^[0-9]{10}+$/', $mobile_no)) {
- // raise Error
- return response()->json([
- 'message' => 'Invalid Mobile Number!'
- ], 403);
- }
-
- // check if account exists
- $user = User::firstWhere('mobile_no', $mobile_no);
- if($user) {
- $account_exists = true;
- } else {
- $account_exists = false;
- }
-
- // generate OTP
- $otp_number = random_int(1000, 9999);
-
- // store OTP in database
- $otp = new OTP;
- $otp->code = $otp_number;
- $otp->mobile_no = $mobile_no;
- $otp->save();
-
- // sent OTP to the mobile number
- $otp->sendToMobile();
-
- return response()->json([
- 'message' => 'OK',
- 'data' => [
- 'account' => $account_exists
- ]
- ]);
- }
login()
- <?php // don't include this line, this is for code decoration
-
- public function login(Request $request) {
- $mobile_no = $request->mobile_no;
- $code = $request->otp;
-
- // validate Mobile Number
- if(!preg_match('/^[0-9]{10}+$/', $mobile_no)) {
- // raise Error
- return response()->json([
- 'message' => 'Invalid Mobile Number!'
- ], 401);
- }
-
- // validate OTP
- if(!preg_match('/^[0-9]{4}+$/', $code)) {
- // raise Error
- return response()->json([
- 'message' => 'Invalid OTP!'
- ], 401);
- }
-
- // fetch OTP from table
- $otp = OTP::where('mobile_no', $mobile_no)
- ->where('code', $code)
- ->where('active', 1)
- ->first();
-
- // check if OTP is valid
- if(!$otp || !$otp->isValidOTPTime()) {
- // send Error
- return response()->json([
- 'message' => 'Invalid OTP!'
- ], 401);
- }
-
- // update valid OTP
- $otp->active = 0;
- $otp->save();
-
- // check if account exists
- $user = User::firstWhere('mobile_no', $mobile_no);
- if(!$user) {
- // if there is not account
- // create account
- $user = new User;
- $user->mobile_no = $mobile_no;
- $user->password = bcrypt(strval(random_int(100000, 999999)));
- $user->save();
- }
-
- // assign user to the session
- Auth::login($user);
-
- // generate access token
- $tokenResult = $user->createToken('Personal Access Token');
- $token = $tokenResult->token;
-
- // mark token active for next five weeks
- $token->expires_at = Carbon::now()->addWeeks(5);
- $token->save();
-
- // send token
- return response()->json([
- 'user' => $user,
- 'access_token' => $tokenResult->accessToken,
- 'token_type' => 'Bearer',
- 'expires_at' => Carbon::parse(
- $tokenResult->token->expires_at
- )->toDateTimeString()
- ]);
- }
I have done the following (in sequence):
- Collect mobile number and OTP from request
- Apply validations on mobile number and OTP
- Fetched OTP from the table
- Checked if OTP is valid (OTP exists and OTP was sent less than 10 minutes ago)
- Marked OTP used (active = false)
- Fetched user assigned with mobile number
- If there is no user in the database, a new user is created.
- User assigned to the session (user logged in)
- Created Passport Access token for 5 weeks
- Sent user details along with access token back to the client.
logout() and refreshToken()
For revoking access for a user, I need to invalidate his access token. I will do this in logout() function.
- <?php // don't include this line, this is for code decoration
-
- public function logout(Request $request) {
- $request->user()->token()->revoke();
- return response()->json(['message' => 'Successfully logged out']);
- }
An access token is valid for only five weeks. So client need to refresh token before the existing token expires. refreshToken() function need to be updated as follow:
- <?php // don't include this line, this is for code decoration
-
- public function refreshToken(Request $request) {
- // get user from request/session
- $user = $request->user();
-
- // create a new access token
- $tokenResult = $user->createToken('Personal Access Token');
- $token = $tokenResult->token;
-
- // mark validity for five weeks
- $token->expires_at = Carbon::now()->addWeeks(5);
- $token->save();
-
- // send token
- return response()->json([
- 'user' => $user,
- 'access_token' => $tokenResult->accessToken,
- 'token_type' => 'Bearer',
- 'expires_at' => Carbon::parse(
- $tokenResult->token->expires_at
- )->toDateTimeString()
- ]);
- }
updateUser()
After registering a new user, I am collecting user's name and photo. This need to be saved in the database through a controller. Update updateUser() as follow:
- <?php // don't include this line, this is for code decoration
-
- public function updateUser(Request $request) {
- // get user from request/session
- $user = $request->user();
-
- // update name
- $user->name = $request->name;
-
- # check is photo is sent in request
- if ($request->hasFile('photo')) {
-
- # check if photo is valud
- if ($request->file('photo')->isValid()) {
-
- # save user photo on the server
- $url = $request->photo->store('public/avatars');
-
- # generate photo url
- $user->photo = Storage::url($url);
- }
- }
-
- // save user in database
- $user->save();
-
- // send updated user
- return response()->json($request->user());
- }
This function will store user's image in laravel_authentication/storage/app/public/avatars. But avatars folder does not exits by default. You need to create this folder. Laravel does not render files directly from storage. I am creating symlink from laravel_authentication/public to storage folder using
- php artisan storage:link
You can find some alternative methods here https://stackoverflow.com/questions/30191330/laravel-5-how-to-access-image-uploaded-in-storage-within-view
Final AuthController.php
- <?php
-
- namespace App\Http\Controllers\Auth;
-
- use App\Models\User;
- use App\Models\Account\OTP;
- use Carbon\Carbon;
- use Illuminate\Http\Request;
- use App\Http\Controllers\Controller;
- use Illuminate\Support\Facades\Auth;
- use Illuminate\Support\Facades\Storage;
-
- class AuthController extends Controller {
- public function sendOTP(Request $request) {
- // collect mobile number
- $mobile_no = $request->mobile_no;
-
- // validate mobile number
- if(!preg_match('/^[0-9]{10}+$/', $mobile_no)) {
- // raise Error
- return response()->json([
- 'message' => 'Invalid Mobile Number!'
- ], 403);
- }
-
- // check if account exists
- $user = User::firstWhere('mobile_no', $mobile_no);
- if($user) {
- $account_exists = true;
- } else {
- $account_exists = false;
- }
-
- // generate OTP
- $otp_number = random_int(1000, 9999);
-
- // store OTP in database
- $otp = new OTP;
- $otp->code = $otp_number;
- $otp->mobile_no = $mobile_no;
- $otp->save();
-
- // sent OTP to the mobile number
- $otp->sendToMobile();
-
- return response()->json([
- 'message' => 'OK',
- 'data' => [
- 'account' => $account_exists
- ]
- ]);
- }
-
- //
- public function login(Request $request) {
- $mobile_no = $request->mobile_no;
- $code = $request->otp;
-
- // validate Mobile Number
- if(!preg_match('/^[0-9]{10}+$/', $mobile_no)) {
- // raise Error
- return response()->json([
- 'message' => 'Invalid Mobile Number!'
- ], 401);
- }
-
- // validate OTP
- if(!preg_match('/^[0-9]{4}+$/', $code)) {
- // raise Error
- return response()->json([
- 'message' => 'Invalid OTP!'
- ], 401);
- }
-
- // fetch OTP from table
- $otp = OTP::where('mobile_no', $mobile_no)
- ->where('code', $code)
- ->where('active', 1)
- ->first();
-
- // check if OTP is valid
- if(!$otp || !$otp->isValidOTPTime()) {
- // send Error
- return response()->json([
- 'message' => 'Invalid OTP!'
- ], 401);
- }
-
- // update valid OTP
- $otp->active = 0;
- $otp->save();
-
- // check if account exists
- $user = User::firstWhere('mobile_no', $mobile_no);
- if(!$user) {
- // if there is not account
- // create account
- $user = new User;
- $user->mobile_no = $mobile_no;
- $user->password = bcrypt(strval(random_int(100000, 999999)));
- $user->save();
- }
-
- // assign user to the session
- Auth::login($user);
-
- // generate access token
- $tokenResult = $user->createToken('Personal Access Token');
- $token = $tokenResult->token;
-
- // mark token active for next five weeks
- $token->expires_at = Carbon::now()->addWeeks(5);
- $token->save();
-
- // send token
- return response()->json([
- 'user' => $user,
- 'access_token' => $tokenResult->accessToken,
- 'token_type' => 'Bearer',
- 'expires_at' => Carbon::parse(
- $tokenResult->token->expires_at
- )->toDateTimeString()
- ]);
- }
-
- public function logout(Request $request) {
- $request->user()->token()->revoke();
- return response()->json(['message' => 'Successfully logged out']);
- }
-
- public function updateUser(Request $request) {
- // get user from request/session
- $user = $request->user();
-
- // update name
- $user->name = $request->name;
-
- # check is photo is sent in request
- if ($request->hasFile('photo')) {
-
- # check if photo is valud
- if ($request->file('photo')->isValid()) {
-
- # save user photo on the server
- $url = $request->photo->store('public/avatars');
-
- # generate photo url
- $user->photo = Storage::url($url);
- }
- }
-
- // save user in database
- $user->save();
-
- // send updated user
- return response()->json($request->user());
- }
-
- public function refreshToken(Request $request) {
- // get user from request/session
- $user = $request->user();
-
- // create a new access token
- $tokenResult = $user->createToken('Personal Access Token');
- $token = $tokenResult->token;
-
- // mark validity for five weeks
- $token->expires_at = Carbon::now()->addWeeks(5);
- $token->save();
-
- // send token
- return response()->json([
- 'user' => $user,
- 'access_token' => $tokenResult->accessToken,
- 'token_type' => 'Bearer',
- 'expires_at' => Carbon::parse(
- $tokenResult->token->expires_at
- )->toDateTimeString()
- ]);
- }
- }
Routing
Laravel 8 provides routes/api.php for writing APIs. In this file, define the routes as follow.
- <?php
-
- use Illuminate\Http\Request;
- use Illuminate\Support\Facades\Route;
- use App\Http\Controllers\Auth\AuthController;
-
- /*
- |--------------------------------------------------------------------------
- | API Routes
- |--------------------------------------------------------------------------
- |
- | Here is where you can register API routes for your application. These
- | routes are loaded by the RouteServiceProvider within a group which
- | is assigned the "api" middleware group. Enjoy building your API!
- |
- */
-
- Route::group([
- 'prefix' => 'account'
- ], function () {
- Route::post('send-otp', [AuthController::class, 'sendOTP']);
- Route::post('login', [AuthController::class, 'login'])->name('login');
-
- Route::group([
- 'middleware' => 'auth:api'
- ], function() {
- Route::post('update-user', [AuthController::class, 'updateUser']);
- Route::post('refresh-token', [AuthController::class, 'refreshToken']);
- Route::get('logout', [AuthController::class, 'logout']);
- });
- });
In the above code, I have defined all five APIs that I had mentioned in the beginning of the post. Every API in the roues/api.php has a prefix api. In line 17, I wrapped the APIs in a Route::group with prefix account. This will add account after api, i.e., now APIs will start with /api/account/. In lines 20 and 21, I have added API for sending OTP and login respectively.
- Route::post('send-otp', [AuthController::class, 'sendOTP']);
In above line, Route::post means that this accepts a POST request. 'send-otp' is route path (i.e., resultant path is /api/account/send-otp). I have already defined AuthController class in project/controllers in the last section. [AuthController::class, 'sendOTP'] means that sendOTP function will be called with a request on this API is received.
Again in line 23, I placed rest three APIs in another Route::group with a middleware. A middleware is a function that will be executed before executing the API controller function. I have added 'auth:api' middleware provided by Laravel Passport. This middleware will ensure that requesting user is authenticated (logged in), otherwise this will raise authentication error.
Configure Passport
For using Laravel Passport, I had to do some more changes.
Update laravel_authentication/app/Providers/AuthServiceProvider.php
- <?php
-
- namespace App\Providers;
-
- use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
- use Illuminate\Support\Facades\Gate;
- use Laravel\Passport\Passport;
-
- class AuthServiceProvider extends ServiceProvider
- {
- /**
- * The policy mappings for the application.
- *
- * @var array
- */
- // add this
- protected $policies = [
- 'App\Models\Model' => 'App\Policies\ModelPolicy',
- ];
-
- /**
- * Register any authentication / authorization services.
- *
- * @return void
- */
- public function boot()
- {
- $this->registerPolicies();
-
- // add this line
- Passport::routes();
- }
- }
This will register Laravel Passport routes. Also update gaurds in laravel_authentication/config/auth.php as
- <?php // don't include this line, this is for code decoration
-
- 'guards' => [
- 'web' => [
- 'driver' => 'session',
- 'provider' => 'users',
- ],
-
- // update these lines
- 'api' => [
- 'driver' => 'passport',
- 'provider' => 'users',
- ],
- ],
CORS Middleware
For using APIs on cross-domain, CORS Middleware is required. Create a new middleware file laravel_authentication/app/Http/Middleware/Cors.php and add the following code in it.
- <?php
-
- namespace App\Http\Middleware;
-
- use Closure;
- use Illuminate\Http\Request;
-
- class Cors
- {
- /**
- * Handle an incoming request.
- *
- * @param \Illuminate\Http\Request $request
- * @param \Closure $next
- * @return mixed
- */
- public function handle(Request $request, Closure $next)
- {
- return $next($request)
- ->header('Access-Control-Allow-Origin', '*')
- ->header('Access-Control-Allow-Methods',
- 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
- ->header('Access-Control-Allow-Headers',
- 'Content-Type, Authorization, X-Requested-With, X-XSRF-TOKEN');
- }
- }
Now register new middleware in app/Http/Kernal.php as:
- <?php // don't include this line, this is for code decoration
-
- protected $middleware = [
- // \App\Http\Middleware\TrustHosts::class,
- \App\Http\Middleware\TrustProxies::class,
- \Fruitcake\Cors\HandleCors::class,
- \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
- \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
- \App\Http\Middleware\TrimStrings::class,
- \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
-
- // add this line
- \App\Http\Middleware\Cors::class,
- ];
Final Step
Now test the APIs using POSTMAN (you can use alternatives like curl). All APIs are working perfectly for me. Here is my POSTMAN Collection https://www.getpostman.com/collections/83b7b35ef90cb3b3c7cd.
For starting Larvel server you need to run:
- php artisan serve
Server will start at localhost:8000
For more advanced demo, you can check my tutorial on Blog demo in Laravel.