Using JWT Auth along with existing authenticatiion system - php

So, this is the first time that I am trying to implement auth in my API. I already had an existing authentication system - Cartalyst Sentinel 2.0 and now to add auth I am using JWT.
What I have done is :
Send a token to the client end at the time of login.
Replaced the existing authentication sentinel middleware with a new middleware that does both authentication and authorization.
Original Middleware :
$authenticateForLogin = function ($app, $user) {
return function () use ($app, $user) {
if ( Sentinel::check() === false ) {
$app->response()->status(401);
$app->stop();
}
};
};
New Middleware :
$checkForAuthorization = function ($app, $user) {
return function () use ($app, $user) {
if ( Sentinel::check() === false ) {
$app->response()->status(401);
$app->stop();
} else {
$authHeader = apache_request_headers()["Authorization"];
if ($authHeader) {
$jwt = str_replace("Authorization: Bearer ", "", $authHeader);
if ($jwt) {
try {
$secretKey = base64_decode(getDbConfig()["AUTH_SECRET"]);
$token = JWT::decode($jwt, $secretKey, array('HS512'));
header('Content-type: application/json');
echo json_encode([
'message' => "Auth Test Successful"
]);
} catch (Exception $e) {
//some action
}
} else {
//some action
}
} else {
//some action
}
}
};
};
So my questions are -
Is this the right approach ? I am under the impression that
authentication and authorization are two separate processes. Are there any security flaws to this ?
Sentinel authentication is cookies, session based system. So is it
good to use something like JWT on the top of it ? Or should I do
authentication also using JWT (don't know how yet) ?
I have lot of doubts regarding Auth/JWT. But these come first.

Related

Registering Shopify Webhooks - Laravel

I am creating a shopify app and currently, i want to register webhooks in my app so that if a customer is created, a notification/sms to sent to the admin.
According to my research, it can be registered through the shop settings but in this case, i want to register it via the app. But there are not enough resources to get this done.. Below is what i have but when i create a customer, my sms is not sent to the admin..
What could i be missing out now ?
VerifyWebhook
public function handle($request, Closure $next)
{
$hmac = request()->header('x-shopify-hmac-sha256') ?: '';
$shop = request()->header('x-shopify-shop-domain');
$data = request()->getContent();
// From https://help.shopify.com/api/getting-started/webhooks#verify-webhook
$hmacLocal = base64_encode(hash_hmac('sha256', $data, env('SHOPIFY_SECRET'), true));
if (!hash_equals($hmac, $hmacLocal) || empty($shop)) {
// Issue with HMAC or missing shop header
abort(401, 'Invalid webhook signature');
}
return $next($request);
}
Route
Route::post('webhook/shopify/customer-created', function(\Illuminate\Http\Request $request) {
// Handle customer created and sms or notification
})->middleware('webhook');
It looks like you are trying to verify webhook instead of creating one. Please go through below process
Create a webhook using simple request:
POST /admin/webhooks.json
{
"webhook": {
"topic": "customers/create",
"address": "https://whatever.hostname.com/",
"format": "json"
}
}
ofcourse you will have to pass shopify auth token in headers.
If you want to use a package to ease the process you can use: https://github.com/oseintow/laravel-shopify
your routes.php/web.php
Route::get('/register-webhook', 'WebhooksController#registerCustomerWebhook')->name('customer');
Route::get('/webhooks/customer-created', 'WebhooksController#customerCreated')->name('customerCreated');
and then import and use it -->
use Oseintow\Shopify\Facades\Shopify;
.
.
// create a webhook
public function registerCustomerWebhook(...){
Shopify::setShopUrl($shopUrl)->setAccessToken($accessToken)->post("admin/webhooks.json", ['webhook' =>
['topic' => 'customers/create',
'address' => 'https://whatever.hostname.com/path',
'format' => 'json'
]
]);
.
}
Verifying webhook:
public function customerCreated(...) {
if (Shopify::verifyWebHook($data, $hmacHeader)) {
// do your stuffs here in background
return response('Hello World', 200)
->header('Content-Type', 'text/plain');
} else {
return response('UnAuthorized', 401)
->header('Content-Type', 'text/plain');
}
.
.
}
Note:
Your endpoint must have proper ssl certificates installed
You need to respond to an webhook as quickly as possible. Its better to do your tasks in background.
Let me know if there is any confusion.

Authenticate usertoken before app run with Slim 3

I am using Slim Framework 3 to make a small internal API to get fetch facebook data. There is about 30 specific users which have access to the API.
I want to authenticate a user by a user token send from the website, and that token is to be checked before the app is run.
The token on the user is set in the DB and when the user is requesting the API a token is send with a GET and if there is a match on the DB and the GET token, the user should be granted access to the API, otherwise the user should be forbidden to access.
I am using this to get facebook data:
$app->get('/fbdata/campaign/{campaign}/bankarea/{bankarea}/from/{from}/to/{to}/utoken/{utoken}', function(Request $request, Response $response) {
$bd = new BankAppData();
$getFb = new GetFacebookData();
$bankarea = $request->getAttribute('bankarea');
$campaign = $request->getAttribute('campaign');
$appid = $bd->BankData($bankarea)->appid;
$appsecret = $bd->BankData($bankarea)->appsecret;
$fbtoken = $bd->BankData($bankarea)->fbtoken;
$dateFrom = $request->getAttribute('from');
$dateTo = $request->getAttribute('to');
$getFb->FetchData($appid, $appsecret, $fbtoken, $campaign, $bankarea, "act_XXXX", $dateFrom, $dateTo);
});
This works just fine, but I want to use a AuthenticationHandler class for checking the utoken before the above is run.
I am adding it by using $app->add(new SNDB\AuthenticationHandler()); but I am unsure on how I can get the utoken from the URL in my AuthenticationHandler class.
Basically I want to do something like
function Authenticate() {
if($dbToken != $utoken) {
//No access - app will just stop doing anything else
} else {
//You have access - just continue what you was trying to do
}
}
You should take a look at the middleware concept from slim3.
Basically there are 2 options how to add middleware:
per anonymous function
$app->add(function ($request, $response, $next) {
$response->getBody()->write('BEFORE');
$response = $next($request, $response);
$response->getBody()->write('AFTER');
return $response;
});
per invokable class
class ExampleMiddleware
{
public function __invoke($request, $response, $next)
{
$response->getBody()->write('BEFORE');
$response = $next($request, $response);
$response->getBody()->write('AFTER');
return $response;
}
}
$app->add(new ExampleMiddleware);
There you have the PSR-7 request and can get your utoken from the url.

Laravel - JWT Auth The token could not be parsed from the request

I have added following code in my middleware for user authentication with JWT Auth, which works fine for all the routes handled by the middleware.
public function handle($request, Closure $next)
{
if ($request->has('token')) {
try {
$this->auth = JWTAuth::parseToken()->authenticate();
return $next($request);
} catch (JWTException $e) {
return redirect()->guest('user/login');
}
}
}
But for one route with Post Method where the token is getting passed properly but still I am getting :
JWTException - The token could not be parsed from the request
on the same route when I tried :
public function handle($request, Closure $next)
{
if ($request->has('token')) {
try {
dd($request->input('token'));
$this->auth = JWTAuth::parseToken()->authenticate();
return $next($request);
} catch (JWTException $e) {
return redirect()->guest('user/login');
}
}
}
Output :
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9iaXNzIjoiaHR0cDpcL1wvbG9jYWxob3N0OjgwMDFcL2F1dGhcL2xvZ2luIiwiaWF0IjoxNDcyNTI4NDU0LCJleHAiOjE0NzI1MzIwNTQsIm5iZiI6MTQ3MjUyODQ1NCwianRpIjoiM2E0M2ExYTZlNmM5NjUxZDgxYjZhNDcxMzkxODJlYjAifQ.CH8ES2ADTCrVWeIO8uU31bGDnH7h-ZVTWxrdXraLw8s"
I am able to see the Valid Token which I am using to access another routes and which is working flawlessly for all other routes.
Thanks in advance!!!
From your description, I checked source file of JWT Auth.
In class Tymon\JWTAuth\JWTAuth line 191 - 219 , there are two functions:
/**
* Parse the token from the request.
*
* #param string $query
*
* #return JWTAuth
*/
public function parseToken($method = 'bearer', $header = 'authorization', $query = 'token')
{
if (! $token = $this->parseAuthHeader($header, $method)) {
if (! $token = $this->request->query($query, false)) {
throw new JWTException('The token could not be parsed from the request', 400);
}
}
return $this->setToken($token);
}
/**
* Parse token from the authorization header.
*
* #param string $header
* #param string $method
*
* #return false|string
*/
protected function parseAuthHeader($header = 'authorization', $method = 'bearer')
{
$header = $this->request->headers->get($header);
if (! starts_with(strtolower($header), $method)) {
return false;
}
return trim(str_ireplace($method, '', $header));
}
Check the logic of them, I believe your request header is not properly provided.
if (! $token = $this->parseAuthHeader($header, $method)) { // all your get method not passed this step
if (! $token = $this->request->query($query, false)) { // all your post method stucked here
throw new JWTException('The token could not be parsed from the request', 400);
}
}
A properly formatted header looks like this :
http POST http://${host}/api/v1/product/favorite/111 "Authorization: Bearer ${token}"
That's all I can offer to you, hope it will help you through your thoughts. If it won't you can still debug those two functions.
I had the same issue on ec2 amazon AMI Linux php7.2 apache2.4 but token get generated in apache request headers but was not visible in Laravel request header
so add this code in middleware this will work only on your server but may not work on localhost.
$headers = apache_request_headers();
$request->headers->set('Authorization', $headers['authorization']);
JWT middleware
try {
$headers = apache_request_headers(); //get header
$request->headers->set('Authorization', $headers['authorization']);// set header in request
$user = JWTAuth::parseToken()->authenticate();
} catch (Exception $e) {
if ($e instanceof \Tymon\JWTAuth\Exceptions\TokenInvalidException){
return response()->json(['status' => 'Token is Invalid']);
}else if ($e instanceof \Tymon\JWTAuth\Exceptions\TokenExpiredException){
return response()->json(['status' => 'Token is Expired']);
}else{
return response()->json(['status' => 'Authorization Token not found']);
}
}
Fixed it by adding RewriteRule ^ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] to the .htaccess, so the authorization header does not get discarded by Laravel. Might be useful to add this to the docs.
I faced an issue and got solution from adding below lines in
.htaccess
RewriteRule ^ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
I add this in /etc/apache2/apache2.conf.
<Directory /var/www/html>
AllowOverride all
</Directory>
(remember to restart your apache)
Removing index.php from my url, resolve this problem.
/public/index.php/api/user/login -> /public/api/user/login

Laravel Token Signature could not be verified

I'm using Laravel/Lumen as an API for the backend of a webapp and run into a hiccup.
In an example I have a route that does not need the user to be authenticated. But I do want to check in the routes controller if the user visiting has a valid token.
So I wrote the following:
if ($tokenFetch = JWTAuth::parseToken()->authenticate()) {
$token = str_replace("Bearer ", "", $request->header('Authorization'));
} else {
$token = '';
}
I believe the above will check the Bearer token is valid else it will return a blank variable.
The following is my entire Controller.
public function show($url, Request $request)
{
if ($tokenFetch = JWTAuth::parseToken()->authenticate()) {
$token = str_replace("Bearer ", "", $request->header('Authorization'));
} else {
$token = 'book';
}
return response()->json(['token' => $token]);
}
The Problem
If I a pass in a valid Token Bearer, it returns the token but if I pass in an invalid one I get the following error:
TokenInvalidException in NamshiAdapter.php line 62:
Token Signature could not be verified.
If I don't pass a token at all:
JWTException in JWTAuth.php line 195:
The token could not be parsed from the request
Is there a way to check if a token is passed and if it has then check if its valid, but also if one has not been passed then return a blank return?
You can wrap it inside try/catch block
public function show($url, Request $request)
{
try {
$tokenFetch = JWTAuth::parseToken()->authenticate())
$token = str_replace("Bearer ", "", $request->header('Authorization'));
}catch(\Tymon\JWTAuth\Exceptions\JWTException $e){//general JWT exception
$token = 'book';
}
return response()->json(['token' => $token]);
}
There are few exceptions that you might want to handle separately (jwt-auth/Exceptions)
Also as you're using laravel 5 you can global handling for JWT exceptions ,not recommended in this case but you should know of this option and choose yourself. app/Exceptions/Handler.php and inside render method add [at the top]
if ($e instanceof \Tymon\JWTAuth\Exceptions\JWTException) {
//what happen when JWT exception occurs
}
Yes it's possible to achieve what you want.
Check if a token is passed:
If you check in the documentation of parseToken you'll see that the algorithm to check if we pass a token is:
if (! $token = $this->parseAuthHeader($header, $method)) {
if (! $token = $this->request->query($query, false)) {
}
}
// which it will be in your case:
$hasToken = true;
$header = $request->headers->get('authorization');
if (! starts_with(strtolower('authorization'), 'bearer')) {
if (! $request->query('token', false)) {
$hasToken = false;
}
}
Check if a token is valid:
Please note that the NamshiAdapter use the Namshi\JOSE package so read the documentation here.
In NamshiAdapter.php as you can see the line who rise your error are:
if (! $jws->verify($this->secret, $this->algo)) {
throw new TokenInvalidException('Token Signature could not be verified.');
}
// in your case:
// + try to var_dump $this->secret, $this->algo
// + use Namshi\JOSE\JWS
// if you var_dump
$jsw = new JWS(['typ' => 'JWT', 'alg' => $algo]);
$jws = $this->jws->load($token, false);
// if you want to follow the documentation of Namshi\JOSE
$jws = JWS::load($tokenString, false, $encoder, 'SecLib');
// again var_dump for $this->secret, $this->algo
$isValidToken = ($jws->verify($this->secret, $this->algo));

Laravel JWT tokens are Invalid after refresh them in a authentication JWT approach

EDIT:
Read the discussion about the bug at: https://github.com/tymondesigns/jwt-auth/issues/83
MY ORIGINAL QUESTION:
I'm implement with jwt-auth my protected resources that require an authenticated user with bellow code:
Route::group(['middleware' => ['before' => 'jwt.auth', 'after' => 'jwt.refresh']], function() {
// Protected routes
});
When user 'sign in' on API an Authorization token is created, and sent on response Authorization header to client application that call the resource. So, client applications when intercept a Authorization token on header of any response, set a variable/session/whatever with this token value, to send again to API on next request.
The first request for a protected resource after 'login' works fine, but the next client application request to API with a refreshed token, gives the following error (API mount all responses in json format):
{
"error": "token_invalid"
}
What can be happen with refreshed tokens? My refresh token implementation (set as a after middleware) is wrong? Or isn't necessary to manually refresh all Authorization token that come with client apps requests?
UPDATE:
I update the jwt-auth RefreshToken middleware as propose here, but the token_invalid persist.
BUG:
I guess that I found what happens. Note that in the refresh method, old token is added to blacklist cache case enabled:
// Tymon\JWTAuth\JWTManager
public function refresh(Token $token)
{
$payload = $this->decode($token);
if ($this->blacklistEnabled) {
// invalidate old token
$this->blacklist->add($payload);
}
// return the new token
return $this->encode(
$this->payloadFactory->setRefreshFlow()->make([
'sub' => $payload['sub'],
'iat' => $payload['iat']
])
);
}
And note that in add to blacklist method the key is the jti param from old token payload:
// Tymon\JWTAuth\Blacklist
public function add(Payload $payload)
{
$exp = Utils::timestamp($payload['exp']);
// there is no need to add the token to the blacklist
// if the token has already expired
if ($exp->isPast()) {
return false;
}
// add a minute to abate potential overlap
$minutes = $exp->diffInMinutes(Utils::now()->subMinute());
$this->storage->add($payload['jti'], [], $minutes);
return true;
}
Thus, when has on blacklist method is called, the old token jti param is the same that the new, so the new token is in blacklist:
// Tymon\JWTAuth\Blacklist
public function has(Payload $payload)
{
return $this->storage->has($payload['jti']);
}
If you don't need the blacklist functionality just set to false on jwt.php configuration file. But I can't say if it expose to some security vulnerability.
Read the discussion about the bug at: https://github.com/tymondesigns/jwt-auth/issues/83
When I get this issue, the solution that I found to get my project working was to generate a new token with data from older token on each new request.
My solution, that works for me, is bad, ugly, and can generate more issues if you have many async requests and your API(or business core) server is slow.
For now is working, but I will investigate more this issue, cause after 0.5.3 version the issue continues.
E.g:
Request 1 (GET /login):
Some guest data on token
Request 2 (POST /login response):
User data merged with guest data on old token generating a new token
Procedural code example(you can do better =) ), you can run this on routes.php out of routes, I say that is ugly haha:
// ----------------------------------------------------------------
// AUTH TOKEN WORK
// ----------------------------------------------------------------
$authToken = null;
$getAuthToken = function() use ($authToken, $Response) {
if($authToken === null) {
$authToken = JWTAuth::parseToken();
}
return $authToken;
};
$getLoggedUser = function() use ($getAuthToken) {
return $getAuthToken()->authenticate();
};
$getAuthPayload = function() use ($getAuthToken) {
try {
return $getAuthToken()->getPayload();
} catch (Exception $e) {
return [];
}
};
$mountAuthPayload = function($customPayload) use ($getLoggedUser, $getAuthPayload) {
$currentPayload = [];
try {
$currentAuthPayload = $getAuthPayload();
if(count($currentAuthPayload)) {
$currentPayload = $currentAuthPayload->toArray();
}
try {
if($user = $getLoggedUser()) {
$currentPayload['user'] = $user;
}
$currentPayload['isGuest'] = false;
} catch (Exception $e) {
// is guest
}
} catch(Exception $e) {
// Impossible to parse token
}
foreach ($customPayload as $key => $value) {
$currentPayload[$key] = $value;
}
return $currentPayload;
};
// ----------------------------------------------------------------
// AUTH TOKEN PAYLOAD
// ----------------------------------------------------------------
try {
$getLoggedUser();
$payload = ['isGuest' => false];
} catch (Exception $e) {
$payload = ['isGuest' => true];
}
try {
$payload = $mountAuthPayload($payload);
} catch (Exception $e) {
// Make nothing cause token is invalid, expired, etc., or not exists.
// Like a guest session. Create a token without user data.
}
Some route(simple example to save user mobile device):
Route::group(['middleware' => ['before' => 'jwt.auth', 'after' => 'jwt.refresh']], function () use ($getLoggedUser, $mountAuthPayload) {
Route::post('/session/device', function () use ($Response, $getLoggedUser, $mountAuthPayload) {
$Response = new \Illuminate\Http\Response();
$user = $getLoggedUser();
// code to save on database the user device from current "session"...
$payload = app('tymon.jwt.payload.factory')->make($mountAuthPayload(['device' => $user->device->last()->toArray()]));
$token = JWTAuth::encode($payload);
$Response->header('Authorization', 'Bearer ' . $token);
$responseContent = ['setted' => 'true'];
$Response->setContent($responseContent);
return $Response;
});
});

Categories