Passwordless OTP based login, Returning JWT with tymon/jwt-auth

Syed Sirajul Islam Anik
5 min readJul 22, 2020

--

Image from: https://cloud.githubusercontent.com/assets/1801923/9915273/119b9350-5cae-11e5-850b-c941cac60b32.png

This is my fourth Laravel based Authentication related article till now. And, this is the second article for JWT based authentication with tymon/jwt-auth package. So, if you missed the previous articles or you’ve any quest to learn the Laravel’s authentication system, or you want to implement cache on your authentication system, then have a look at the following article links. Cheers.

History & what is this article about?

So, I was thinking to develop a system where you can users can log in without any password. Users will be given an OTP over the phone or an email and with that OTP you will let that user login. But, the shipped authentication system for the above package doesn’t support that. Without any customization, you’ll have to provide the username or email and the password to authenticate a user. So, this article actually provides the solution to achieve the goal. Let’s get our hands dirty.

Prerequisite?

As you’re here, you know PHP I assume. So, that’s all. I’ll go through all the points to make it easy for you. Bear with me.

Solution

The solution to this problem is to create your own provider which satisfies the Illuminate\Contracts\Auth\UserProvider interface. We’ll create that in a moment. But before that, from the first article mentioned above, we know (if you have read the article) that the Authentication’s entry point is resolved by the Illuminate\Auth\AuthManager class. If you’re using auth() method or Auth facade or $request->user(), that forwards the calls to the AuthManager class.

As we’re using the tymon/jwt-auth package, upon setting up correctly, if we explicitly mention the auth guard as api or the api as a default guard, the AuthManager will forward all the authentication-related calls to the Tymon\JWTAuth\JWTGuard class.

Points to keep in mind

  • If your subject (the model that will verify the user’s email or phone) is the User model, then you’re good to go. Otherwise, the subject must have to implement the Illuminate\Contracts\Auth\Authenticatable interface. Because Illuminate\Contracts\Auth\UserProvider contract’s validateCredentials method requires an object of Authenticatable as a parameter.
  • JWTGuard requires the provider to have a concrete implementation of retrieveById, retrieveByCredentials. Other methods of UserProvider can be left unimplemented. Because they’re not used by the JWTGuard as of the time I am writing this article.

The codebase

We’ll assume we have a model App\Models\Member that will have the phone numbers through which we will verify if the user exists or not.

So, our model Member will be like this. As we are not using the User model, we had to implement the Authenticatable interface. Even though the model doesn’t do anything with those methods, we just had to declare those methods. And, JWTSubject interface is used because the model Member is our subject. And it’s required by the package. That’s all for the model.

Now, we have to create a new provider that will be liable to validate the user/member when the auth package tries to authenticate the user. This class will also provide the user with the identifier.

As we require our provider to implement the UserProvider interface, we had to implement all the methods that the interface had. But, as we don’t have any logic for the implementation of how retrieveByToken, updateRememberToken should behave, when called will raise an exception when called. The other three methods are self-explanatory. Whenever the JWTGuard require to retrieve a subject by an identifier, it’ll call the retrieveById method on the provider, it should return an object of our subject or null if not found. Laravel’s implementation of retrieveByCredentials for its returns an object or null for the Authenticatable implemented object. We did the same in that method. The validateCredentials method validates the subject returned by the retrieveByCredentials method.

That’s all for the coding. Now, we need to integrate our classes. To integrate our class with Laravel/Lumen, we’ll update our App\Providers\AuthServiceProvider class. You can add it to any service provider. But as it’s related to auth, I am adding this in AuthServiceProvider.

And, in our config/auth.php (for Lumen, you’ll have to copy this file from the vendor/laravel/lumen-framework/config/auth.php directory and then put it in your project’s config directory, and then register this configuration) we’ll have to add like below.

<?php// config/auth.php
return [
'defaults' => [
'guard' => env('AUTH_GUARD', 'api'),
],
'guards' => [
'api' => [
'driver' => 'jwt',
'provider' => 'otp-user', // Previously 'users'
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
// Following array is newly added to config
'otp-user' => [
'driver' => 'otp-based-auth-provider'
],

],
'passwords' => [],
];

In our config, auth.guards.api.provider‘s value is otp-user, which points to the auth.providers.otp-user array. Here otp-user is just like a variable string. You can use whatever you want. Next, auth.providers.otp-user.drivers value is otp-based-auth-provider which points to the value of the register method's registered provider in AuthServiceProvider.

$this->app[‘auth’]->provider(‘otp-based-auth-provider’,function (){});

Here, otp-based-auth-provider is also a string. You can choose whatever you want.

How does it work in code then?

So, now in your Authentication Controller, you can do it like

<?phpclass AuthController extends Controller 
{
public function login (Request $request) {
$otp = $request->get('otp');
$phone = 'YOUR-VALID-PHONE-NUMBER';
// you may skip the 'api' if you set your default guard
$token = auth('api')->attempt([
'otp' => $otp,
'phone' => $phone,

]);
// array in attempt method are the credentials
// which will be received in the
// MemberUserProvider::retrieveByCredentials
// MemberUserProvider::validateCredentials
return $token ? [ 'token' => $token ] : [ 'error' => true ];
}
}

If your retrieveByCredentials method returned a subject (Member in our case) and validateCredentials returned true, then the subject is verified and a token is returned by the JWTGuard. But, if any of the methods return null or false, then the user will not be verified and token will be false. For simplicity, we checked the OTP value with 12345. You can put your complex logic there.

So far so good. Wanna trace the calls?

So, that’s all for the passwordless OTP based login with tymon/jwt-auth. And I hope it helps you.

After you got your hands dirty, make sure to wash them thoroughly at least in this pandemic situation. And wash them too often.

Happy coding. ❤

--

--

Syed Sirajul Islam Anik

software engineer with "Senior" tag | procrastinator | programmer | !polyglot | What else 🙄 — Open to Remote