Chapters
-
Course Code
Subscribe to download the code!Compatible PHP versions: ^7.1.3, <8.0
Subscribe to download the code!Compatible PHP versions: ^7.1.3, <8.0
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Subtitles
Subscribe to download the subtitles!
Subscribe to download the subtitles!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
Login Success & the Session
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
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.
28 Comments
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!
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 :)
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!
Thanks for the answer ! I did a fade out and hide on success finally.
Thanks again !
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.
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!
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.
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!
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
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/current/components/http_foundation/session_configuration.html#configuring-garbage-collection'
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!
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?!
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!
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.
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!
Yes! Thank you!
It was a guard authenticator that I created before and forgot to remove it in the security.yaml file.
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.
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!
I thing in this case it might be good idea to just use "plaintext" hashing algorithm. At least for this chapter only. ;)
Hey TomaszGasior
Thanks for the feedback, I think Ryan wanted to talk about the security:encode
command :)
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.
experiencing the same
I have a similar error 401, but for example if I explicitly set the algorithm: bcrypt option in security.yaml, all is working nice.
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 <?php phpinfo(); ?>
in browser. I guess you will find some different versions or configuration
Cheers!
Hey sadikoff Yes, you are right. I didn't notice distinguish between cli php version and symfony server php version. Thank you.
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!
"Houston: no signs of life"
Start the conversation!
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.21.6
"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
}
}
how would we log into the swagger ui documentation pages without a front end? For example I'm using the distribution package.