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"}
Related
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 implementing facebook data deletion callback but I got really lost and i can't continue on the JSON response that facebook is expecting.
Return a JSON response that contains a URL where the user can check the status of their deletion request and an alphanumeric confirmation code. The JSON response has the following form:
{ url: '<url>', confirmation_code: '<code>' }
that is the part that I got lost and stuck. My question is
what is the URL should do or show.
what is the logic between the confirmation code
so far here is what I did on my controller.
<?php
namespace App\Http\Controllers\User\Auth\Socialite;
use App\Models\User;
use Illuminate\Http\Request;
class FacebookSocialLoginController extends SocialLoginFactory
{
public function provider(): string
{
return 'facebook';
}
public function dataDeletionCallback(Request $request)
{
$signed_request = $request->get('signed_request');
$data = $this->parse_signed_request($signed_request);
$user_id = $data['user_id'];
// here will delete the user base on the user_id from facebook
User::where([
['provider' => 'facebook'],
['provider_id' => $user_id]
])->forceDelete();
// here will check if the user is deleted
$isDeleted = User::withTrashed()->where([
['provider' => 'facebook'],
['provider_id' => $user_id]
])->find();
if ($isDeleted ===null) {
return response()->json([
'url' => '', // <------ i dont know what to put on this or what should it do
'code' => '', // <------ i dont know what is the logic of this code
]);
}
return response()->json([
'message' => 'operation not successful'
], 500);
}
private function parse_signed_request($signed_request) {
list($encoded_sig, $payload) = explode('.', $signed_request, 2);
$secret = config('service.facebook.client_secret'); // Use your app secret here
// decode the data
$sig = $this->base64_url_decode($encoded_sig);
$data = json_decode($this->base64_url_decode($payload), true);
// confirm the signature
$expected_sig = hash_hmac('sha256', $payload, $secret, $raw = true);
if ($sig !== $expected_sig) {
error_log('Bad Signed JSON signature!');
return null;
}
return $data;
}
private function base64_url_decode($input) {
return base64_decode(strtr($input, '-_', '+/'));
}
}
what is the URL should do or show.
The purpose of this URL, is what the documentation said - to provide a way for the user, to check on the status of their deletion request.
Not all apps will be able to delete all personal user data immediately, the moment the user requests it.
Some might need to keep a subset of the data for legal reasons; others might simply need some extra processing time, because the process can not be handled in a totally automated matter, and a human needs to get involved.
So the user is given this status check URL in response to their request – so that they can go visit that URL tomorrow, or two weeks or six months from now, and check on the status of their deletion request - were you able to delete all data by now, will it still take some time, is there some data that won’t be deleted for legal reasons, etc.
what is the logic between the confirmation code
Just a different way to access the same information. Maybe checking the status via the URL you provided is not enough for the user, so they might want to call or send an email to your support staff, to inquire about the status of their deletion request. Then they can give your support people that code, and they can go look up the necessary information via that.
If you check the code examples in the documentation, they are using the same code value in the status check URL, and as the confirmation code. So you can use the same code for both.
Create it, store it in your database, and associate the status of a particular user’s deletion request with that code.
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.
I don't know if it's the right terms to employ...
I made an API, in which the answer is sent by the die() function, to avoid some more useless calculations and/or functions calls.
example :
if (isset($authorize->refusalReason)) {
die ($this->api_return(true, [
'resultCode' => $authorize->resultCode,
'reason' => $authorize->refusalReason
]
));
}
// api_return method:
protected function api_return($error, $params = []) {
$time = (new DateTime())->format('Y-m-d H:i:s');
$params = (array) $params;
$params = ['error' => $error, 'date_time' => $time] + $params;
return (Response::json($params)->sendHeaders()->getContent());
}
But my website is based on this API, so I made a function to create a Request and return the contents of it, based on its URI, method, params, and headers:
protected function get_route_contents($uri, $type, $params = [], $headers = []) {
$request = Request::create($uri, $type, $params);
if (Auth::user()->check()) {
$request->headers->set('S-token', Auth::user()->get()->Key);
}
foreach ($headers as $key => $header) {
$request->headers->set($key, $header);
}
// things to merge the Inputs into the new request.
$originalInput = Request::input();
Request::replace($request->input());
$response = Route::dispatch($request);
Request::replace($originalInput);
$response = json_decode($response->getContent());
// This header cancels the one there is in api_return. sendHeaders() makes Content-Type: application/json
header('Content-Type: text/html');
return $response;
}
But now when I'm trying to call an API function, The request in the API dies but dies also my current Request.
public function postCard($token) {
$auth = $this->get_route_contents("/api/v2/booking/payment/card/authorize/$token", 'POST', Input::all());
// the code below is not executed since the API request uses die()
if ($auth->error === false) {
return Redirect::route('appts')->with(['success' => trans('messages.booked_ok')]);
}
return Redirect::back()->with(['error' => $auth->reason]);
}
Do you know if I can handle it better than this ? Any suggestion of how I should turn my code into ?
I know I could just use returns, but I was always wondering if there were any other solutions. I mean, I want to be better, so I wouldn't ask this question if I knew for sure that the only way of doing what I want is using returns.
So it seems that you are calling an API endpoint through your code as if it is coming from the browser(client) and I am assuming that your Route:dispatch is not making any external request(like curl etc)
Now There can be various approaches to handle this:
If you function get_route_contents is going to handle all the requests, then you need to remove the die from your endpoints and simply make them return the data(instead of echoing). Your this "handler" will take care of response.
Make your Endpoint function to have an optional parameter(or some property set in the $request variable), which will tell the function that this is an internal request and data should be returned, when the request comes directly from a browser(client) you can do echo
Make an external call your code using curl etc(only do this if there is no other option)
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.