I have installed the packages Google-mailer, doctrine -fixtures-bundle, and webpack-bundle. I'm at the beginning of this project and there to create the first controller which should have a form to login. Controller and From were created with the commands bin/console make: controller and make: form, and not changed. But I already have a problem because I get an error when I call the form
Failed to start the session: already started by PHP.
I can explain this message with the fact that when creating the view Symfony tries to start a session because the form is protected with csrf (which should stay that way). But this should not be a problem because Symfony manages the sessions.
php.ini => session.auto_start=0;
framework.yaml:
framework:
secret: '%env(APP_SECRET)%'
#csrf_protection: true
#http_method_override: true
# Enables session support. Note that the session will ONLY be started if you read or write from it.
# Remove or comment this section to explicitly disable session support.
session:
handler_id: null
cookie_secure: auto
cookie_samesite: lax
#esi: true
#fragments: true
php_errors:
log: true
Test function in HomeController:
/**
* #Route("/testform", name="testform", methods={"GET","POST"})
*/
public function testform()
{
$User = new User();
$form = $this->createForm(TestFormType::class, $User);
$form->handleRequest($this->request);
if ($form->isSubmitted() && $form->isValid()) {
return $this->redirectToRoute('home');
}
return $this->render('home/testform.html.twig', [
'form' => $form->createView(),
]);
}
TestFormType:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email')
->add('forname')
->add('name')
->add('password');
}
First off, I am building using Symfony components. I am using 3.4. I was following the form tutorial https://symfony.com/doc/3.4/components/form.html which lead me to this page
https://symfony.com/doc/current/forms.html#usage
I noticed that Symfony added a Form directory to my application.
This was great! I thought I was on my way. So, in my controller, I added this line.
$form = Forms::createFormFactory();
When I tried loading the page, everything went well with no error messages until I added the next two lines.
->addExtension(new HttpFoundationExtension())
->getForm();
I removed the ->addExtension(new HttpFoundationExtension()) line and left the ->getForm() thinking it would process without the add method call. It did not. So, I backed up to see if the IDE would type hint for me.
In the IDE PHPStorm, these are the methods that I have access to but not getForm per the tutorial
Every tutorial I have tried ends with not being able to find some method that does not exist. What do I need to install in order to have access to the ->getForm() method?
UPDATE:
I have made a couple of steps forward.
$form = Forms::createFormFactory()
->createBuilder(TaskType::class);
The code above loads with no errors. (Why is still fuzzy). But next stop is the createView(). None existant also. I only get hinted with create().
Reading between the lines in this video help with the last two steps. https://symfonycasts.com/screencast/symfony3-forms/render-form-bootstrap#play
UPDATE 2:
This is what I have now.
$session = new Session();
$csrfManager = new CsrfTokenManager();
$help = new \Twig_ExtensionInterface();
$formFactory = Forms::createFormFactoryBuilder()
->getFormFactory();
$form = $formFactory->createBuilder(TaskType::class)
->getForm();
//$form->handleRequest();
$loader = new FilesystemLoader('../../templates/billing');
$twig = new Environment($loader, [
'debug' => true,
]);
$twig->addExtension(new HeaderExtension());
$twig->addExtension(new DebugExtension());
$twig->addExtension($help, FormRendererEngineInterface::class);
return $twig->render('requeueCharge.html.twig', [
'payments' => 'Charge',
'reportForm' => $form->createView()
]);
Does anyone know of an update standalone for example? The one that everyone keeps pointing two is 6 years old. There have been many things deprecated in that time period. So, it is not an example to follow.
Your Form class and method createFormFactory must return object that implement FormBuilderInterface then getForm method will be available. You need create formBuilder object.
But this can't be called from static method because formBuilder object need dependency from DI container. Look at controller.
If you want you need register your own class in DI and create formBuilder object with dependencies and return that instance of object.
EDIT
You don't need to use abstract controller. You can create your own class which is registered in DI for geting dependencies. In that class you create method which create new FormBuilder(..dependencies from DI from your class ...) and return instance of that FormBuilder. Then you can inject your class in controller via DI.
Example (not tested)
// class registered in DI
class CustomFormFactory
{
private $_factory;
private $_dispatcher;
public CustomFormFactory(EventDispatcherInterface $dispatcher, FormFactoryInterface $factory)
{
$_dispatcher = $dispatcher;
$_factory = $factory;
}
public function createForm(?string $name, ?string $dataClass, array $options = []): FormBuilderInterface
{
// create instance in combination with DI dependencies (factory..) and your parameters
return new FormBuilder($name, $dataClass, $_dispatcher, $_factory, $options);
}
}
Usage
$factory = $this->container->get('CustomFormFactory');
$fb = $factory->createForm();
$form = $fb->getForm();
Today I noticed something disturbing while inspecting session files in storage/framework/sessions folder created by Laravel 5.
Here is what happened:
I logged in as user A
I navigated to a page which stores variable X in the Session
I logged out, but did not close the browser.
Session file in storage/framework/sessions was still there, and browser
cookie was alive.
I logged in as user B.
The old session file in storage/framework/sessions got deleted and a new session file was there.
I looked into the new session file - surprise! variable X has survived log-out and is still there, accessible for user B!
It leads to security concerns because now user B has access to the data of user A.
While debugging through Laravel source code, I found out that Session Store is never being cleared up during logout/login process. Only login credentials are being removed in Illuminate\Auth\Guard::clearUserDataFromStorage() method, but all the session Store attributes are still there, and then later when $kernel->terminate($request, $response); is called, which in turn leads to Illuminate\Session\Middleware\StartSession::terminate() calling Store::save(), which blindly saves $this->attributes to the new session, ignoring the fact that it now belongs to another user.
From one hand, it seems logical - Laravel has no assumptions about my data and whether I want it to expire together with authentication or not. But it would be great to have it documented somewhere with a solution to attach some sensitive data to authentication object and expire together with it.
This means that I as a programmer am responsible for completely clearing away all the sensitive data from current session when a new (or the same) user is logging in.
Clearing on logging out would not be reliable, because user might never click Logout link but wait for the session to "expire", which for Laravel still does not clear up the session.
One more thing to keep in mind: I should not clear the session too early - there is AntiForgery token which must be present, or else login form will always fail.
I have found a forum topic which also tries to solve somewhat similar problem:
http://laravel.io/forum/04-27-2014-how-to-expire-session-data
I got confused by this:
I had another go at it today and realised what the problem was: Session::flush() does not delete session data that the app creates, such as the shopping cart details
If this is true, then the only way to get completely rid of session would be to use PHP native session_unset() and session_destroy() but I wouldn't want to go that way - I would prefer to find a cleaner, Laravel-ish solution, if possible.
How do I tell Laravel that I want my old session data to be removed together with user authentication data when authentication expires or user logs out?
In the laravel docs it says you can:
Removing An Item From The Session
Session::forget('key');
Removing All Items From The Session
Session::flush();
You could navigate to the AuthenticatesAndRegistersUsers.php trait and rewrite
/**
* Log the user out of the application.
*
* #return \Illuminate\Http\Response
*/
public function getLogout()
{
$this->auth->logout();
return redirect(property_exists($this, 'redirectAfterLogout') ? $this->redirectAfterLogout : '/');
}
to
/**
* Log the user out of the application.
*
* #return \Illuminate\Http\Response
*/
public function getLogout()
{
Session::flush();
$this->auth->logout();
return redirect(property_exists($this, 'redirectAfterLogout') ? $this->redirectAfterLogout : '/');
}
I have no idea if this actually work, but give it a try :)
Update
According to this answer here on Stack Overflow, you can set the session to expire on browser close, or after XXX minutes. Used together with the above solution, it should fix the problem?
In config/session.php
/*
|--------------------------------------------------------------------------
| Session Lifetime
|--------------------------------------------------------------------------
|
| Here you may specify the number of minutes that you wish the session
| to be allowed to remain idle before it expires. If you want them
| to immediately expire on the browser closing, set that option.
|
*/
'lifetime' => 120,
'expire_on_close' => false
I believe this is the correct answer to this question/problem:
When making multiple requests in one test, the state of your laravel application is not reset between the requests. The Auth manager is a singleton in the laravel container, and it keeps a local cache of the resolved auth guards. The resolved auth guards keep a local cache of the authed user.
So, your first request to your api/logout endpoint resolves the auth manager, which resolves the api guard, which stores a references to the authed user whose token you will be revoking.
Now, when you make your second request to /api/user, the already resolved auth manager is pulled from the container, the already resolved api guard is pulled from it's local cache, and the same already resolved user is pulled from the guard's local cache. This is why the second request passes authentication instead of failing it.
When testing auth related stuff with multiple requests in the same test, you need to reset the resolved instances between tests. Also, you can't just unset the resolved auth manager instance, because when it is resolved again, it won't have the extended passport driver defined.
So, the easiest way I've found is to use reflection to unset the protected guards property on the resolved auth manager. You also need to call the logout method on the resolved session guards.
Source: Method Illuminate\Auth\RequestGuard::logout does not exist Laravel Passport
To use that, add this to:
TestCase.php
protected function resetAuth(array $guards = null) : void
{
$guards = $guards ?: array_keys(config('auth.guards'));
foreach ($guards as $guard) {
$guard = $this->app['auth']->guard($guard);
if ($guard instanceof SessionGuard) {
$guard->logout();
}
}
$protectedProperty = new \ReflectionProperty($this->app['auth'], 'guards');
$protectedProperty->setAccessible(true);
$protectedProperty->setValue($this->app['auth'], []);
}
Then, use it like this:
LoginTest.php
class LoginTest extends TestCase
{
use DatabaseTransactions, ThrottlesLogins;
protected $auth_guard = 'web';
/** #test */
public function it_can_login()
{
$user = $this->user();
$this->postJson(route('login'), ['email' => $user->email, 'password' => TestCase::AUTH_PASSWORD])
->assertStatus(200)
->assertJsonStructure([
'user' => [
'id' ,
'status',
'name',
'email',
'email_verified_at',
'created_at',
'updated_at',
'photo_url',
'roles_list',
'roles',
],
]);
$this->assertEquals(Auth::check(), true);
$this->assertEquals(Auth::user()->email, $user->email);
$this->assertAuthenticated($this->auth_guard);
$this->assertAuthenticatedAs($user, $this->auth_guard);
$this->resetAuth();
}
/** #test */
public function it_can_logout()
{
$this->actingAs($this->user())
->postJson(route('logout'))
->assertStatus(204);
$this->assertGuest($this->auth_guard);
$this->resetAuth();
}
/** #test */
public function it_should_get_two_cookies_upon_login_without_remember_me()
{
$user = $this->user();
$response = $this->postJson(route('login'), [
'email' => $user->email,
'password' => TestCase::AUTH_PASSWORD,
]);
$response->assertCookieNotExpired(Str::slug(config('app.name'), '_').'_session');
$response->assertCookieNotExpired('XSRF-TOKEN');
$this->assertEquals(config('session.http_only'), true);
$this->resetAuth();
}
/** #test */
public function it_should_get_three_cookies_upon_login_with_remember_me()
{
$user = $this->user();
$response = $this->postJson(route('login'), [
'email' => $user->email,
'password' => TestCase::AUTH_PASSWORD,
'remember' => true,
]);
$response->assertCookieNotExpired(Str::slug(config('app.name'), '_').'_session');
$response->assertCookieNotExpired('XSRF-TOKEN');
$response->assertCookieNotExpired(Auth::getRecallerName());
$this->resetAuth();
}
/** #test */
public function it_should_throw_error_422_on_login_attempt_without_email()
{
$this->postJson(route('login'), ['email' => '', 'password' => TestCase::AUTH_PASSWORD])
->assertStatus(422)
->assertJsonStructure(['message', 'errors' => ['email']]);
$this->assertGuest($this->auth_guard);
$this->resetAuth();
}
/** #test */
public function it_should_throw_error_422_on_login_attempt_without_password()
{
$this->postJson(route('login'), ['email' => $this->adminUser()->email, 'password' => ''])
->assertStatus(422)
->assertJsonStructure(['message', 'errors' => ['password']]);
$this->assertGuest($this->auth_guard);
$this->resetAuth();
}
/** #test */
public function it_should_throw_error_422_on_login_attempt_with_empty_form()
{
$this->postJson(route('login'), ['email' => '', 'password' => ''])
->assertStatus(422)
->assertJsonStructure(['message', 'errors' => ['email', 'password']]);
$this->assertGuest($this->auth_guard);
$this->resetAuth();
}
/** #test */
public function it_should_throw_error_401_as_guest_on_protected_routes()
{
$this->assertGuest($this->auth_guard);
$this->getJson(route('me'))
->assertStatus(401)
->assertJson(['message' => 'Unauthenticated.']);
}
/** #test */
public function it_should_throw_error_429_when_login_attempt_is_throttled()
{
$this->resetAuth();
$throttledUser = factory(User::class, 1)->create()->first();
foreach (range(0, 9) as $attempt) {
$this->postJson(route('login'), ['email' => $throttledUser->email, 'password' => "{TestCase::AUTH_PASSWORD}_{$attempt}"]);
}
$this->postJson(route('login'), ['email' => $throttledUser->email, 'password' => TestCase::AUTH_PASSWORD . 'k'])
->assertStatus(429)
->assertJson(['message' => 'Too Many Attempts.']);
$this->resetAuth();
}
}
A note about the throttle one. It took me several days to figure out how to ensure that 429 behaviour. Earlier unit tests will increase the number of 'attempts' leading up to the throttling, so you need to resetAuth before the throttle test or the throttle will be triggered at the wrong time and screw the test.
Given the above unit test code, I am using this:
Route::group(['middleware' => ['guest', 'throttle:10,5']], function () { /**/ });
You can observe it working by changing any of those numbers, like 10,5 to 9,5 or 11,5 and watch how it affects the throttle unit test. You can also uncomment the resetAuth method and watch how it also screws the test.
For unit testing anything related to auth, the resetAuth utility method is extremely useful, must-have. Also the knowledge of auth caching in AuthManager is a must-know to make sense of observed behaviour.
I'm using Laravel 5.0 and trying to Authorize with Dropbox. I'm loosely following this example: http://dropbox.github.io/dropbox-sdk-php/api-docs/v1.1.x/class-Dropbox.WebAuth.html
When I go to /start. I get redirected to Dropbox and click "Allow", but when I get redirected back to /finish, I keep getting Missing CSRF token in session. Does anyone have any ideas? I have read that $_SESSION doesn't work in Laravel but I'm not sure how else to go about it.
Here is the code I am working with:
public function start()
{
$authorizeUrl = $this->getWebAuth()->start();
return redirect()->away($authorizeUrl);
}
public function finish()
{
$test = $this->getWebAuth()->finish($_GET);
dd($test);
}
private function getWebAuth()
{
$appKey = 'key';
$appSecret = 'secret';
$appName = 'name';
$appRedirect = 'http://example.com/finish';
$appInfo = new Dropbox\AppInfo($appKey, $appSecret);
$csrfTokenStore = new Dropbox\ArrayEntryStore($_SESSION, 'dropbox-auth-csrf-token');
$webAuth = new Dropbox\WebAuth($appInfo, $appName, $appRedirect, $csrfTokenStore);
return $webAuth;
}
Update 1:
Okay so I tried getting it working with Laravel Socialite and the Dropbox Socialite provider. I changed my code to what is below, but I get an error when I hit /start. Driver [dropbox] not supported. I got really confused on step 3 of the instructions, so maybe I did something wrong there.
composer.json
"require": {
"laravel/framework": "5.0.*",
"dropbox/dropbox-sdk": "1.1.*",
"laravel/socialite": "~2.0",
"socialiteproviders/dropbox": "~1.0"
},
Controller
use Socialite;
class ExampleController extends Controller {
public function start()
{
return Socialite::with('dropbox')->redirect();
}
public function finish()
{
$user = Socialite::with('dropbox')->user();
dd($user->token);
}
}
config/app.php
'providers' => [
//'Laravel\Socialite\SocialiteServiceProvider',
'SocialiteProviders\Manager\ServiceProvider',
],
'aliases' => [
'Socialite' => 'Laravel\Socialite\Facades\Socialite',
],
app/Providers/EventServiceProvider.php
protected $listen = [
'SocialiteProviders\Manager\SocialiteWasCalled' => [],
];
Update 2:
I figured it out, I added this and it worked.
app/Providers/EventServiceProvider.php
protected $listen = [
'SocialiteProviders\Manager\SocialiteWasCalled' => [
'SocialiteProviders\Dropbox\DropboxExtendSocialite#handle',
],
];
Why reinvent the wheel, if you have a wrapper that can do this for you:
https://github.com/GrahamCampbell/Laravel-Dropbox
The reason is that the POST routes are protected with CSRF. If you do not want to use a wrapper, you need to disable this security layer, but nobody would recommend that.
Even better is using Laravel Socialite. Only the fact is here that Dropbox is not natively supported in it, but this package will solve that.
Credits to ceejayoz for helping with this!
Note: Using a Dropbox package as in #Blaatpraat's answer is generally a better idea than this. If you're dead-set on using your own logic, though:
Laravel 5 POST routes (Dropbox is posting back to you at the end of the process) are protected by default by the CSRF protection middleware. Because Dropbox doesn't know your Laravel app's CSRF token (nor does it know to send one), the _token parameter is missing and fails the middleware.
You'll need to modify app/Http/Middleware/VerifyCsrfToken.php to exempt this route. Where it says:
return parent::handle($request, $next);
You'll want something like this to bypass the CSRF check on certain routes:
if(\Request::is('finish') { return $next($request); }
return parent::handle($request, $next);
I seem to have come across an issue with Symfony 2 that I have not seen before (or more likley im missing something really obvious).
I had a route which took no parameters and worked perfectly, linking up to the controller and displaying the correct view. I then updated the route to take a single parameter like so:
# Note routing
gibbo_dummy_notes_Add:
pattern: /notes/add/{notebook}
defaults: { _controller: GibboDummyBundle:Note:add, notebook: 0}
requirements:
_method: POST|GET
However if I now try and access this route notes/add/test I get the error The controller must return a response (null given). Did you forget to add a return statement somewhere in your controller?. If I remove the parameter it works perfectly notes/add.
This setup is exactly the same as the rest of the routes that use parameters in my app.
The action in the controller definitely returns a response object. If I place a die('test'); at the top of the action and remove the parameter from the URL I reach the die statement and see 'test', but if I add the parameter to the URL it shows the error, so its clearly not reaching my controller or action when including the parameter but as far as I can tell the route is setup correctly, and im not getting the normal route not defined error I would expect to see if there was an issue with the url.
Im running it under the dev environment and I have tried other browsers / cleared cache etc. The action inside the controller NoteController looks like
public function addAction($notebook)
{
$pageTitle = 'Note';
$form = $this->createForm(new NoteType(), null, array(
'validation_groups' => array('add')
));
$request = $this->getRequest();
if ($request->isMethod('POST')) {
$form->bind($request);
if ($form->isValid()) {
/** #var $em \Doctrine\ORM\EntityManager */
$em = $this->getDoctrine()->getManager();
/** #var $note Note */
$note = $form->getData();
$note->setNotebook();
$em->persist($note);
$em->flush();
return $this->redirect($this->generateUrl('gibbo_dummy_note_View'));
}
}
return $this->render('GibboDummyBundle:Note:new.html.twig', array(
'pageTitle' => $pageTitle,
'form_add' => $form->createView(),
'selected' => 'codebooks'
));
}
Can anyone shed some light about what I might be doing wrong?
Please add the controller code for gibbo_dummy_note_View route.
Most likely, your redirection ends up in a bad controller method - the one that does not have return statement