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 with json_login
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
If your login system looks similar to the traditional email & password or username & password setup, Symfony has a nice, built-in authentication mechanism to help. In config/packages/security.yaml
, under the main
firewall, add a new key: json_login
. Below that, set check_path
to app_login
.
security: | |
Show Lines
|
// ... lines 2 - 12 |
firewalls: | |
Show Lines
|
// ... lines 14 - 16 |
main: | |
Show Lines
|
// ... lines 18 - 19 |
json_login: | |
check_path: app_login | |
Show Lines
|
// ... lines 22 - 36 |
This is the name of a route that we're going to create in a second - and we'll set its URL to /login
. Below this, set username_path
to email
- because that's what we'll use to log in, and password_path
set to password
.
security: | |
Show Lines
|
// ... lines 2 - 12 |
firewalls: | |
Show Lines
|
// ... lines 14 - 16 |
main: | |
Show Lines
|
// ... lines 18 - 19 |
json_login: | |
Show Lines
|
// ... line 21 |
username_path: email | |
password_path: password | |
Show Lines
|
// ... lines 24 - 36 |
With this setup, when we send a POST
request to /login
, the json_login
authenticator will automatically start running, look for JSON in the request, decode it, and use the email
and password
keys inside to log us in.
How does it know to load the user from the database... and which field to use for that query? The answer is: the providers
section. This was added in the last tutorial for us by the make:user
command. It tells the security system that our User
lives in Doctrine and it should query for the user via the email
property. If you have a more complex query... or you need to load users from somewhere totally different, you'll need to create a custom user provider or an entirely custom Guard authenticator, instead of using json_login
. Basically, json_login
works great if you fit into this system. If not, you can throw it in the trash and create your own authenticator.
So, there may be some differences between your setup and what we have here. But the really important part - what we're going to do on authentication success and failure - will probably be the same.
The SecurityController
To get the json_login
system fully working, we need to create that app_login
route. In src/Controller
create a new PHP class called, how about, SecurityController
. Make it extend the normal AbstractController
and then create public function login()
. Above that, I'll put the @Route
annotation and hit tab to auto-complete that and add the use
statement. Set the URL to /login
, then name="app_login"
and also methods={"POST"}
: nobody needs to make a GET request to this.
Show Lines
|
// ... lines 1 - 4 |
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |
Show Lines
|
// ... line 6 |
use Symfony\Component\Routing\Annotation\Route; | |
Show Lines
|
// ... line 8 |
class SecurityController extends AbstractController | |
{ | |
/** | |
* @Route("/login", name="app_login", methods={"POST"}) | |
*/ | |
public function login() | |
{ | |
Show Lines
|
// ... lines 16 - 18 |
} | |
} |
Initially, you need to have this route here just because that's the way Symfony works: you can't POST to /login
and have the json_login
authenticator do its magic unless you at least have a route. If you don't have a route, the request will 404 before json_login
can get started.
But also, by default, after we log in successfully, json_login
does... nothing! I mean, it will authenticate us, but then it will allow the request to continue and hit our controller. So the easiest way to control what data we return after a successful authentication is to return something from this controller!
But... hmm... I don't really know what we should return yet - I haven't thought about what might be useful. For now, let's return $this->json()
with an array, and a user
key set to either the authenticated user's id or null.
Show Lines
|
// ... lines 1 - 13 |
public function login() | |
{ | |
return $this->json([ | |
'user' => $this->getUser() ? $this->getUser()->getId() : null] | |
); | |
} | |
Show Lines
|
// ... lines 20 - 21 |
AJAX Login in Vue.js
Let's try this! When we go to https://localhost:8000
, we see a small frontend built with Vue.js. Don't worry, you don't need to know Vue.js - I just wanted to use something a bit more realistic. This login form comes from assets/js/components/LoginForm.vue
.
It's mostly HTML: the only real functionality is that, when we submit the form, it won't actually submit. Instead, Vue will call the handleSubmit()
function. Inside, uncomment that big axios block. Axios is a really nice utility for making AJAX requests. This will make a POST request to /login
and send up two fields of data email
and password
. this.email
and this.password
will be whatever the user entered into those boxes.
Show Lines
|
// ... lines 1 - 23 |
<script> | |
Show Lines
|
// ... lines 25 - 41 |
axios | |
.post('/login', { | |
email: this.email, | |
password: this.password | |
}) | |
.then(response => { | |
console.log(response.data); | |
//this.$emit('user-authenticated', userUri); | |
//this.email = ''; | |
//this.password = ''; | |
}).catch(error => { | |
console.log(error.response.data); | |
}).finally(() => { | |
this.isLoading = false; | |
}) | |
Show Lines
|
// ... lines 58 - 60 |
</script> | |
Show Lines
|
// ... lines 62 - 65 |
One important detail about axios is that it will automatically encode these two fields as JSON. A lot of AJAX libraries do not do this... and it'll make a big difference. More on that later.
Anyways, on success, I'm logging the data from the response and on error - that's .catch()
- I'm doing the same thing.
Since we haven't even tried to add real users to the database yet... let's see what failure feels like! Log in as quesolover@example.com
, any password and... huh... nothing happens?
Hmm, if you get this, first check that Webpack Encore is running in the background: otherwise you might still be executing the old, commented-out JavaScript. Mine is running. I'll do a force refresh - I think my browser is messing with me! Let's try that again: queso_lover@example.com
, password foo
and... yes! We get a 401 status code and it logged error
Invalid credentials.
. If you look at the response itself... on failure, the json_login
system gives us this simple, but perfectly useful API response.
Next, let's hook up our frontend to use this and learn how json_login
behaves when we accidentally send it a, let's say, less-well-formed login request.
44 Comments
Hey Paul!
Very cool setup! Ok, I think you've done the hard parts, we just need to clarify a few things.
1) First, you should only have 1 authenticator. In this case, your custom authenticator. You only need 2 authenticators if there were legitimately creating two separate ways for your user to log in. In your custom authenticator, you are really already doing all of the "work" needed for authentication. The only missing piece, if I understand things correctly, is the JWT response. So, we're going to move that into your custom authenticator. Remove all the json_login
stuff.
2) Yes, you DO need a route for /authentication_token
, or whatever URL that you want your custom authenticator to "operate on". And, json_login
does NOT have anything special about this: it TOO needs this URL/route and it does NOT create it automatically (it's possible the lexik bundle adds it automatically in some way, I can't remember). Anyways, this is no big deal, and to understand why, let me explain the "flow" of what happens on authentication:
A) The user POSTs to /authentication_token
with whatever data
B) In Symfony, BEFORE the security system (i.e. your authenticator) is called, Symfony executes its routing. If /authentication_token
does not match a route, boom! The 404 page is triggered BEFORE the security system is ever initialized.
C) Assuming a route WAS found, the security system is then initialized.
D) IF your authenticator is successful, then what happens next depends on your onAuthenticationSuccess
method. If you return a Response
(which is what you want to do - more on that soon), then that Response
is returned and the controller attached to the route (if there is any - it's possible to create a route in YAML that has no controller) is NEVER executed. If you return null
from onAuthenticationSuccess
, the request continues and Symfony executes the route.
Based on your error, I think you DO have a route for /authentication_token
, but it is not attached to a controller (very likely this lives in config/routes.yaml
or config/routing/{some-other-file}.yaml
). And that is ok! This route exists PURELY so that Symfony doesn't trigger a 404 too early in step (B).
What you need to do is implement onAuthenticationSuccess()
and return a JsonResponse
. Can you reuse the lexik_jwt_authentication.handler.authentication_success
stuff? Actually, yes, though, it's probably easier not too - that class doesn't do all that much for you. Instead, follow the first example here - https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/7-manual-token-creation.rst - autowire JWTTokenManagerInterface
into your class, use it to generate the token, then return a JsonResponse
.
The last missing piece is: how do you add your custom data to the JWT? For that, I believe you should follow this guide by adding a listener to the token creation process: https://symfony.com/bundles/LexikJWTAuthenticationBundle/current/2-data-customization.html#adding-custom-data-or-headers-to-the-jwt
Overall, what I would do is:
1) In my authenticator, get back the data you need from Cognito. Determine the "extra stuff" that you need to include in your JWT. Store this as request attributes, which are just a convenient way to pass data around during a request - e.g. $request->attributes->set('some_cognito_value', 'the-value')
2) In your event listener/subscriber (the docs show a listener, but I would use a subscriber, they're simpler as they don't require any YAML config), read these request attributes and use them to modify the data/payload on the JWT.
And... let me know how this all goes!
Cheers!
Hi Ryan,
I was evidently frustratingly close - just removing the json_login
yaml block and implementing the JsonResponse
on success got me over the line (I'd already implemented a failure response). I used what I think is an EventListener for onJWTCreated
rather than a subscriber, since that's what the documentation suggested and I'd already partially implemented it.
So now I can use the API /authentication_token
endpoint to log in against Cognito and return a JWT with the Cognito access and refresh JWTs embedded inside, along with some other specific user-related parameter from the API database. And when I call another endpoint with the BEARER token, I can access those parameters. [EDIT: I suspect embedding the Cognito tokens inside the API platform access token isn't the right option - I'll likely change it to return the tokens separately - but that a simple refactor now that it's working.]
I'll need to work out token refreshing and password update / reset via the API, but that's less urgent for the current stage of my project.
Thank you very much for getting me unstuck again!
Kind regards, Paul
Hey Paul!
Awesome job! Thanks for sharing your success - I think this is precisely the slightly-more-advanced use-case that the lexik bundle wants to make possible. So very cool to see that you've done it!
Keep up the good work :).
Hi, amazing screencast, thanks!
I I tried to do the same with React but without success, isn't there an authentification tutorial with React? What I did manage to do is create a list of songs (with title and artist) with api-platform and display them in my app.js with axios. I am very pleased with how useful symfony is for managing the backend.
Hey Leonel D.!
Woo! I'm glad it's useful!
> I I tried to do the same with React but without success, isn't there an authentification tutorial with React?
Because we're using cookie-based session authentication in this tutorial, it shouldn't require *anything* special in React. Quite literally, the steps are to:
A) Create a form
B) When that form submits, send a POST Ajax request to /login, including "email" and "password" POST parameters (not as JSON, just as normal POST fields on the request).
And... that's it. You don't even need to do anything with the response. If that Ajax call is successful, its response will contain the session cookie, which your browser will automatically start using. The reason it is a POST request to /login with email & password as POST parameters (not JSON) is simple because that's how I build the "form login endpoint" for this project. We're also using Axios for this in assets/js/components/LoginForm.vue, so you should be able to copy and paste that into your "on submit" React function and have it work just the same.
Let me know if that helps :).
Cheers!
Hi there,
It may not the right chapter to ask this, but I have a small issue with API Platform Security: when I login, i get back the right infos from my entity thanks to the useful groups ("@groups({"user:self", "user:list"}) for example.). Of course, the password isn't in any group. That part works fine.
BUT, when I "PUT" with success something in my user entity, it returns the whole data, including the hashed password and seems to ignore all groups configuration ; for example, when I want to update the nickname of an user.
What have I done wrong? Or is it the expected behaviour? Is it a security issue?
Thanks.
Hey Jean-tilapin!
Hmm. So when you SEND data (like a PUT or a POST), the serialize will use the "deserializationContext" groups, which you might have configured directly on your ApiResource annotation/attribute or you might have configured specifically for that operation. But when that endpoint serializes the data to return to the user, it will then use the "serializationContext" groups... so you should get the same behavior as when you GET a specific user. So I would check and make sure your serializationContext config for the PUT endpoint are what you expect. I usually set this on the "ApiResource" level so that it applies automatically to all operations. If you have *not* configured it at the "ApiResource" level (i.e. directly under the annotation/attribute) and are instead configuring this on an operation-by-operation basis, then you need to make sure that it's also configured for your PUT operation. Overall, it definitely sounds like, for some reason, when it "serializes" for your PUT operation, it is serialization with NO serialization groups (and thus it is included all fields).
Let me know what you find out :).
Cheers!
Hi, amazing screencast, thanks!
I encounter some issues with API-Platform v2.6. Cookies were never send in response to login request. I discovered that the config framework.yaml file had no more session configuration by default. I had to put it back, and to put the stateless attribute to false in api_platform.yaml.
Then, Cookies were back!
But... I'm wondering what are the consequences of disabling the stateless mode. For example, will it impact the Varnish cache? Will I get each result cached for each user (because of auth data included in requests)?
Hey Jeremy Pasco!
Nice to chat with you :). And good questions!
I discovered that the config framework.yaml file had no more session configuration by default.
Hmm. I believe that there IS still cookie config in the default framework.yaml
file: https://github.com/symfony/recipes/blob/0aadfa1169876356288225f3d6028cf07cdde48a/symfony/framework-bundle/5.3/config/packages/framework.yaml#L9-L13
My guess is that you may have started your project with the API Platform distribution? If so, then yes, the cookie config is not there by default :) https://github.com/api-platform/api-platform/blob/main/api/config/packages/framework.yaml
and to put the stateless attribute to false in api_platform.yaml
I think you mean security.yaml (but please tell me if I'm wrong). And yes, you would need to set stateless: false
to get session based authentication to work (or remove stateless
entirely, as it defaults to false. security.yaml does not have a stateless
key at all when you start).
But... I'm wondering what are the consequences of disabling the stateless mode. For example, will it impact the Varnish cache? Will I get each results cached for each user (because of auth data included in reaquests)?
You've probably nailed the only (but potentially big) downside. Varnish does not like cookies :). But let me back up and say that I'm far from a Varnish expert. But I believe that, by default, if a session cookie is sent on a request, then Varnish does not cache that request... or it at least "varies" by that cache value, I can't remember exactly. You certainly can write some vcl to tell Varnish to not do this: to treat /api/products the same regardless of the session cookie, which is what I would do. Of course, you then need to be very careful not to "cheat" and start returning different results from /api/products based on the current user :).
Let me know if that helps a bit!
Cheers!
Thanks for your reply, it helps a lot!
> My guess is that you may have started your project with the API Platform distribution?
I am indeed using the distribution instead of bundle installation.
> I think you mean security.yaml
The stateless attribute I changed was in api_platform.yaml ( https://github.com/api-platform/api-platform/blob/main/api/config/packages/api_platform.yaml ) but you're right, security.yaml allows it too. I guess that the api_platform one is just a way to inject the stateless attribute into the api_platform security config?
> You certainly *can* write some vcl to tell Varnish to not do this
Thanks, it confirms my intuition. The more I work with API Platform, the more I feel that I won't be able to elude VCL much longer :)
At first, I considered going back stateless (with JWT) just to avoid this. But I end up having few other endpoints with custom operations that cannot be fully cached (like /me or other computed entities not related to auth). Sometimes, these endpoints return strange result sets depending on which user asked for it previously. At the end, I feel that handling Varnish properly is not an option.
Hey Jeremy Pasco!
> The stateless attribute I changed was in api_platform.yaml
Ah! I actually didn't know about this option! It looks like it comes from v2.6. This causes the routes defined by your API to be "stateless", which *prohibits* the session from being used - https://github.com/symfony/... - that's a cool feature :).
Anyways, you would, indeed, need to remove stateless in both spots, but they are 2 different things:
A) API Platform "stateless" config - prevents session usage on API Platform routes
B) security.yaml "stateless" - tells the security system not to store (or read) a session cookie that contains authentication information.
> Thanks, it confirms my intuition. The more I work with API Platform, the more I feel that I won't be able to elude VCL much longer :)
Oh caching :). I don't know your situation, but in general, I look for the "low hanging fruit" and cache that stuff :). So, cache endpoints that are heavy, frequently used AND that you know won't contain user-specific data. Then, don't cache the rest. But, it could be that your heaviest endpoints are the ones you need to cache most... so it really depends on your situation :p
Good luck!
Hello everyone! I have this all working out great with the API, is there a way that I tell symfony that when the session expires to redirect the user not to /login but to / or another route as /login is set to only allow POST and it brakes the re-login cycle?
Hi everyone! I'm having an issue with the login on my application. I'm using the same as described on this symfonycast, but it seems like the session cookie is not being saved to the browser. For example, it is not showing up when I inspect the website and go to Application -> Storage -> Cookies. It's not there. It is however in the response headers that I got back from my server. Just wondering why this is not be stored in the storage, as it seems to be when following along with this project.
Hey @Jesse!
Hmm. First, it sounds like you are checking the correct places: you've checked that the response header DOES contain a Set-Cookie header... and you've verified that the cookie is NOT being stored. The only reason I can think of that this would happen is that your browser is rejecting the cookie. The most common reason this might happen is if your frontend and backend are on different domains - do you have a situation like this?
Cheers!
Thanks for the reply! Yes that is correct, we have the backend running on a separate server then the frontend. From reading up a bit more, would the solution be, using SameSite cookie as none and running the server through https? Or is there a better alternative? Had no luck so far trying to get a self signed certificate and vue js setup properly in Windows 10.
Hey @Jesse!
Sorry for the slow reply - I had a family matter come up.
Ah, yea, life gets more complicated when your frontend and backend aren't on the same "host". If they are under the same domain... but just have different subdomains (e.g. frontend.example.com and api.example.com) then that is ok: you can configure your cookies to use the domain example.com and everything will work fine (even with SameSite cookies). But, my guess is that you are not so lucky and you have 2 legitimately different domains. In that case, I believe that session-based authentication (i.e. with cookies) may not be an option for you - and tbh, this isn't an area that I have a lot of practice expertise. But generally speaking, I think you might need to rely on normal API token authentication instead of cookies... or find some workaround - e.g. https://stackoverflow.com/q...
> would the solution be, using SameSite cookie as none and running the server through https
Umm... this might work too :). I suppose you could have your backend set the cookie on its domain... with SameSite set to none.. then it should be sent back on future requests to the backend. Of course, you'd need to consider the security ramifications - specifically protected from CSRF attacks on your API.
Sorry I can't be more clear - my head is still a bit jumbled from being away for a week and this isn't an area that I have had to tackle in the real world.
Cheers!
Hi, i'm using the same configuration as you said api.exemple.com and frontend.exemple.com in the same domain but how
i can configure my cookies to use the main domain : exemple.com ?
Cheers !
Hey Youenn T.
Have you tried this config?
framework:
session:
cookie_domain: .example.org
Cheers!
Now I'm with this config but when I using my /me on production
i got this in my request :
`
PHPSESSID
domain ".exmple.com"
expires "1970-01-01T00:00:01.000Z"
httpOnly true
path "/"
samesite "Lax"
secure true
value "deleted"
PHPSESSID "mo3lub68kgpfjcqi6sior8stje"
`
Everything works fine in localhost with the same domain
I'm not sure I follow you but on production, you have to use set your production domain.
Hi Guys!
I'm trying to apply what I learned here about Api Platform on a new application, but it may be too ambitious for my skills...I have a basic logic problem, and I can't figure out what to do. I know it's not the best place to ask questions like that but...you're nice and competent, so I'll try :)
So I'm trying to create a back-end with Symfony and (obviously) Api Platform. It's hard but I'm making progress. For my front-end I use Vue with Vue-Router. For authentication, I use Lexik JWT Bundle with the Http Only Cookie Mode. So far I've managed to make all that working together : API calls, authorization through the cookie, that's great. A lot of work to do, still.
Only One thing is missing : the user profile! I want my user to click on a "my profile" button and get its infos, so I'm guessing I need to make a get call to "api/users/{id}", where the id is the current user id. But how am I supposed to get its own id ? Am I supposed to decode the token in Vue ? Or use a Symfony controller to identify the user ? But that would be against Api Platform recommendations, and make the JWT authentication useless, and complicate the vue-router ? Or is it something else ? Maybe a data provider ?
I'm really confused about that particular point. Could you please help me and tell me how to do that "api/users/{id}" call ?
Hi Jean-tilapin!
Nice to chat with you again!
For authentication, I use Lexik JWT Bundle with the Http Only Cookie Mode. So far I've managed to make all that working together : API calls, authorization through the cookie, that's great. A lot of work to do, still.
Awesome work! I am personally a fan of, good, old-fashioned cookie/session authentication, but what you need varies case-by-case :).
Only One thing is missing : the user profile! I want my user to click on a "my profile" button and get its infos, so I'm guessing I need to make a get call to "api/users/{id}", where the id is the current user id. But how am I supposed to get its own id
Personally, I would render (in Twig, in your base layout) a global window.currentUserId = {{ app.user.id }}
global variable. Or I might even set a global variable to some actual user information to avoid the need for an AJAX request to get that basic info. I guess you could also store some user info in local storage, but it makes sense to me to communicate some information from the server to the client-side by outputting some global vars (you could also, for example, add something like a data-user="" attribute on the body tag - that's kind of a matter of preference versus global vars.
So... let me know what you think about this and if it works for your situation. It's such a simple solution, that I'd like you to tell me if it doesn't work (and why) before I suggest alternatives :).
Cheers!
For now, I've solved that issue with this technique (I don't use Twig, only Vue with vuex, vue-router and vuetify): https://github.com/api-plat...
It's quite simple but I still have a lot of trouble understanding what is - or not - a "best practice" with Api Platform.
FOSRestBundle seems to be more friendly, but too late, I've made a choice, I have to stick with it.
Now I have a new issue (great!) : configuring the security.yaml to allow users from Lexik JWT authenticator AND users from the admin back-office with traditional login form to both access the API, with different privileges. If you have some time to waste...
Hey @Xav!
Nice job solving the issue!
> It's quite simple but I still have a lot of trouble understanding what is - or not - a "best practice" with Api Platform.
API Platform talks a lot about REST best practices. And so often, there are perfectly fine solutions that are not RESTful, and so it seems (when reading the docs or issues) that this is "not a good solution". It is, it's fine - good job ;).
> Now I have a new issue (great!) : configuring the security.yaml to allow users from Lexik JWT authenticator AND users from the admin back-office with traditional login form to both access the API, with different privileges. If you have some time to waste...
My first question would be: do you have aa single "User" object in your app or 2 User objects (one for the normal JWT authenticated users and another for the admin users)? Hopefully you have one (not that 2 is wrong, it's just more complex). If you *do* have 1, then it's all comes down to:
A) On login, intelligently setting roles based on the JWT or the admin. So, ultimately the authentication is different, but you use different information to decide the roles the user should have.
B) If that is too simple (or for cases that are not this simple), rely heavily on voters - you can do anything in them. Heck, you even have access to the authentication token so you could know in a voter if a user authenticated via JWT or normal login.
Cheers!
Hi! Is it possible to implement "remember me" functionality with json_login? My front and back are living on different subdomains because their development is completely independent. Domains are my.example.com and api.example.com, if it helps. Api is used to fuel SPA and it is somewhat inconvenient to relogin each time session ends. I've found that json_login doesn't support "remember me" and according to <a href="https://github.com/symfony/symfony/issues/29729">this thread</a>, never will(but why support remember_me field then?).
Solutions I've found over internet(see below) are either incomplete or not working, even if it's claimed as working for Symfony4:
https://www.purcellyoon.com/insights/articles/symfony-4-using-the-json-login-with-remember-me
https://stackoverflow.com/questions/53574421/remember-me-does-not-work-with-json-login
It seems guys from FOSUserBundle somehow implemented it: https://github.com/FriendsOfSymfony/FOSUserBundle/issues/747
But I cannot get whole picture and make it work. Documentation about working with TokenBasedRememberMeServices(for example) is very poor(next to non-existent).
Can you shed some light, please, and give me some directions? IDK, maybe I missed some documentation, after all..
Some basic information from bin/console about
:
<blockquote>
Symfony
Version 4.4.2
PHP
Version 7.4.3
</blockquote>
Anton K. - You're selling us a bit short. We have this working in production on multiple sites. Also have it working between different domains and Ionic smartphone clients. How far along are you? Any specific errors? Be sure you're using "withCredentials: true" client-side and you're using some type of CORS Bundle on the Symfony4 side. Cheers!
Hi Anton K.!
Fortunately, you're not the first person to ask this :D. The short answer is that remember_me does not work with json_login. But if you re-implement json_login as a Guard authenticator (which is not too difficult), you *can* make it support that. Check out the thread for answers here - https://symfonycasts.com/sc... - and let me know if you have some luck.
Cheers!
Hi! I have an angular frontend aplication with is hosted on other server, so there is no way to read cookies set to httpOnly: true. As far as i understand CSRF is not available then? Or I could do some configuration in API platform to anable this? Thanks in advance.
Hi @skywaler!
That's a really excellent question - and not something I've had to implement before! So, if your backend is hosted on another server, are you still using session-based authentication - i.e. the other server sets a cookie and your JavaScript sends it to be authenticated? If so, indeed - that complicates things :). First, let me say that this stuff is *tricky*, so I'll do my best to be accurate here - but I can't make any promises.
Because of your setup, the session cookie that's set by the other server doesn't use SameSite... which means that it *is* in theory vulnerable to CSRF attacks. From some looking around, it looks like you could implement CSRF by using headers or by using cookies that only your Angular domain can use - https://security.stackexcha...
The topic in general doesn't really relate to API Platform - it's more of a Symfony security setup. This bundle - https://github.com/dunglas/... - which doesn't work for Symfony 5 (as the author is mostly hoping that SameSite cookies will eliminate the need for CSRF in the long-run), is a good example of how this might all work.
A more abstract answer to your question is this: using session/cookie based authentication is GREAT when your frontend and backend live on the same domain. By using SameSite cookies, you *may* not need to worry about CSRF (most of your users will be using browsers that support SameSite). If you have your frontend and backend on different domains, security is just tricky. You probably don't want to use session-based authentication (using token-based instead), but now you have the problem of storing authentication tokens in JavaScript, which can be dangerous. And this leads to another solution: put a backend on the same domain as your frontend... which makes the API requests for you. This is actually what GitHub does. The JavaScript on GitHub.com does *not* talk to the GitHub API directly, as far as I've seen. Nope, it makes AJAX requests back to GitHub.com and uses session-based authentication. Behind the scenes. the GitHub.com backend probably makes API requests to the GitHub API using API tokens.
I hope this helps - but sorry if it does not ;). I could be wrong about some of these details - but I've spent a lot of time researching them.
Cheers!
Hi, have just come back to this after rebuilding my backend with the latest version of api-platform etc and decided to use session-based authentication instead of JWT as it looks like the cleaner solution for my needs.
BUT - although the modified login stage seems to work ok according to the logs:
[info] User has been authenticated successfully.
[debug] Stored the security token in the session.
It continues to authenticate anonymously when my client subsequently fetches resources from the API:
[info] Populated the TokenStorage with an anonymous Token.
So when my code calls:<br />$user = $this->tokenStorage->getToken()->getUser();<br />
it returns a string: 'anon'
Any idea where I might be going wrong?
Hey Simon D.
I'm not sure how you are doing your API calls but seems to me that you are not sending the session cookie. We explain how in this chapter https://symfonycasts.com/sc...
Cheers!
Actually, I think my problem is that I have separated the front and back-end (as per API-Platform recommendations) so that they're currently on different ports of localhost - my thinking being that in production I can have them on different servers or even different domains.
However, according to https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch:
fetch() won't receive cross-site cookies; you can’t establish a cross site session using fetch. Set-Cookie headers from other sites are silently ignored.
So I have a new question for you and Ryan: Do you still consider session cookies to be an acceptable means of authentication for APIs when the front and back ends are separated like this?
The folks at API-Platform seem pretty adamant that JWT is the only way to go for APIs.
Ohh, I see your problem. I found this article that explains how you can send your credentials via CORS request http://promincproductions.c...
I'm not sure about how secure it is, it's possible that's not the most secure option but it should work. If you don't like it, or you are aware of any vulnerabilities, then, using JWT would be your option
Cheers!
Thanks,
In the end, I compromised by going back to JWT but setting the token in an http-only cookie.
This gives me greater flexibility for the future, seems more secure and has the advantage of being a genuinely RESTful solution.
Hi
Thank you for this great tutorial.
Is it possible to implement the json_login authentication with Mercure knowing that API Platform doc https://api-platform.com/do..., they are using JWT. My main goal is to secure the API and notify users when a new article is created. Thanks
Hey Taieb!
As far as I understand it, absolutely :). The JWT that you configure with MecureBundle will be used to communicate and authenticate against your Mecure hub, not your Symfony app. Basically, when you set up the Mercure Hub, you configure some secret key (not a JWT) on the JWT_KEY
setting. You then use that key to create JWT's that will be used by MecureBundle / your app. Basically, you'll create a JWT that has this structure (https://jwt.io/#debugger-io?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjdXJlIjp7InN1YnNjcmliZSI6WyJmb28iLCJiYXIiXSwicHVibGlzaCI6WyJmb28iXX19.afLx2f2ut3YgNVFStCx95Zm_UND1mZJ69OenXaDuZL8) and then sign it with the same "key" that the Mecure hub is using (see https://github.com/dunglas/mercure/blob/master/docs/getting-started.md#publishing and https://symfony.com/doc/current/mercure.html#configuration)
I hope that helps. The key this is that your Symfony security and this whole "JWT Mecure" thing have nothing to do with each other :). The Mercure thing is all about communicating securely between your Symfony app and the Mercure hub.
Cheers!
Hi
Those who trying to implement the json_login authentication instead of JWT with separate domain Single Page App's (html static page on a separate domain without php). You may get bunch of different CORS warnings and errors during the login process. I have spent several hours to resolve this issue and I believe it may useful to someone...
For those who are struggling with "Log in" button not handling
clicks on it under Canary Google Chrome... Do not panic - it just does not work in Canary :(
Hey Egor,
Thanks for this tip! I see Canary is unstable yet, so probably this issue might be related to this.
Cheers!
Thanks for the tutorials! I have two questions :)
First, it seems like you can still see /api page and run commands in the page without any authentication. How can I make /api page require login?
Second, do you have plan to add JWT authentication in API Platform in this tutorial? I tried the API Platform website, but it only shows how to install and configure. There are no further information to get started. Also Symfony RESTful API: Authentication with JWT (Course 4) doesn't seem much relevant to the API Platform. Thanks for your work!
Hey Sung L.!
Sorry for the slow reply! Let's get to these questions :)
First, it seems like you can still see /api page and run commands in the page without any authentication. How can I make /api page require login?
If you'd like to do this, use access_control
in security.yaml to require, for example, ROLE_USER or ROLE_ADMIN. Basically, you can use normal Symfony security to protect this if you'd like.
Second, do you have plan to add JWT authentication in API Platform in this tutorial?
No :). In a future tutorial, we're going to talk about some different authentication schemes beyond the one we show here. That's on purpose - I think JWT is being over-used - its truly useful if you have a central authentication system and then many different applications where you want an API client to be able to talk to all of these with the same "token" and avoid all of these separate apps from needing to communicate back to the central authentication system on every request. It's a situation that few people actually have. For many use-cases, you should be using HttpOnly cookie-based authentication... which could be a session cookie or JWT. Though, while this is a fine use of JWT, it doesn't really offer any advantages over session-based auth in most situations and is more complex to setup.
That's why we won't talk about it in this tutorial... but I do want to expand on all of this in a future tutorial. Let me know if that helps :).
Cheers!
Hi, I'm very happy about this course.
I'm wondering if there is any recommendation which frontend framevork is best (easiest) to use with the Api platform.
And I wonder if one particular frontend framevork will be used in the following courses and which one?
Hey Sasa,
All popular JS frameworks have both pros and cons, and most of the time the choice between of them falls on one that you know the best :) Really, if know a JS framework and you're comfortable with it - just use it. Well, sometimes it's more complex choice because your choice may depends on your project and its needs. But what about working with API - I think all popular JS frameworks are good in it, that's a pretty simple thing and it's more matter of taste.
Well, if you're interested about our opinion - we love ReactJS, and we have a screencast about it: https://symfonycasts.com/sc... . Also, we're looking at VueJS, so probably the further screencasts might be based on it, but it's not 100% yet :)
I hope this helps!
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
}
}
Thank you again for the excellent screencasts.
I have an issue that I'm really struggling with and I'm hoping you might be able to offer me some pointers. I've successfully implemented JWT authentication as in the API Platform docs: https://api-platform.com/docs/v2.1/core/jwt/#jwt-authentication
However, I want to replace the local username / password authentication with a call to AWS Cognito AND include some of the Cognito response fields in the JWT returned by the API (essentially wrapped inside the JWT payload).
I've added a custom authenticator to the firewall, which successful calls out and authenticates against Cognito and retrieves the Cognito auth and refresh tokens. But I still have the json_login: entry in the firewall definition, so it then calls that authenticator too (against the local DB) and the function issuing the JWT only has access to the data from that authenticator (I can't work out if / how I can pass the Cognito info into the second authenticator).
I've tried removing the json_login: authenticator and adding the "onAuthenticationSuccess" and "onAuthenticationFailure" functions from JsonLoginAuthenticator to the custom authenticator, but I'm getting an
"Unable to find the controller for path \"/authentication_token\". The route is wrongly configured."
error. So I guess I need a Controller, but I'm not sure how to make the controller call the custom authenticator (or if that's the right way to go). json_login seems to handle the route without a controller (though I'm probably just not finding it).It would also be nice if I could pass the options (?) from json_login into the custom authenticator, but I can't work out if that's possible? This bit is definitely not critical.
I've been stepping though code and going around in circles for weeks and I'm not getting anywhere. :-(
Many thanks, Paul