I have minor security in place to not allow users to download MP3s off my site. Ajax sends a request for a token which is a one time download, this token is attached to the URL I feed into soundmanager2. This security works fine except in Safari.
Front End Request
streamSong: function(song)
{
$.ajax({
url: '/streamsong/'+song.id,
type: 'get',
success: function(token) {
var stream = '/streamsong/'+song.id+'/'+token;
Player.sendSongToPlayer(song, stream);
}
});
}
Route
Route::get('/streamsong/{id}/{token?}', 'StreamController#setupStream');
Controller
class StreamController extends Controller {
public function setupStream($id, $token = null)
{
$stream = new Stream();
if ($token == null) {
if (Request::ajax()) {
$sessionToken = $stream->setToken(str_random(40));
return response($sessionToken);
} else {
return 'no way jose';
}
}
if ($token == $stream->getToken() ) {
return($stream->sendStream($id));
}
}
}
Stream class
public function setToken($token)
{
Session::flash('songToken', $token);
return($token);
}
public function getToken()
{
$token = Session::get('songToken');
return($token);
}
public function sendStream($id)
{
$post = Post::find($id);
$pathToFile = base_path().'/storage/app/mp3/'.$post->song_path;
$fileSize = filesize($pathToFile);
$name = $post->song_path;
$headers = array(
'Content-Type'=>'audio/mpeg',
'Pragma'=>'public',
'Content-Transfer-Encoding' => 'binary',
'Expires'=> 0,
'Cache-Control'=> 'must-revalidate, post-check=0, pre-check=0',
'Filename'=>$name,
'Content-Length'=>$fileSize,
'Connection'=> 'keep-alive'
);
return response()->download($pathToFile, $name, $headers);
}
The only conclusion I've come to is that Safari makes more than one request for the file download so the token is being destroyed on the first attempt. However I only see one GET request in the timeline console. If I set the Session::flash to Session::set it works fine in Safari but this bypasses the security measures. Even with Session::set I can't remove the session token variable until after the response to download has been sent out, which seems very strange.
Has anyone else experience behavior like this in Safari? I'm pretty stumped on this.
Can you check if the browser does an OPTIONS request before it does the actual request? Sometimes this request is performed to check the capabilities of a service.
Session might also not be your best option here, you could create a JWT that hold all the information you need in its payload to stream a song once. It's easy and solid.
Related
I am trying to integrate the Cashfree payment gateway in my Laravel 8 project. The only issue I face is in the callback URL, where an active session is automatically destroyed after getting the post data from Payment Gateway. I have also added the CSRF exception to Middleware. I have added 'secure' => env('SESSION_SECURE_COOKIE', false) & 'same_site' => null.
I have tried with a redirect()->away($payment_link), iFrame tag, and form submits directly to payment link but still getting the same issue.
The session is getting destroyed in the Mozilla browser, but it works fine in the chrome browser.
Controller (Generate Payment Request, URL, and Process Callback)
class PaymentController extends Controller
{
public function credits_add()
{
AuthCheck();
$this->data['page_name'] = 'Add Credits';
return view('merchant.payment.add_credits', $this->data);
}
public function credits_save(Request $request)
{
$request->validate([
'credit_amount' => 'required',
'credit_transaction_type' => 'required'
]);
if (!empty($request->input('credit_transaction_type')) && $request->input('credit_transaction_type') == 'Cashfree') {
$cashfreeDetails = $this->pay_with_cashfree($request);
if (!empty($cashfreeDetails) && !empty($cashfreeDetails['paymentLink'])) {
return Redirect::to($cashfreeDetails['paymentLink']);
} else {
return redirect('credits/add')->with('errorMessage', 'Sorry! Your transaction has failed.');
}
}
return redirect('credits/add');
}
public function pay_with_cashfree($request)
{
$order = new Order();
$od["orderId"] = "ORDER-84984941";
$od["orderAmount"] = 10000;
$od["orderNote"] = "Subscription";
$od["customerPhone"] = "9000012345";
$od["customerName"] = "Test Name";
$od["customerEmail"] = "test#cashfree.com";
$od["returnUrl"] = route('CreditsSuccess');
$od["notifyUrl"] = route('CreditsSuccess');
$order->create($od);
$linkArray = $order->getLink($od['orderId']);
$detailsArray = $order->getDetails($od['orderId']);
if (!empty($order) && !empty($linkArray) && !empty($linkArray->status) && $linkArray->status == 'OK') {
return array(
'paymentLink' => $linkArray->paymentLink,
'paymentDetails' => $detailsArray
);
} else {
return array();
}
}
public function credits_success(Request $request)
{
$orderId = $request->orderId;
$orderAmount = $request->orderAmount;
$referenceId = $request->referenceId;
$txStatus = $request->txStatus;
$paymentMode = $request->paymentMode;
$txMsg = $request->txMsg;
$txTime = $request->txTime;
$signature = $request->signature;
if ($txStatus == 'SUCCESS') {
return redirect('credits/add')->with('successMessage', $txMsg);
} else {
return redirect('credits/add')->with('errorMessage', $txMsg);
}
}
}
OK I figured out the problem for myself.
The new versions of the browsers might be logging you out because of the new cookie policy.
References https://developers.google.com/search/blog/2020/01/get-ready-for-new-samesitenone-secure
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
Whenever the cookie is required to be sent to server, the browser sees the SameSite attribute to decide if the cookie to be sent to server or blocked. For user actions, it is sent to the server but for auto-redirects, it doesn't if SameSite is set to 'Strict' or 'Lax' (Lax is going to be the default value now).
Solution: The cookie attribute SameSite can be set to 'None' along with specifying the 'Secure' attribute to 'true'. Setting 'Secure' attribute to 'true' would require your site to run on https. Sites running with http:// protocol will not be able to set 'Secure' cookie. Please set the 'HttpOnly' attribute to 'true' for making it accessible for http requests to the server only.
You can consider the following configuration for cookie:
SESSION_SECURE_COOKIE=true
SAME_SITE=none
You can also refer below tutorial that explains the cashfree integration in laravel:
https://www.w3techpoint.com/laravel/laravel-9-cashfree-payment-gateway-integration
I have started working on an old application developed in PHP with codeigniter. The previous dev isn't in the company anymore, I'm all alone to figure out what's happening.
First of all, the code seems to be working as intended on the first "development" server, but raises an error on the "test" server. The code is the same, the servers should be configured the same (I still don't have all the clearances I need to check this). Anyway, here is the logic :
There is a login page which asks the usual credentiels. They are sent to the back controler via POST, he then sends a request to the authentication API which returns a token if everything is correct.
The token is then written on the server, recovered in the front page and the user is redirected to the home page. This last redirection is completed with a "autorization: Bearer {token}" in the header.
Here are the screens, obtained via Postman :
First call from the front to Authentication::login :
[![first call from front to authenticaiton::login][1]][1]
The token is then correctly recovered, and apparently stored server-side :
Authentication controler
public function login(){
[...]
$make_call = $this->callAPI('POST', $this->config->items('apiurl') . 'token/', $data_array, ^this->config->items('apibearer'), $this->config->item('proxy'));
$response = json_èdecode($make_call, true);
if(isset($response['error'])){
exit(json_encode(array('error'=>true, 'error_id'=>$response['error'])));
}
//parse token in JWT
$token = $this->auc9_config->getParser()->parse((string) $response['id_token']);
$token = $thi->writeToken($userId, $token);
exit(json_encode(array('error' => false, 'token' => $tken->__toString())));
}
}
public function writeToken($userId, $token){
$role = $token->claims()->get('functional_post');
$eds = $role['structure_element_id'];
if(!is_null($eds)){
$builder = $this->company_config->createBuilder();
$new_token = $builder->issuedAt(new DateTimeImmutable())
->expriresAt((new DateTimeImmutable())->add(new DateInterval('PT' . (3600 * 4) . 'S')))
->withClaim('id_cr', $token->claims()->get('structure_id'))
->withClaim('eds', $eds)
->withClaim('uuid_tablet','web_demo')
->withClaim('version_apk', 'web_demo')
->getToken($this->company_config(getSigner(), $this->company_config->getSigningKey());
return $new_token;
} else {
die(json_encode(array('error'=>true, 'error_id'=>'invalid_user')));
}
}
JS side, the url is rewriten and the token is sent in the header :
index.js
if(data.token){
const url = window.location.pathname.replace(/authentication\/?/, '');
$.ajax({
type : 'GET',
url,
headers: {
'autorization': 'Bearer ' + data.token
},
success : function(data){
window.location.reload();
},
error : function(err) {
$(form).find('.erros').append('<p>invalid token</p>')
}
});
}
Finaly, the home page is called via the last ajax call, the the page is loaded if the token workds properly :
Home controler
public function index(){
$home_page = $this->home_page->fetch_activated();
if(home_page === null){
show_404();
}else{
//display content
[...]
}
}
Home Model
class HomePage extends MY_Model {
[...]
public function fetch_activated() {
$result = $this->read(array('active IS NOT NULL' => null, 'active >' => 0));
return count($result) === 1 ? $result[0] : null;
}
}
**Error : **
In the development server, the user is correctly redirected to the home page
In the test server, the user is rooted to the 404 page
Without ssh access to the server, I can't put up some exit() command via VIM and watch the results, nor can I access any log file.
Do you have any idea whats I can do ?
Edit1 : transform images to code
Edit2 : added Home Model
As said by #nitrin0, there was a problem in the database. Once I got the credentials to access is, troubleshouting the problem was quite easy.
I am integrating Laravel into a legacy php app. The login page used to directly post to verifyUser.php which also started a Symfony Session.
The new architecture now posts to a laravel api which makes a Guzzle post to verifyUser.php.
javascript:
$(document).ready(function(){
$('#signIn').submit(function(){
var a = $('#email').val();
$.post('/api/login', { //this used to post to verifyUser.php
Username: $('#email').val(),
Password: $('#password').val()
}, function(data){
if(data['credentials'] == true){
console.log('credentials true');
console.log(data['uri']);
window.location.href=data['uri'];
} else {
$('#errMsg').html(data['errMsg']);
$('.alert').show();
}
}, 'json');
return false;
});
controller functions:
public function authenticate(Request $request) //aka api/login endpoint
{
//...
$legacyRes = $this->authenticateLegacy($request);
//...
}
private function authenticateLegacy(Request $request)
{
$response = null;
try {
$response = $this->client->post('/admin/verifyUser.php', [
'form_params' => ['Username' => $request->get('Username'),
'Password' => $request->get('Password')]
]);
}
catch(Exception $exception){
Log::error('Errrererererer', [$exception->getMessage()]);
}
$body = (string)$response->getBody();
Log::info('BODY:', [$body]);
return $body;
}
I have left out verifyUser.php because I have tested it and it returns the expected results.
When using the browser, the session information doesn't seem to get set. But according to my post responses, everything should be working.
Is this because I am routing the request through guzzle?
Posting under my answer to show updated code:
private function authenticateLegacy(Request $request)
{
//...
//parse cookie id from guzzle response
$body = (string)$response->getBody();
$cookie = $response->getHeader('Set-Cookie'); //PHPSESSID=SOMEID; path=/
$cookieBite = explode(';', $cookie)[0]; ////PHPSESSID=SOMEID
$cookieId = explode('=', $cookieBite)[1];
$data = json_decode($body, true);
$data['session'] = $cookieId;
return $data;
}
In the action:
public function authenticate(Request $request)
{
//...
$legacyRes = $this->authenticateLegacy($request);
//...
// this will have the session id in the body but will also
// set the cookie for the client so I don't have
// to set document.cookie w/ js
return response($legacyRes, 200)
->withCookie('PHPSESSID', $legacyRes['session']);
}
I assume your legacy endpoint uses cookies to identify a user's session.
A successfull request to the legacy endpoint returns a Set-Cookie header.
Guzzle doesn't forward this Set-Cookie header from the API response to the browser - you'll have to program this behaviour into the "wrapping" application.
You will need to tell guzzle to explicitly pass the corresponding Cookie header to the legacy api (to maintain the user's login state) when sending any further requests.
In order to achieve this you'll need to save this cookie within your new application (i.e. in the user's session or in database) and then pass it within a Cookie header along with all further requests you make to the legacy API.
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;
});
});
I'm currently using a Zend Controller Plugin to check authentication. The following probably looks familiar:
class SF_Plugin_Member_Auth extends Zend_Controller_Plugin_Abstract {
public function preDispatch(Zend_Controller_Request_Abstract $request) {
if (!SF_Auth::getInstance('Member')->hasIdentity()) {
if ($request->getControllerName() !== 'auth' && $request->getControllerName() !== 'error') {
$r = Zend_Controller_Action_HelperBroker::getStaticHelper('redirector');
$r->gotoSimpleAndExit('login', 'auth', $request->getModuleName());
}
}
}
}
What I'm unsure of is the best way of dealing with an AJAX request that isn't authenticated. So say someone tries to login using a form that's sent over AJAX, how should the Javascript know that it actually needs to redirect the user to the login page?
My first thought is to check to see if the request is an AJAX request, and then echo out a JSON object with details of where to redirect the user to - the Javascript can then look for a particular property in the returned JSON object and use that as the URL to "location.href" the user to.
There are two problems with the above:
I'm not sure how to stop the request from being dispatched - all I want to do is echo out a simple JSON string if it's an AJAX request.
It doesn't feel like a Zend-like way of doing things.
Is there anyone out there who's hit upon and solved this very scenario?
Thanks very much,
James.
You can set your json values in the response object and gracefully stop the request with the redirector.
if (!SF_Auth::getInstance('Member')->hasIdentity()) {
if ($request->getControllerName() !== 'auth' && $request->getControllerName() !== 'error') {
if ($request->isXmlHttpRequest()) {
$json = Zend_Json::encode(array('auth' => false, 'url' => 'http://foo.bar/login'));
// Prepare response
$this->getResponse()
->setHttpResponseCode(200) // Or maybe HTTP Status 401 Unauthorized
->setBody($json)
->sendResponse();
// redirectAndExit() cleans up, sends the headers and stopts the script
Zend_Controller_Action_HelperBroker::getStaticHelper('redirector')->redirectAndExit();
} else {
$r = Zend_Controller_Action_HelperBroker::getStaticHelper('redirector');
$r->gotoSimpleAndExit('login', 'auth', $request->getModuleName());
}
}
}
This will output something like this:
{"auth":false,"url":"http:\/\/foo.bar\/login"}