I don't understand JWT refresh token's behaviour (LARAVEL) - php

I have just tried JWT auth with LARAVEL and this https://github.com/tymondesigns/jwt-auth
But there's something i can't understand. In their config they put :
'ttl' => env('JWT_TTL', 60), // in munutes
'refresh_ttl' => env('JWT_REFRESH_TTL', 20160), // in minutes
What i understant : the token's live is 1hour and can be refreshed within 2 weeks
But after 3hours, if i try to query something, it says "token expired".
Does this system mean, a user must get his token updated / refreshed within every hour but with a limit of 2 weeks ? I don't get it.
How can a user persist login with this kind of system ? How is the refresh Token useful when after the first hour, though it hasn't been 2 weeks yet, i can't get a fresh token ?
thanks
UPDATE: CODE
config/jwt.php
'ttl' => 2, // 2 minutes
'refresh_ttl' => 5, // 5 minutes
routes/api.php
Route::post('/login', 'AuthController#login');
Route::get('/test', 'AuthController#test')->middleware('jwt.auth', 'jwt.refresh');
Http/Controllers/AuthController
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use JWTAuth;
use Tymon\JWTAuth\Exceptions\JWTException;
class AuthController extends Controller
{
public function test()
{
return response()->json(['coucou' => 1]);
}
public function login(Request $request)
{
// grab credentials from the request
$credentials = $request->only('email', 'password');
try {
// attempt to verify the credentials and create a token for the user
if (! $token = JWTAuth::attempt($credentials)) {
return response()->json(['error' => 'invalid_credentials'], 401);
}
} catch (JWTException $e) {
// something went wrong whilst attempting to encode the token
return response()->json(['error' => 'could_not_create_token'], 500);
}
// all good so return the token
return response()->json(compact('token'));
}
}
AND THIS IS THE FLOW :
request to /login with {username: xxx, password: xxx}
response of /login > {token: xxxxxxx}
request to /test straight after (10 secs) with Bearer xxxxxx
response of /test > the good json response with NEW TOKEN in HEADER
request to /test after 3 minutes (so 3mins 10 secs have past now, less than the 5min of refresh limit)
response of /test > token expired
I don't understand.

After the access token is expired you can use the refresh token to get a new access token without asking the user to input his username and password again.
Only after the refresh token is expired, the user needs to login again.
But after 3hours, if i try to query something, it says "token expired".
that's because the access token is expired.
Does this system mean, a user must get his token updated / refreshed within every hour but with a limit of 2 weeks ? I don't get it.
yes. You keep the refresh token in your client system and use it to request a new access token when the access token is expired.

Ok, finally I have something that works.
Remove "jwt.refresh" from your middleware. This is for one-pass-tokens as I´ve commented above.
I couldn´t get JWTAuth::refresh() to work with blacklists enabled. A "TokenBlacklistedException" is thrown when I call JWTAuth::refresh() even though I know its only expired since I do this in the catch block for "TokenExpiredException". Bug? Workaround:
JWT_BLACKLIST_ENABLED=false
You need to have a refresh endpoint you can call if /test returns 401. I use same as login, but it´s kind of custom in my case.
...
try
{
if($token = JWTAuth::getToken())
{
JWTAuth::checkOrFail();
}
$user = JWTAuth::authenticate();
}
catch(TokenExpiredException $e)
{
JWTAuth::setToken(JWTAuth::refresh());
$user = JWTAuth::authenticate();
}
if($user /*&& check $user against parameter or not*/)
{
return response()->json([
'user' => $user->profile(),
'accessToken'=> JWTAuth::getToken()->get(),
], 200);
}
else
{
return response()->json(false, 401); //show login form
} ...

This is what I did for mine, I have to set the token time to be valid to 24 hours by doing this
'ttl' => env('JWT_TTL', 1400)
I changed the 60 to 1440, and my token now last for a day.

Related

Quickbooks PHP API: Refresh OAuth 2 Token Failed

I'm trying to access Quickbooks API using the PHP SDK but getting the following error:
Refresh OAuth 2 Access token with Refresh Token failed. Body: [{"error":"invalid_grant"}].
My Tokens seem to work for 24 hours but after that I receive the error above. Each time I call the API, I am saving my updated tokens to my database:
//Client ID & Secret
$qbClientId = $this->scopeConfig->getValue('quickbooks/api/qb_client_id', $storeScope);
$qbClientSecret = $this->scopeConfig->getValue('quickbooks/api/qb_client_secret', $storeScope);
//Retrieve currently saved Refresh_Token from DB
$qbRefreshToken = $this->scopeConfig->getValue('quickbooks/api/qb_refresh_token', $storeScope);
$OAuth2LoginHelper = new OAuth2LoginHelper($qbClientId, $qbClientSecret);
$accessTokenObj = $OAuth2LoginHelper->refreshAccessTokenWithRefreshToken($qbRefreshToken);
$error = $OAuth2LoginHelper->getLastError();
if($error) {
throw new \Exception($error);
} else {
// The refresh token and access token expiration
$refreshTokenValue = $accessTokenObj->getRefreshToken();
$refreshTokenExpiry = $accessTokenObj->getRefreshTokenExpiresAt();
// Save new Refresh Token & Expiry to DB
$this->configInterface->saveConfig('quickbooks/api/qb_refresh_token', $this->encryptor->encrypt($refreshTokenValue), 'default', 0);
$this->configInterface->saveConfig('quickbooks/api/qb_refresh_token_expiry', $refreshTokenExpiry, 'default', 0);
// The access token and access token expiration
$accessTokenValue = $accessTokenObj->getAccessToken();
$accessTokenExpiry = $accessTokenObj->getAccessTokenExpiresAt();
// Save new Access Token & Expiry to DB
$this->configInterface->saveConfig('quickbooks/api/qb_access_token', $this->encryptor->encrypt($accessTokenValue), 'default', 0);
$this->configInterface->saveConfig('quickbooks/api/qb_access_token_expiry', $accessTokenExpiry, 'default', 0);
return DataService::Configure(array(
'auth_mode' => 'oauth2',
'ClientID' => $qbClientId,
'ClientSecret' => $qbClientSecret,
'accessTokenKey' => $accessTokenValue,
'refreshTokenKey' => $refreshTokenValue,
'QBORealmID' => 'MyRealmID',
'baseUrl' => 'Development'
));
}
So as you can see, on each API call, I'm using the refreshAccessTokenWithRefreshToken($qbRefreshToken) method to get new Refresh and Access Tokens and saving those to my DB for next use, however I still receive invalid_grant errors after 24hours.
Any ideas?
I struggled with the same problem. In my case, I had updated the tokens in the database, but not everywhere in memory (specifically in the $dataService object). So continuing to use the $dataService object to make API calls was using the old tokens. D'oh!
Although I think it would still work, you do not need to refresh the tokens with every API call (as David pointed out). My solution was to make the API call and if it fails, then I refresh the tokens and make the API call again. Here is a simplified version of my code:
$user = ...; // get from database
$dataService = getDataServiceObject();
$response = $dataService->Add(...); // hit the QBO API
$error = $dataService->getLastError();
if ($error) {
refreshTokens();
$response = $dataService->Add(...); // try the API call again
}
// ... "Add" complete, onto the next thing
////////////////////////////////
function getDataServiceObject() {
global $user;
return DataService::Configure(array(
'auth_mode' => 'oauth2',
'ClientID' => '...',
'ClientSecret' => '...',
'accessTokenKey' => $user->getQbAccessToken(),
'refreshTokenKey' => $user->getQbRefreshToken(),
'QBORealmID' => $user->getQbRealmId(),
'baseUrl' => '...',
));
}
function refreshTokens() {
global $dataService;
global $user;
$OAuth2LoginHelper = $dataService->getOAuth2LoginHelper();
$obj = $OAuth2LoginHelper->refreshAccessTokenWithRefreshToken($user->getQbRefreshToken());
$newAccessToken = $obj->getAccessToken();
$newRefreshToken = $obj->getRefreshToken();
// update $user and store in database
$user->setQbAccessToken($newAccessToken);
$user->setQbRefreshToken($newRefreshToken);
$user->save();
// update $dataService object
$dataService = getDataServiceObject();
}
https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/faq
Why does my refresh token expire after 24 hours?
Stale refresh tokens expire after 24 hours. Each time you refresh the access_token a new refresh_token is returned with a lifetime of 100 days. The previous refresh_token is now stale and expires after 24 hours. When refreshing the access_token, always use the latest refresh_token returned to you.
Are you sure that the latest refresh token is used?
So as you can see, on each API call, I'm using the refreshAccessTokenWithRefreshToken($qbRefreshToken) method to get new Refresh and Access Tokens and saving those to my DB for next use...
Why are you requesting a new access token and refresh token every time you make a request? Why are not checking if the old access token is stil valid?
As you stated above, you are even storing the expiration time of the access token. So you should know if it is still valid or not.
So when you are making the API request and you access token has expired, you will get a error message. In return, you can now request a new one.

Passing response from social provider back to API endpoint

I am trying to add social authentication to a laravel 5.8 API application using socialite. Following the documentation here https://laravel.com/docs/5.8/socialite#routing I created a SocialAuthController that wiill redirect the user to the provider auth page and handle the callback like this
...
use Socialite;
...
public function redirectToProvider($provider)
{
return Socialite::driver($provider)->redirect();
}
public function handleProviderCallback($provider)
{
// retrieve social user info
$socialUser = Socialite::driver($provider)->stateless()->user();
// check if social user provider record is stored
$userSocialAccount = SocialAccount::where('provider_id', $socialUser->id)->where('provider_name', $provider)->first();
if ($userSocialAccount) {
// retrieve the user from users store
$user = User::find($userSocialAccount->user_id);
// assign access token to user
$token = $user->createToken('string')->accessToken;
// return access token & user data
return response()->json([
'token' => $token,
'user' => (new UserResource($user))
]);
} else {
// store the new user record
$user = User::create([...]);
// store user social provider info
if ($user) {
SocialAccount::create([...]);
}
// assign passport token to user
$token = $user->createToken('string')->accessToken;
$newUser = new UserResource($user);
$responseMessage = 'Successfully Registered.';
$responseStatus = 201;
// return response
return response()->json([
'responseMessage' => $responseMessage,
'responseStatus' => $responseStatus,
'token' => $token,
'user' => $newUser
]);
}
}
Added the routes to web.php
Route::get('/auth/{provider}', 'SocialAuthController#redirectToProvider');
Route::get('/auth/{provider}/callback', 'SocialAuthController#handleProviderCallback');
Then I set the GOOGLE_CALLBACK_URL=http://localhost:8000/api/v1/user in my env file.
When a user is successfully authenticated using email/password, they will be redirected to a dashboard that will consume the endpoint http://localhost:8000/api/v1/user. So in the google app, I set the URI that users will be redirected to after they are successfully authenticated to the same endpoint http://localhost:8000/api/v1/user
Now when a user tries to login with google, the app throws a 401 unauthenticated error.
// 20190803205528
// http://localhost:8000/api/v1/user?state=lCZ52RKuBQJX8EGhz1kiMWTUzB5yx4IZY2dYmHyJ&code=4/lgFLWpfJsUC51a9yQRh6mKjQhcM7eMoYbINluA58mYjs5NUm-yLLQARTDtfBn4fXgQx9MvOIlclrCeARG0NC7L8&scope=email+profile+openid+https://www.googleapis.com/auth/userinfo.profile+https://www.googleapis.com/auth/userinfo.email&authuser=0&session_state=359516252b9d6dadaae740d0d704580aa1940f1d..10ea&prompt=none
{
"responseMessage": "Unauthenticated",
"responseStatus": 401
}
If I change the URI where google authenticated users should be redirect to like this GOOGLE_CALLBACK_URL=http://localhost:8000/auth/google/callback the social user information is returned.
So how should I be doing it. I have been on this for a couple of days now.
That is because you haven't put authorization in your header with your request.
you don't need to redirect user if you are working with token, your app should be a spa project, so you will redirect him from your side using js frameworks.
You need to send Authorization in your headers plus you need to specify it with your token which you returned it in your response like this:
jQuery.ajaxSetup({
headers: {
'Authorization': 'Bearer '+token
}
});
or if you are using axios
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;

ArgumentCountError in Laravel 5.8

I am trying to add social authentication to a Laravel 5.8 API project using socialite.
When trying to handle a social provide callback, the ArgumentCountError is thrown here
Too few arguments to function App\Http\Controllers\SocialAuthController::handleProviderCallback(), 0 passed and exactly 1 expected
The error is referring to the very first line of this code block
public function handleProviderCallback($provider)
{
// retrieve social user info
$socialUser = Socialite::driver($provider)->stateless()->user();
// check if social user provider record is stored
$userSocialAccount = SocialAccount::where('provider_id', $socialUser->id)->where('provider_name', $provider)->first();
if ($userSocialAccount) {
// retrieve the user from users store
$user = User::find($userSocialAccount->user_id);
// assign access token to user
$token = $user->createToken('Pramopro')->accessToken;
// return access token & user data
return response()->json([
'token' => $token,
'user' => (new UserResource($user))
]);
} else {
// store the new user record
$user = User::create([
'name' => $socialUser->name,
'username' => $socialUser->email,
'email_verified_at' => now()
]);
...
// assign passport token to user
$token = $user->createToken('******')->accessToken;
// return response
return response()->json(['token' => $token]);
}
}
Below is how I have set up other code. Frist in env I added
GOOGLE_CLIENT_ID=******
GOOGLE_CLIENT_SECRET=*******
GOOGLE_CALLBACK_URL=https://staging.appdomain.com/api/v1/user
Then modified web.php
Auth::routes(['verify' => true]);
Route::get('/auth/{provider}', 'SocialAuthController#redirectToProvider');
Route::get('/auth/{provider}/callback', 'SocialAuthController#handleProviderCallback');
Lastly in the google app, I added the uri path where users will be redirected to after successful authentication
https://staging.appdomain.com/api/v1/user
How do I fix this?
The callback uri that user should be redirected to after successful authentication was apparently not being cached. So running php artisan route:cache fixed it.

Authentication with JWT Laravel 5 without password

I'm trying to learn Laravel and my goal is to be able to build a RESTful API (no use of views or blade, only JSON results. Later, an AngularJS web app and a Cordova hybrid mobile app will consume this api.
After some research, I'm inclining to choose JWT-Auth library for completely stateless benefit. My problem is: I have 2 main types of users: customers and moderators. Customers are not required to have a password. I need to be able to generate a token for access with the provided email only. If that email exists in the database and it belongs to a customer, it will generate and return the token.
If it exists and belongs to a moderator, it will return false so the interface can request a password. If the email doesn't exist, it throws an invalid parameter error.
I read the docs here and it says it's possible to use Custom Claims. But the docs doesn't explain what are claims and what it means the array being passed as custom claims. I'd like some input on how to go about achieving what I explain above.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\Http\Controllers\Controller;
use JWTAuth;
use Tymon\JWTAuth\Exceptions\JWTException;
class AuthenticateController extends Controller
{
public function authenticate(Request $request)
{
$credentials = $request->only('email', 'password');
try {
// verify the credentials and create a token for the user
if (! $token = JWTAuth::attempt($credentials)) {
return response()->json(['error' => 'invalid_credentials'], 401);
}
} catch (JWTException $e) {
// something went wrong
return response()->json(['error' => 'could_not_create_token'], 500);
}
// if no errors are encountered we can return a JWT
return response()->json(compact('token'));
}
}
Thanks you.
Update
Bounty's code
public function authenticate(Request $request) {
$email = $request->input('email');
$user = User::where('email', '=', $email)->first();
try {
// verify the credentials and create a token for the user
if (! $token = JWTAuth::fromUser($user)) {
return response()->json(['error' => 'invalid_credentials'], 401);
}
} catch (JWTException $e) {
// something went wrong
return response()->json(['error' => 'could_not_create_token'], 500);
}
// if no errors are encountered we can return a JWT
return response()->json(compact('token'));
}
try with this:
$user=User::where('email','=','user2#gmail.com')->first();
if (!$userToken=JWTAuth::fromUser($user)) {
return response()->json(['error' => 'invalid_credentials'], 401);
}
return response()->json(compact('userToken'));
it works for me, hope can help
Generating token for the customers (without password) can be achieved through
$user = \App\Modules\User\Models\UserModel::whereEmail('xyz#gmail.com')->first();
$userToken=JWTAuth::fromUser($user);
Here $userToken
will stores the token after existence check of email in the table configured in UserModel file.
I have assumed that you stores both customer and moderators in the same table, there must be some flag to discriminate among them. Assume the flag is user_type
$token = null;
$user = \App\Modules\User\Models\UserModel::whereEmail('xyz#gmail.com')->first();
if($user['user_type'] == 'customer'){
$credentials = $request->only('email');
$token =JWTAuth::fromUser($user);
}else if($user['user_type'] == 'moderator'){
$credentials = $request->only('email','password');
$token = JWTAuth::attempt($credentials);
}else{
//No such user exists
}
return $token;
As far as custom claims are concerned these are custom defined payloads which can be attached to token string.
For example, JWTAuth::attempt($credentials,['role'=>1]); Will attempt to add role object to token payload.
Once you decode the token string through JWT Facade JWTAuth::parseToken()->getPayload(); you in turn get all payloads defined in required_claims under config/jwt.php with additional role payload.
Refer https://github.com/tymondesigns/jwt-auth/wiki/Creating-Tokens#creating-a-token-based-on-anything-you-like
Let me know in case you requires anything else.
Rather than making a different login strategy for customers and moderators, you can add token authentication to both user type. this will makes your life easier and prepare for scalability.
In your api, you can just restrict moderator users to not have access to the api by sending
<?php
Response::json('error'=>'method not allowed')
Apart from this suggestion, I believe #Alimnjan code should work.
If you don't already have an App\User object, get it with something like
$user = App\User::find(1);
Generate the token using the fromUser() method of JWTAuth
$token = \JWTAuth::fromUser($user)
The above doesn't authenticate the user, it only generates a JWT token. If you need to authenticate the user, then you have to add something like this
\JWTAuth::setToken($token)->toUser();

JWT Auth custom user token

I'm using laravel 5 and JWTauth package. I'm wondering if it's possible to use a custom token to authenticate a user. Instead of encoding the user's full details, just encode id and email.
The reason why I want to do this is because every time the user updates his details, the app needs to generate new token and update the header bearer token. Otherwise the token is invalid. Is there other way/better way to do this?
I would appreciate your recommendation. Thanks!
You can obtain the token in this way.
$user = User::first();
$token = JWTAuth::fromUser($user);
or
try {
//attempt to verify the credentials and create a token for the user
if(!$userToken = JWTAuth::attempt($credentials)) {
return response()->json(['error' => 'invalid_credentials'], 401);
}
} catch(JWTException $e) {
return response()->json(['error' => 'could_not_create_token'], 500);
}

Categories