Ajax Auth redirect on Laravel 5.6 - php

I have a Like button that fires an Ajax Post, this route is protected by auth:api middleware:
myproject/routes/api.php
Route::group(['middleware' => ['auth:api']], function () {
Route::post('/v1/like', 'APIController#set_like');
});
When an authenticated user clicks the like button, no problem at all, everything works smoothly. But when guests click the button, I redirected them to login page via Javascript and after authentication they are redirected to the page specified in RedirectIfAuthenticated middleware, so usually to /home.
I modified that middleware as follows:
myproject/app/Http/Middleware/RedirectIfAuthenticated.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #param string|null $guard
* #return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->check()) {
return redirect()->intended('/home');
}
return $next($request);
}
}
My Ajax call is this:
var toggleLike = function(){
var token = USER_TOKEN; //javascript variable
var current_type = $(thisLikeable).attr("data-type");
var current_id = $(thisLikeable).attr("data-id");
var jqxhr = $.post( APP_URL + "/api/v1/like", {api_token: token, type: current_type, id: current_id}, function(data) {
setLikeAppearance(data.message);
})
.fail(function(xhr, status, error){
if (xhr.status == 401) {
window.location = APP_URL + "/login" ;
}
});
};
The problem here's the intended() function, which for Ajax calls is not storing the correct session variable and I am not figuring out how to set it properly.
I am clearly missing something obvious, can anyone help?
Cheers!
EDIT
What I want to achieve is this:
GUEST is in //mysite/blabla
clicks Like button
gets redirected to login
logs in (or register)
gets redirected to //mysite/blabla with the Like already triggered on

What's happening is that in APIs sessions are not managed or in other words it's stateless. So the session middleware is not implemented on Laravel framework for API requests. Though you can manually add, it's not idle to use. So if the API does not use sessions and uses the redirect, fronted does not know about it, as API and frontend work as two separate apps. SO you need to send the frontend the status of the response and let the frontend handle the redirect as you have done with ajax. Just remove the redirect if unauthenticated and let the API throw unauthorized exception. Then, from the handler, handle the unauthorized exception.
Here is how to do it.
Add this to app/Exceptions/Handler.php
/**
* Convert an authentication exception into an unauthenticated response.
*
* #param \Illuminate\Http\Request $request
* #param \Illuminate\Auth\AuthenticationException $exception
* #return \Illuminate\Http\Response
*/
protected function unauthenticated($request, AuthenticationException $exception)
{
if ($request->expectsJson()) {
return response()->json(['error' => 'Unauthenticated.'], 401);
}
return redirect()->guest('login');
}
this will send the user a 401 with message Unauthenticated if the request was json(api request), else(if web request) will redirect to login
check the render method below or check it from source to understand what's happening. when an unauthorized exception is thrown we are telling to check the request type and if it's from an API request, we are sending a json response with 401 status code. So know from frontend we could redirect the user to login page after seeing the 401 status code.
From source
/**
* Render an exception into a response.
*
* #param \Illuminate\Http\Request $request
* #param \Exception $e
* #return \Symfony\Component\HttpFoundation\Response
*/
public function render($request, Exception $e)
{
if (method_exists($e, 'render') && $response = $e->render($request)) {
return Router::toResponse($request, $response);
} elseif ($e instanceof Responsable) {
return $e->toResponse($request);
}
$e = $this->prepareException($e);
if ($e instanceof HttpResponseException) {
return $e->getResponse();
} elseif ($e instanceof AuthenticationException) {
return $this->unauthenticated($request, $e);
} elseif ($e instanceof ValidationException) {
return $this->convertValidationExceptionToResponse($e, $request);
}
return $request->expectsJson()
? $this->prepareJsonResponse($request, $e)
: $this->prepareResponse($request, $e);
}
AFTER EDIT
The intended method() is only for web routes as it uses session to extract the intended route or manually passed value. Here is the intended() method.
/**
* Create a new redirect response to the previously intended location.
*
* #param string $default
* #param int $status
* #param array $headers
* #param bool $secure
* #return \Illuminate\Http\RedirectResponse
*/
public function intended($default = '/', $status = 302, $headers = [], $secure = null)
{
$path = $this->session->pull('url.intended', $default);
return $this->to($path, $status, $headers, $secure);
}
To achive the redirect to the page the user is comming from you can
1 - Manually pass some queries with url like
/login?redirect=like(not the url, just a mapping for /comments/like url)&value=true(true is liked, false is unliked)
and handle it manually.
2 - Get and check the query parameters from url
3 - Use to() method to pass the intended url instead of using intended(). here is the to() method. (see 4 to see the recommended way)
/**
* Create a new redirect response to the given path.
*
* #param string $path
* #param int $status
* #param array $headers
* #param bool $secure
* #return \Illuminate\Http\RedirectResponse
*/
public function to($path, $status = 302, $headers = [], $secure = null)
{
return $this->createRedirect($this->generator->to($path, [], $secure), $status, $headers);
}
4 - But, I would recommend sending redirect url (I mean the mapping ex: like) as a response to frontend and let the frontend handle the redirecting. As API redirecting will work if the API is used by websites only. Suppose if you are using this same api for a mobile app, wonder how API redirect will work. It's not a work of API to redirect, unless if it's for things like OAuth Authentication, which would have a redirect url specified.
Remember to sanitize the url params to block XSS like stuff. Better
Send some values and map it to the urls. Like
[
//like is mapping
//comments/like is the actual url
'like' => 'comments/like'
]
Then, get the mapping url from array or use frontend mappings.

You can make changes in your RedirectIfAuthenticated.php to distinguish between Ajax call & normal login like this:
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #param string|null $guard
* #return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->check()) {
if ($request->has('api_token')) {
// do whatever you want
return response()->json('some response');
}
return redirect()->intended('/home');
}
return $next($request);
}
}
Update:
Another solution is to remove Auth middleware from your route. In APIController#set_like function manually login user, trigger like & return json response.

If a button is not for a guest, then you shouldn't render it on page.
Instead, you should render a link to login, then if the user logs in you will redirect him back to where he was before. Now, user can see and click the button.

Related

Rate Limit for only success requests (Laravel 9)

Is there anyway to apply rate limiting to the route but for only success responses. Like for example if user sends request to send/code endpoint 5 times and if all of them was successful then block the user to send request again. But if 2 of them was unsuccessful (like validation error or something) but 3 was successful then user should have 2 more attempts for the given time.
I know rate limiting checks before request get executed, then block or let the user to continue. But is there anyway to apply my logic or should I try to approach differently?
You would probably need to make your own middleware, but you can extend the ThrottleRequests class and just customize how you want to handle responses:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Support\Arr;
class ThrottleSuccess extends ThrottleRequests
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #param array $limits
* #return \Symfony\Component\HttpFoundation\Response
*
* #throws \Illuminate\Http\Exceptions\ThrottleRequestsException
*/
protected function handleRequest($request, Closure $next, array $limits)
{
$response = $next($request); // call the controller first
if ($response->statusCode === 200) { // only hit limiter on successful response
foreach ($limits as $limit) {
if ($this->limiter->tooManyAttempts($limit->key, $limit->maxAttempts)) {
throw $this->buildException($request, $limit->key, $limit->maxAttempts, $limit->responseCallback);
}
$this->limiter->hit($limit->key, $limit->decayMinutes * 60);
}
}
foreach ($limits as $limit) {
$response = $this->addHeaders(
$response,
$limit->maxAttempts,
$this->calculateRemainingAttempts($limit->key, $limit->maxAttempts)
);
}
return $response;
}
}
Then add your middleware to Kernel.php:
protected $routeMiddleware = [
// ...
'throttle.success' => ThrottleSuccess::class,
// ...
];
Then use it in a route like the original throttle middleware:
Route::middleware('throttle.success:5,1')->group(function () {
// ...
});
Note: you may have to override handleRequestUsingNamedLimiter if you want to return a custom response built from RateLimiter::for, I have not done anything for that here.

Cross site scripting issue by adding new parameter in the request

Below is my url with request
http://test.com?leavingfrom=BOM&goingto=DEL&travel=DOM&_token=gclligMoBzOHW4wwruDShklxbOh3SjsKTWRvWFK0&Default=O&leavingfrom1=Mumbai+%28BOM%29&goingto1=New+Delhi+%28DEL%29&depart=16-02-2017&arrive=&class=E&adults=1&child=0&infants=0
Cross site scripting is happening for this URL as below. New parameter is added in the URL as '-alert-'=1 and passed to nextpage
http://test.com?leavingfrom=BOM&goingto=DEL&travel=DOM&_token=gclligMoBzOHW4wwruDShklxbOh3SjsKTWRvWFK0&Default=O&leavingfrom1=Mumbai+%28BOM%29&goingto1=New+Delhi+%28DEL%29&depart=16-02-2017&arrive=&class=E&adults=1&child=0&infants=0&'-alert-'=1
How can i stop the cross site scripting if new parameter is added
You can use this class as middleware
class XSSProtection
{
/**
* The following method loops through all request input and strips out all tags from
* the request. This to ensure that users are unable to set ANY HTML within the form
* submissions, but also cleans up input.
*
* #param Request $request
* #param callable $next
* #return mixed
*/
public function handle(Request $request, \Closure $next)
{
if (!in_array(strtolower($request->method()), ['put', 'post'])) {
return $next($request);
}
$input = $request->all();
array_walk_recursive($input, function(&$input) {
$input = strip_tags($input);
});
$request->merge($input);
return $next($request);
}
}
REF : https://gist.github.com/kirkbushell/5d40fdd7f7b364716742

How to get variable from middleware?

I've created my own middleware for API. Following is my code for getting valid User details based on the request params access_token
namespace App\Http\Middleware;
use Closure;
use App\Exceptions\GeneralException;
use App\Models\Access\User\User;
class authNS
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
try {
$user = User::where("access_token",$request->header('access-token'))->with('details')->first();
} catch (Exception $e) {
return response()->json(['error'=>'Something is wrong']);
}
return $next($request);
}
}
But how can I access this $user variable within my Controller?
You can use onceUsingId() to log a user into the application for a single request. No sessions or cookies will be utilized, which means this method may be helpful when building a stateless API:
So in your middleware you can use it as:
$user = User::where("access_token",$request->header('access-token'))->first();
if($user) {
auth()->onceUsingId($user->id);
}
Then in your controller, you can use it as:
auth()->user()
Docs

Laravel looses Session ( Authentication)

I have a problem with a specific route on Laravel. Every second time (and sometimes on the first time) when ill call a specific route, ill get an 401 error, returned from the Authentication Middleware.
File Middleware/Authenticate.php
class Authenticate
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #param string|null $guard
* #return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->guest()) {
if ($request->ajax() || $request->wantsJson()) {
return response('Unauthorized.', 401); // THIS IS CALLED
} else {
return redirect()->guest('login');
}
}
return $next($request);
}
From that route:
Route::group(['middleware' => 'auth'], function () {
.........
Route::get('events', 'TaskController#events');
});
TaskController.php
public function events(Request $request) {
$time_from = $request->start;
$time_to = $request->end;
$events = array();
$user_id = Auth::user()->id;
.....
return response()->json($events, 200);
}
All called from a JQuery $.get Request. I dont know why Laravel thinks i am a guest, and then looses the Session?
When you are doing ajax/api requests laravel thinks you're guest because session based authentications doesn't apply to this type of calls. Whenever you use auth middleware you'll get 401 on ajax, even if you're authenticated.
You need some type of token based authentication for the ajax/api calls, that sends Authentications header on requests and new middleware that handles authentications for it.

Redirect to URL after login with Socialite in Laravel

I need to register in a tournament with the URL:
http://laravel.dev/tournaments/1/register/
This URL is in the middleware 'auth', so if the user is not logged, he is redirected to login page.
What I need is to redirect to
http://laravel.dev/tournaments/1/register/
After social login ( I have 2 providers, fb and google)
In this case, I don't want the user to be redirected to the redirected url defined in .env
This is the function I use to login with Socialite:
public function execute($request, $listener, $provider) {
if (!$request) {
return $this->getAuthorizationFirst($provider);
}
$user = $this->users->findByUserNameOrCreate($this->getSocialUser($provider), $provider);
if (!is_null($user)){
$this->auth->login($user, true);
}else{
Session::flash('error', Lang::get('auth.account_already_exists'));
return redirect('auth/login');
}
return $listener->userHasLoggedIn($user);
}
How can I do to make the system redirect me to the initial action and not the default redirect_url param.
I have it resolved with normal login with the guest() function, but I don't know how to implement it in this case.
Problem is quite similar to this post
The key functions are guest() and intended() :
guest() creates a redirect response to a URL of your choice,
ideally your login page, whilst storing the current url where you
can redirect your user to, when the latter successfully completes
the authentication process.
intended() fetches the intended redirect URL after the user
completes the authentication.
Demo
Authenticate Middleware - used to verify if user who is accessing the url is logged in.
class Authenticate
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
//check if user is not logged in.
if (Sentinel::check() === false) {
//If ajax, send back ajax response
if ($request->ajax()) {
return response('Unauthorized.', 401);
} else {
//redirect guest to login page
return redirect()->guest('/login');
}
}
return $next($request);
}
}
RESTful UserController, where we handle all login attempts
/**
* POST: Login
*
* #param Request $request
* #return $this|\Illuminate\Http\RedirectResponse|\Illuminate\Http\Response
*/
public function postLogin(Request $request)
{
//Authenticate User if possible
if ($this->service->user->loginByEmail($request->only(['email','password','remember_me']))) {
//Authentication Successful - Redirect to url that guest was trying to access, else redirect to home page.
return redirect()->intended('/');
} else {
//Authentication error, redirect back to login page with input
return redirect('/login')->withErrors(['password_incorrect' => 'Your password is incorrect. Please try again.'])->withInput();
}
}
Declarations of functions already in framework
Illuminate\Routing\Redirector
/**
* Create a new redirect response, while putting the current URL in the session.
*
* #param string $path
* #param int $status
* #param array $headers
* #param bool $secure
* #return \Illuminate\Http\RedirectResponse
*/
public function guest($path, $status = 302, $headers = [], $secure = null)
{
//Store current full URL in session
$this->session->put('url.intended', $this->generator->full());
//Redirect
return $this->to($path, $status, $headers, $secure);
}
/**
* Create a new redirect response to the previously intended location.
*
* #param string $default
* #param int $status
* #param array $headers
* #param bool $secure
* #return \Illuminate\Http\RedirectResponse
*/
public function intended($default = '/', $status = 302, $headers = [], $secure = null)
{
//Get path that guest was trying to access
$path = $this->session->pull('url.intended', $default);
//Redirect
return $this->to($path, $status, $headers, $secure);
}
Use it like this: return redirect()->guest('auth/login');.

Categories