Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Login Success & the Session

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Let's see if we can log in for real. But first... we, uh, need to put some users in our database. Head to /api - we'll use our API to do that! Eating our own dog food.

I do have a few users in my database already... but you probably don't... and I don't think I set any of these with real passwords anyways. So let's create a brand new shiny user.

But... our API currently has a big shortcoming. I'll close this and open up the POST endpoint. When someone uses our API to create a user, they will eventually send the plain-text password on the password field. But... in the database, this field needs to be set to an encoded version of that password. So far, we don't have any mechanism to intercept the plain text password and encode it before it gets to the database.

We'll fix this soon, but we're going to cheat for now. Find your terminal and run:

php bin/console security:encode

This is a fun utility where you can give it a plain-text password - I'll use foo - and it will give us back an encoded version of that password. Copy that.

Now we can use our endpoint: I'll use the POST endpoint with email set to quesolover@example.com, password set to the long, encoded password string, username set to quesolover and I'll remove the cheeseListings field: we don't need to create any cheese listings right now. Hit "Execute" and... perfect! A 201 status code. Say hello to quesolover@example.com!

Copy that email address, then go back to our homepage. On the web debug toolbar, you can see that we are not logged in: we are anonymous.

Ok, let me open my browser's debugger again... then try to log in: quesolover@example.com, password foo and... nothing updates on our Vue.js app yet... but let's see what happened with that AJAX request.

Yea! It returned a 200 status code with a user key set to 6! It worked! And that response is coming from our SecurityController: we're returning that data.

Wait, Where's my API Token?

But wait, it gets better! If we refresh the homepage, we are now logged in as quesolover. And our important job number one is done! Just because we're creating an API doesn't mean that we now need to start thinking about some crazy API token system where the authentication endpoint returns a token string, we store that in JavaScript and then we send that as an Authorization header on all future requests. No, forget that! We're done! Starting now, all future AJAX requests will automatically send the session cookie and we'll be authenticated like normal. It's just that simple.

And yes, we are going to talk a bit about API token authentication later. But... there's a good chance you don't need it. And if you don't need it... but try to use it anyways, you'll complicate your app & may make it less secure. As a general rule, while you can use API tokens in your JavaScript, you should never store them anywhere - like local storage of cookies due to security. That makes using API tokens in JavaScript... tricky.

So... if we're not going to return an API token from the authentication endpoint... what should we return? Just returning the number 6... probably isn't very useful: our JavaScript won't know the email, username or any other information about who just logged in. So... what should we return? There's not a perfect answer to that question, but I'll show you what I recommend next.

Leave a comment!

28
Login or Register to join the conversation
Patrick Avatar
Patrick Avatar Patrick | posted 2 years ago

how would we log into the swagger ui documentation pages without a front end? For example I'm using the distribution package.

1 Reply

Hey Patrick

I'm not sure I understood your question correctly but if what you are looking for is a way to dump the documentation of your API, here you can see some examples of how to do it via a command https://api-platform.com/do...

I hope it helps. Cheers!

Reply
Piotr S. Avatar

:)

Reply
Default user avatar
Default user avatar Pax Bryan | posted 1 year ago

I'm using this in a modal and it works. Although, how can I either refresh my page(1), redirect(2) or close the modal and be connected (3) ?
I would prefere the third solution but at least one would be great :)

Reply

Hey Pax Bryan

So if you work with modals, you should send your form via ajax, and you can do everything you want in JavaScript. All you need is configure correct response on success login, for example 200 json response with some data to validate. Then you will be able to use document.location.href = document.location.href this will reload page, or document.location.href = response.redirectUrl if you pass redirect in your response, or you can just close your modal and update only needed parts of your page via ajax, you will be authenticated =)

Cheers!

Reply
Default user avatar

Thanks for the answer ! I did a fade out and hide on success finally.
Thanks again !

Reply
Jean-tilapin Avatar
Jean-tilapin Avatar Jean-tilapin | posted 1 year ago

Hi!
Quick question about auto-login after registration using json_login: I've seen on Symfony website :


return $guardHandler->authenticateUserAndHandleSuccess(
$user, // the User object you just created
$request,
$authenticator, // authenticator whose onAuthenticationSuccess you want to use
'main' // the name of your firewall in security.yaml
);


But in that particular case, what is the $authenticator default value, when in my security.yaml I just have what you wrote last chapter? I'm not looking for something fancy (yet), but the regular json_login default success handler.

Thank you.

Reply

Hey Xav,

I don't see authenticateUserAndHandleSuccess() has any optional arguments, so I suppose there's no default values for it, you always should pass those values. And we're passing our form authenticator here: https://symfonycasts.com/sc...

I hope this helps!

Cheers!

Reply
Jean-tilapin Avatar
Jean-tilapin Avatar Jean-tilapin | victor | posted 1 year ago

Hi! Thank you for your answer but I still don't understand. We've created in chapter 3 the login process just by adding "json_login" in the firewall, and it works with no extra file, listener or LoginFormAuthenticator? So I don't get what triggers onAuthenticationSuccess I want to use for the auto-login process after registration. This is probably a vey dumb question and I must have missed something obvious. Sorry.

Reply

Hey Xav,

If you have a custom Guard authenticator - I'm afraid it will handle your login requests then, and so you will be end in onAuthenticationSuccess() or onAuthenticationFailure(). I think you can easily debug it there. So, the idea would be to check if you have an AJAX request there in your guard authenticator.. and if so - handle it as AJAX request and return a json response for example, otherwise handle it as a normal post request with redirecting to the specific page.

Cheers!

Reply
Sebastian K. Avatar
Sebastian K. Avatar Sebastian K. | posted 2 years ago

It seems my cookie get lost after a short period of time

After the login I get the cookie

Set-Cookie: PHPSESSID=ch4cmqui34pmvj07hbco8tpon2; expires=Fri, 15-Oct-2021 20:49:16 GMT; Max-Age=31536000; path=/; secure; HttpOnly; SameSite=lax

and every request uses this cookie

Cookie: PHPSESSID=ch4cmqui34pmvj07hbco8tpon2

but after a few minutes, this is in my response headers:

Set-Cookie: PHPSESSID=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly

This is my framework.session from symfony


session:
handler_id: null
cookie_secure: auto
cookie_samesite: lax
cookie_lifetime: 31536000
Reply

Hey Sebastian K.!

Hmmm. It looks like your session is being invalidated/expired on the server side. Assuming this happens randomly (I mean, it's not a specific page that causes this), then I think it would be garbage collection on your session storage.

Check out this: https://symfony.com/doc/cur...

Specifically, check the session.gc_maxlifetime setting in php.ini. Session garbage control is weird... this defines how long a session needs to be idle before it is *eligible* to be "garbage controlled". Then there is a different equation to figure out if a specific request will actually trigger a garbage collection process or not.

Anyways, set session.gc_maxlifetime to a higher value and see if it helps. We set this to a higher value than the default here on SymfonyCasts.

Cheers!

Reply
Sebastian K. Avatar

Hey, thanks for the reply. I changed my session config to


session:
handler_id: 'session.handler.native_file'
save_path: '%kernel.project_dir%/var/sessions/%kernel.environment%'
cookie_secure: auto
cookie_samesite: lax
cookie_lifetime: 31536000
gc_maxlifetime: 31536000

and it works. But I am not sure if it's the right way to set it to the same value as the cookie_lifetime?!

Reply

Hey Sebastian K. !

Excellent! So, let me clarify what each of those settings does:

* gc_maxlifetime defines how long a session can be IDLE before it is removed from the server. So, if a session has not been used for 31536000 seconds, it is eligible to be removed. But, if the session remains active, it won't be idle and will not be removed.

* cookie_lifetime defines how long the session cookie can be used. This means that after 31536000 seconds, the cookie well become invalid and the session will end, no matter what - even if the user is still actively browsing your site.

I hope this helps!

Cheers!

Reply
Mohammed Avatar
Mohammed Avatar Mohammed | posted 2 years ago

Hi! I have an issue that I couldn't seem to solve.

I'm using Angular for the front-end and whenever I make a POST request I get a 302 code and it redirects and changes my POST request into a GET, therefore all passed data (email, password) gets lost. And the response of that GET request always contains { user: null }

Sorry if this might be not a server side issue. I'd appreciate any help.

Reply

Hey Mohammed

You need to check your what's doing the server when you make that request. Do you have a redirect set up any where? It could come from the web server itself, or maybe a .htaccess rule. Or, it could be Symfony telling you that you need to login first before accessing such page
To where it redirects you?

Cheers!

1 Reply
Mohammed Avatar

Yes! Thank you!
It was a guard authenticator that I created before and forgot to remove it in the security.yaml file.

Reply

What about mobile app, for example, or other resource on the other domain? For example, the api will be located on api.example.com and the client on example.com. In this case api will not provide a cookie, because it's not a samesite. You said that you will explain this later, but I can't see this anywhere.

Reply

Hey @Алексей Хромец!

> You said that you will explain this later, but I can't see this anywhere

Yea, sorry about that! This really became a "we'll talk about this in a future tutorial" but that tutorial hasn't happened yet :p.

> What about mobile app, for example, or other resource on the other domain? For example, the api will be located on api.example.com and the client on example.com.

So, there are a few different situations. And this stuff is *tricky*, at least if you truly want to be secure. I will also admit that I'm not an expert here - though I have done quite extensive research.

A) Let's start with a mobile app. This is one of my favorite resources: https://www.ory.sh/oauth2-f.... As you can see, they highlight some problematic approaches if you're worried about "spoofing" apps that pretend to be legitimate. One caveat with that blog post is that it talks about OAuth, which *technically* isn't necessary if you only need to support authentication for YOUR app, and not allow 3rd party apps to authenticate (that's the true power of OAuth). So I believe (but have not ever done this myself) that you could also use an HTTP Only cookie for authentication with your app. For example, on authentication, you might create a JWT (or type of "token") and set it on an HTTP Only cookie. Then your mobile app would send that cookie back to the site for authentication (you would have an authenticator that reads the cookie and logs in). But overall, I know the least about mobile apps.

B) The second situation is "or other resource on the other domain". Let's assume that you are talking to an *entirely* different domain. This is tricky. The best practice (I believe) would be for your API to send back a token (JWT, or whatever, that's not as important) on a header so that your JavaScript can read it (you'll of course also need to make sure your API allows cross-site scripting via CORS). But then, you're not supposed to "store" this token anywhere in JavaScript - https://auth0.com/docs/toke... - that's really not considered secure. If you follow that rule, then you have the problem that each time the user closes their browser (or even refreshes the page), you lose the token and are no longer authenticated. From my research, in general, making authenticated AJAX requests from JavaScript to an external domain is something you should try to avoid. The safer practice is to make an AJAX request to your *own* domain, and then *it* makes the API request to the external API for you (and then returns the results). In that setup, because you have a "backend", you can safely store tokens or use cookies.

C) The last situation is "the api will be located on api.example.com and the client on example.com". Unless I'm mistaken, this doesn't pose a problem - these are on the SameSite because they share the same root domain. So you can use a SameSite HttpOnly Session cookie.

Let me know if this helps!

Cheers!

1 Reply

The *B)* case is smart enough :)
Thank you, Ryan!

Reply

I thing in this case it might be good idea to just use "plaintext" hashing algorithm. At least for this chapter only. ;)

Reply

Hey TomaszGasior

Thanks for the feedback, I think Ryan wanted to talk about the security:encode command :)

Reply
Default user avatar
Default user avatar Codsworth | posted 3 years ago

I can register new users without problem through https://localhost:8000/api. I'm using the hashed password from ./bin/console security:encode when register user, and get 201 responses. But still get 401 {"error":"Invalid credentials."} responses under network in Chrome. Also get 401 when I don't hast the password.

Reply
Czarek B. Avatar

experiencing the same

Reply

I have a similar error 401, but for example if I explicitly set the algorithm: bcrypt option in security.yaml, all is working nice.

Reply

Hey Serhii

Looks like your cli and http php version configured differently or even php versions are different, checkout your php -v or php -i in console and in browser. I guess you will find some different versions or configuration

Cheers!

Reply

Hey sadikoff Yes, you are right. I didn't notice distinguish between cli php version and symfony server php version. Thank you.

Reply

Hey!

Hmm, invalid passwords like this are hard to debug... because you can't *actually* see for yourself if the passwords are the same or not. The easiest way to try to debug this might be to switch ahead to chapters 19 & 20 - https://symfonycasts.com/sc... - to where we start encoding the plain-text password automatically. That's the only way to rule out some sort of human error, or database field being too short or any other reason that the passwords might accidentally not be matching.

I hope this helps! Let me know what you find out.

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial works great for Symfony 5 and API Platform 2.5/2.6.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3, <8.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^2.1", // v2.4.5
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^1.6", // 1.11.2
        "doctrine/doctrine-migrations-bundle": "^2.0", // v2.0.0
        "doctrine/orm": "^2.4.5", // v2.7.2
        "nelmio/cors-bundle": "^1.5", // 1.5.6
        "nesbot/carbon": "^2.17", // 2.21.3
        "phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.1
        "symfony/asset": "4.3.*", // v4.3.2
        "symfony/console": "4.3.*", // v4.3.2
        "symfony/dotenv": "4.3.*", // v4.3.2
        "symfony/expression-language": "4.3.*", // v4.3.2
        "symfony/flex": "^1.1", // v1.18.7
        "symfony/framework-bundle": "4.3.*", // v4.3.2
        "symfony/http-client": "4.3.*", // v4.3.3
        "symfony/monolog-bundle": "^3.4", // v3.4.0
        "symfony/security-bundle": "4.3.*", // v4.3.2
        "symfony/twig-bundle": "4.3.*", // v4.3.2
        "symfony/validator": "4.3.*", // v4.3.2
        "symfony/webpack-encore-bundle": "^1.6", // v1.6.2
        "symfony/yaml": "4.3.*" // v4.3.2
    },
    "require-dev": {
        "hautelook/alice-bundle": "^2.5", // 2.7.3
        "symfony/browser-kit": "4.3.*", // v4.3.3
        "symfony/css-selector": "4.3.*", // v4.3.3
        "symfony/maker-bundle": "^1.11", // v1.12.0
        "symfony/phpunit-bridge": "^4.3", // v4.3.3
        "symfony/stopwatch": "4.3.*", // v4.3.2
        "symfony/web-profiler-bundle": "4.3.*" // v4.3.2
    }
}