Passwordless OTP based login, Returning JWT with tymon/jwt-auth
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 theIlluminate\Contracts\Auth\Authenticatable
interface. BecauseIlluminate\Contracts\Auth\UserProvider
contract’svalidateCredentials
method requires an object ofAuthenticatable
as a parameter. JWTGuard
requires the provider to have a concrete implementation ofretrieveById
,retrieveByCredentials
. Other methods ofUserProvider
can be left unimplemented. Because they’re not used by theJWTGuard
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.driver
‘s 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?
- Call handled by the guard in AuthManager.
- The call is resolved and forwarded to the
JWTGuard
by AuthManager because theJWTGuard
was registered in the Service Provider. JWTGuard::attempt
gets the next call when callingattempt
onauth()
.- Attempt then forwards the call to our provider’s
retrieveByCredentials
method. - If the subject is returned as non-null, then forwards the call to
validateCredentials
to our provider. - If the above methods return anything truthy in combined, then it returns a token for the subject.
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. ❤