Tell whether route is behind firewall in Symfony2 - php

I'm currently writing an event listener in Symfony2, which listens for the kernel.response event, and adds a cookie to it if: a) a user is logged in, and b) no such cookie currently exists. It takes the service container as an argument.
However, I'm getting an error when the listener responds to events not behind a firewall (such as those in the dev toolbar) since the token is empty and an AuthenticationCredentialsNotFoundException is thrown. However, I can't for the life of me figure out how to tell whether the route is behind a firewall or not. Could anyone help?
Code
public function onKernelResponse(FilterResponseEvent $event) {
// does the request have a device cookie?
if ($this->container->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')
&& !$this->getRequest()->cookies->has(DeviceManager::COOKIE_PREFIX.'id')) {
// no. Create one.
$DeviceManager = $this->container->get('salus_user.device_manager');
$Cookie = $DeviceManager->createDeviceCookie();
$Response = $event->getResponse();
$Response->headers->setCookie($Cookie); // and save it
}
// else, yes, we don't need to do anything
}
Error
AuthenticationCredentialsNotFoundException in classes.php line 2888:
The token storage contains no authentication token. One possible reason may be that there is no firewall configured for this URL.

First check if token exist:
public function onKernelResponse(FilterResponseEvent $event) {
if (!$this->container->get('security.token_storage')->getToken()) {
return;
}
// Rest of code.
}

Related

Laravel Oauth2 controller using League OAuth2 client

I'm trying to use the League OAuth2 Client to allow users to authenticate my Laravel web app to set appointments on their calendar. NOTE: I'm not trying to let users login to my site or authenticate into my site using OAuth! I just want to be able to let users add appointments to their own calendars.
I'm basically following the flow outlined here: https://github.com/thephpleague/oauth2-google and have created a single controller (called OauthController with a single method, redirectGoogle. My redirect route (which is registered with Google) is https://example.com/oauth2/google. When I hit this endpoint in my Laravel app, I get redirected to Google to approve my app to access my account data as expected, and then redirected back to the controller endpoint.
However it fails every time at the exit('Invalid state'); line.
Here's the controller method code:
public function redirectGoogle(Request $request)
{
$provider = new Google([
'clientId' => config('oauth.google_oauth_id'),
'clientSecret' => config('oauth.google_oauth_secret'),
'redirectUri' => 'https://example.com/oauth2/google',
]);
if (!empty($request->input('error'))) {
// Got an error, probably user denied access
dd($request->input('error'));
} elseif (empty($request->input('code'))) {
// If we don't have an authorization code then get one
$authUrl = $provider->getAuthorizationUrl();
session(['oauth2state', $provider->getState()]);
Log::info('Storing provider state ' . session('oauth2state')); <-- Log entry exists so we know session value was written
header('Location: ' . $authUrl);
exit;
} elseif (empty($request->input('state')) || ($request->input('state') !== session('oauth2state', false))) {
Log::error($request->input('state') . ' did not equal stored value ' . session('oauth2state', false)); <-- Log entry exists
// State is invalid, possible CSRF attack in progress
exit('Invalid state'); <-- Breaks here
} else {
// Try to get an access token (using the authorization code grant)
$token = $provider->getAccessToken('authorization_code', [
'code' => $request->input('code')
]);
// Optional: Now you have a token you can look up a users profile data
try {
// We got an access token, let's now get the owner details
$ownerDetails = $provider->getResourceOwner($token);
// Use these details to create a new profile
dd('Hello %s!', $ownerDetails->getFirstName());
} catch (Exception $e) {
// Failed to get user details
dd('Something went wrong: ' . $e->getMessage());
}
// Use this to interact with an API on the users behalf
echo $token->getToken() . PHP_EOL;
// Use this to get a new access token if the old one expires
echo $token->getRefreshToken() . PHP_EOL;
// Unix timestamp at which the access token expires
echo $token->getExpires() . PHP_EOL;
dd();
}
}
The strange thing is that the log messages noted in the code above both exist, and the values match (at least, it is attempting to write the first session variable with a value that would match the second log file's value):
[2020-05-04 21:02:48] local.INFO: Storing provider state 4963a33bbd5bcf52d3e21c787f24bd7b
[2020-05-04 21:02:51] local.ERROR: 4963a33bbd5bcf52d3e21c787f24bd7b did not equal stored value <null>
Why is it that the second time through the code the oauth2state session value is null, when it was successfully written on the first loop?
NOTE: the problem appears to be that the sessions are different, which makes sense, but how can this session stay consistent, or otherwise keep the data straight?
[2020-05-05 15:25:06] local.INFO: Session id: bV7F5mNM69rJAVJNWK9ZD0rcoN284FxXvjNAmUiw
[2020-05-05 15:25:06] local.INFO: Storing provider state 7351b313b741df41a6be9a049f71db6b
[2020-05-05 15:25:10] local.INFO: Session id: VNiBxr1gYYIA9Nr11x9c4JJArHOiKQScEGh2jkuc
[2020-05-05 15:25:10] local.ERROR: 7351b313b741df41a6be9a049f71db6b did not equal stored value <null>
EDIT2: I've tried the tutorial here which uses a slightly different approach using Laravel and the League Oauth library-- it has the exact same problem, the session ID is different between the two requests, meaning there's no way you'll ever get a match between the state keys.
I believe the problem lies with how you redirect to google.
Problem:
Laravel needs to run trough the whole request in order to persist values into the session.
By using exit; you are interrupting the request and therefore Laravel will not get the chance to persist the values into the session.
Solution:
By using the redirect() helper as suggested in the docs, Laravel will be able to complete the request.
elseif(empty($request->input('code'))) {
// If we don't have an authorization code then get one
$authUrl = $provider->getAuthorizationUrl();
session(['oauth2state', $provider->getState()]);
Log::info('Storing provider state ' . session('oauth2state'));
return redirect($authUrl);
}
Explanation:
In Laravel you can decide when a middleware is run, from the docs:
Before & After Middleware
Whether a middleware runs before or after a request depends on the
middleware itself. For example, the following middleware would perform
some task before the request is handled by the application:
public function handle($request, Closure $next)
{
// Perform action
return $next($request);
}
However, this middleware would perform its task after the request is
handled by the application:
public function handle($request, Closure $next)
{
$response = $next($request);
// Perform action
return $response;
}
Now if we take a look at how Laravel persists the session data in the StartSession middleware, you can see here that Laravel tries to persist the data into the session after the request has been handled by the application, so by using exit;, die(); or dd(); your are stopping the script and Laravel never gets the opportunity to persist the values in the session.
protected function handleStatefulRequest(Request $request, $session, Closure $next)
{
// Before middleware
$request->setLaravelSession(
$this->startSession($request, $session)
);
$this->collectGarbage($session);
$response = $next($request);
// After middleware
$this->storeCurrentUrl($request, $session);
$this->addCookieToResponse($response, $session);
$this->saveSession($request);
return $response;
}

CakePHP 3: Session was already started with Hybridauth 3

I have a LoginController where I do my usual login operation with combination of an email address and a password associated with the account.
I have separated my Hybridauth related code into a separate controller named OauthController where I have all my Hybridauth magic and where my callback / endpoint resides.
In the OauthController I check if user's email from the specified provider is already registered, and in either case I try to login that user with $this->Auth->setUser(object).
Whenever, or whatever from the $this->Auth is called, I get a response stating:
Session was already started
I have browser through CakePHP 3 code and found the following statement in:
vendor/cakephp/cakephp/src/Network/Session.php (335)
public function start()
{
if ($this->_started) {
return true;
}
if ($this->_isCLI) {
$_SESSION = [];
$this->id('cli');
return $this->_started = true;
}
if (session_status() === \PHP_SESSION_ACTIVE) {
throw new RuntimeException('Session was already started');
}
...
And that's the point in code where that message is thrown at me.
Now, as I browsed through the Hybridauth code itself, I have found following in:
vendor/hybridauth/hybridauth/src/Storage/Session.php (46)
public function __construct()
{
if (session_id()) {
return;
}
if (headers_sent()) {
throw new RuntimeException('HTTP headers already sent to browser and Hybridauth won\'t be able to start/resume PHP session. To resolve this, session_start() must be called before outputing any data.');
}
if (! session_start()) {
throw new RuntimeException('PHP session failed to start.');
}
}
And both of them call session_start, one before the other, although CakePHP's part is blocking me.
I have tried removing !session_start() check from Hybridauth, but then Hybridauth doesn't know where to read out it's thingies it needs to read.
So, as a demonstrator, I am trying to achieve this in OauthController:
<?php
namespace App\Controller;
use Hybridauth\Hybridauth;
class OauthController extends AppController
{
public function callback($provider)
{
try {
$hybridauth = new Hybridauth($config);
// additional mystery code
$hybridauth->authenticate();
if($everything_okay) {
$this->Auth->setUser($userObject); // and this is the point of failure
return $this->redirect('/account'); // and this never happends... :(
}
}
}
}
Any help, ideas, insights on how to deal with this are all welcome!
Simply start the CakePHP session manually before using the Hybridauth library, so that it bails out at the session_id() check and picks up the existing session.
For example in your controller:
$this->getRequest()->getSession()->start();
// in CakePHP versions before 3.6/3.5
// $this->request->session()->start();

Symfony 4 : ignore kernel events coming from debug toolbar

I'm quite new to Symfony so forgive me if it seems obvious for you :)
For my project, i need to perform some actions depending on the url. I use kernel events, more specifically the kernel request to do so.
In services.yaml :
App\Service\UrlManager:
tags:
- { name: kernel.event_listener, event: kernel.request}
In UrlManager.php :
public function onKernelRequest(GetResponseEvent $event)
{
$request = Request::createFromGlobals();
$hostname = parse_url($request->server->get('HTTP_HOST'),PHP_URL_HOST);
/*
* here my treatment that works fine :)
*/
But as i'm in DEV mode, the same event is fired again by the debug toolbar...
The only workaround i found was by adding this before my treatment :
if (substr($request->server->get('REQUEST_URI'),0,6) != '/_wdt/') {
Also works fine, but i think it's not the best thing to do, because something very specific will stay in the project, and only for DEV mode.
Is there a way to "tell" the toolbar not to fire this event ? Maybe something to add in services.yaml ? Or some other config parameter ?
So I did a bit more research. It's not that the kernel event is being fired twice but rather that once your original page is sent to the browser a bit of javascript initiates a second _wdt request for additional information. So you actually have two independent requests. You can see this by pressing F12 in your browser and then selecting the network tab and refreshing.
It is easy enough to filter the debug request since the name of the route will always be _wdt. And you can also get the host directly from the request. Still want to check for the master request because eventually your code might trigger sub requests.
public function onRequest(GetResponseEvent $event)
{
// Only master
if (!$event->isMasterRequest()) {
return;
}
$request = $event->getRequest();
// Ignore toolbar
if ($request->attributes->get('_route') === '_wdt') {
return;
}
// Avoid parsing urls and other stuff, the request object should have
// all the info you need
$host = $request->getHost();
}

return from onBootStrap() in zend

I am calling one function from onBootStrap() to authorize user, in that function I am using header information to verify the user.
If this is not correct, I want to stop execution here(onBootStrap()) without even calling the actual API and return some response to the user .
User should get some response because then only user can know what's the problem.
How I can return response from there?
Simply said, onBootstrap is not sufficient for this. Usually, you have two stages in your application. The first is bootstrapping, the second is running. During run you can authorize users and return responses, during bootstrap this is not possible.
The reason is simple, you might have another module overriding it's behaviour. If you stop bootstrapping after your module, you can stop the execution of these modules. It's better to move the logic to run. This run stage is defined with various listeners, of which the first is route. There isn't much going on after bootstrap and before route, so in terms of performance it's neglectable.
A code example:
use Zend\Mvc\MvcEvent;
use Zend\Json\Json;
class Module
{
public function onBootstrap($e)
{
$app = $e->getApplication();
$em = $app->getEventManager();
$em->attach(MvcEvent::EVENT_ROUTE, function($e) use ($app) {
// your auth logic here
if (!$auth) {
$response = $e->getResponse();
$response->setStatusCode(403);
$response->setContent(Json::encode(array(
'error' => 12345,
'message' => 'You are not authorized for this request',
));
return $response;
}
}, PHP_INT_MAX);
}
}
The listener is attached at an very early stage (PHP_INT_MAX) so the check happens as first in the complete route stage. You can also choose for quite a high number (like, 1000) so you can hook in this event before user authorization.

How to create IP blacklist in Symfony2?

Yes, I know there's Voter tutorial in cookbook. But I'm looking for something slightly different. I need two different layers of blacklisting:
deny certain IP to access whole site
deny certain IP to log in
I wrote Voter that checks if user's IP is in database. For first scenario, I wrote a kernel listener that checks every request and throws 403 in case it encounters banned user:
if (VoterInterface::ACCESS_DENIED === $this->voter->vote($token, $this, array())) {
throw new AccessDeniedHttpException('Blacklisted, punk!');
}
First problem lies in VoterInterface itself, which forces me to use TokenInterface $token, which I don't really need in this case. But that doesn't matter that much I guess. Next thing is that I actually had to use AccessDeniedHttpException as AccessDeniedException always tries to redirect me to login page and causes endless redirect loop in this case. I'd live with it as it works just fine in dev environment, but when I switch to prod I keep getting 503 with following in prod log:
[2011-11-21 20:54:04] security.INFO: Populated SecurityContext with an
anonymous Token [] []
[2011-11-21 20:54:04] request.ERROR:
Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException:
Blacklisted, punk! (uncaught exception) at xxx line 28 [] []
[2011-11-21 20:54:04] request.ERROR: Exception thrown when handling an
exception
(Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException:
Blacklisted, punk!) [] []
From what I've read, it might be problem with xdebug, but it happens even when I turn it off. I also tried vanilla \Exception and it does the same thing. Anyone have any idea why it happens? Or maybe some other solution for such blacklisting case.
Also, I've no idea how to solve second case as I don't know how to stop user before he gets token assigned. My current solution is dealing with InteractiveLoginEvent, checking if user is blacklisted and if so, removing his token. It doesn't seem to be a safe one and I'm not really comfortable with it. So, any idea how to solve this one? I guess I'm just missing some obvious "pre login event".
To deny access to the entire website, you can adapt the whitelist code used to secure the dev environment. Stick something like this in app.php:
if (in_array(#$_SERVER['REMOTE_ADDR'], array('127.0.0.1', '1.2.3.4',))) {
header('HTTP/1.0 403 Forbidden');
exit('You are not allowed to access this site.');
}
For site-wide IP restrictions it's best to handle them at the apache level, so your app does not even get hit by the request. In case you are trying to keep out a spammer, this way you don't waste any resources on their sometimes automated requests. In your case, writing the deny rules to the .htaccess file would be appropriate. In larger setups you can also configure a firewall to block specific IPs so those requests don't even hit your server at all.
To the first problem – there are filters in EventDispatcher, so you can throw AccessDeniedHttpException before Controller start process request.
To the second problem – if you use custom User Provider you can check for banned IP addresses in UserRepository.
namespace Acme\SecurityBundle\Entity;
//… some namespaces
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* UserRepository
*/
class UserRepository extends … implements …
{
public function loadUserByUsername($username)
{
if ( $this->isBanned() ) {
throw new AccessDeniedHttpException("You're banned!");
}
//… load user from DB
}
//… some other methods
private function isBanned()
{
$q = $this->getEntityManager()->createQuery('SELECT b FROM AcmeSecurityBundle:BlackList b WHERE b.ip = :ip')
->setParameter('ip', #$_SERVER['REMOTE_ADDR'])
->setMaxResults(1)
;
$blackList = $q->getOneOrNullResult();
//… check if is banned
}
}
you also can use a firewall on the server, for example: http://www.shorewall.net/blacklisting_support.htm
which blocks the given ips completly of the server.
to autogenerate such a blacklist file, look at the following example:
http://www.justin.my/2010/08/generate-shorewall-blacklist-from-spamhaus-and-dshield/
You can easily block IP and range of IP using my bundle => https://github.com/Spomky-Labs/SpomkyIpFilterBundle
It's not a best practice. Insight (analyse by Sensio) returns : "Using PHP response functions (like header() here) is discouraged, as it bypasses the Symfony event system. Use the HttpFoundationResponse class instead." and "$_SERVER super global should not be used."
<?php
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$loader = require_once __DIR__.'/../app/bootstrap.php.cache';
require_once __DIR__.'/../app/AppKernel.php';
$request = Request::createFromGlobals();
$client_ip = $request->getClientIp();
$authorized_hosts = ['127.0.0.1', 'fe80::1', '::1', 'localhost', 'yourIpAddress'];
// Securisation
if (!in_array($client_ip, $authorized_hosts)) {
$response = new Response(
"Forbidden",
Response::HTTP_FORBIDDEN,
array('content-type' => 'text/html')
);
$response->send();
exit();
}
$kernel = new AppKernel('prod', false);
$kernel->loadClassCache();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
It's ok for SensioInsight

Categories