Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Login with json_login

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.

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:
... lines 2 - 12
firewalls:
... lines 14 - 16
main:
... lines 18 - 19
json_login:
check_path: app_login
... 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:
... lines 2 - 12
firewalls:
... lines 14 - 16
main:
... lines 18 - 19
json_login:
... line 21
username_path: email
password_path: password
... 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.

... lines 1 - 4
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
... line 6
use Symfony\Component\Routing\Annotation\Route;
... line 8
class SecurityController extends AbstractController
{
/**
* @Route("/login", name="app_login", methods={"POST"})
*/
public function login()
{
... 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.

... lines 1 - 13
public function login()
{
return $this->json([
'user' => $this->getUser() ? $this->getUser()->getId() : null]
);
}
... 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.

... lines 1 - 23
<script>
... 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;
})
... lines 58 - 60
</script>
... 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.

Leave a comment!

44
Login or Register to join the conversation
Paul-L Avatar
Paul-L Avatar Paul-L | posted 10 months ago | edited

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.

    check_path: /authentication_token
    username_path: email
    password_path: password
    success_handler: lexik_jwt_authentication.handler.authentication_success
    failure_handler: lexik_jwt_authentication.handler.authentication_failure

I've been stepping though code and going around in circles for weeks and I'm not getting anywhere. :-(

Many thanks, Paul

Reply

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!

1 Reply
Paul-L Avatar
Paul-L Avatar Paul-L | weaverryan | posted 10 months ago | edited

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

Reply

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 :).

Reply
Leonel D. Avatar
Leonel D. Avatar Leonel D. | posted 1 year ago

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.

Reply

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!

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

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.

Reply

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!

Reply
Default user avatar
Default user avatar Jeremy Pasco | posted 1 year ago

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)?

Reply

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!

Reply
Default user avatar
Default user avatar Jeremy Pasco | weaverryan | posted 1 year ago

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.

Reply

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!

1 Reply
JavierMendezK Avatar
JavierMendezK Avatar JavierMendezK | posted 2 years ago

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?

Reply
Joshua G. Avatar
Joshua G. Avatar Joshua G. | posted 2 years ago

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.

Reply

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!

Reply
Joshua G. Avatar

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.

Reply

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!

Reply
Youenn T. Avatar

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 !

Reply

Hey Youenn T.

Have you tried this config?


framework:
  session:
    cookie_domain: .example.org

Cheers!

1 Reply
Youenn T. Avatar
Youenn T. Avatar Youenn T. | MolloKhan | posted 2 years ago | edited

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

Reply

I'm not sure I follow you but on production, you have to use set your production domain.

Reply
Jean-tilapin Avatar
Jean-tilapin Avatar Jean-tilapin | posted 2 years ago

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 ?

Reply

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!

Reply
Jean-tilapin Avatar

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...

Reply

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!

Reply
Anton K. Avatar
Anton K. Avatar Anton K. | posted 3 years ago | edited

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&quot;&gt;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>

Reply
Default user avatar
Default user avatar Patrick Purcell | Anton K. | posted 3 years ago | edited

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!

Reply

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!

Reply
Viktoriia K. Avatar
Viktoriia K. Avatar Viktoriia K. | posted 3 years ago

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.

Reply

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!

1 Reply
Simon D. Avatar
Simon D. Avatar Simon D. | posted 3 years ago | edited

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?

Reply

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!

Reply
Simon D. Avatar

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.

Reply

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!

Reply
Simon D. Avatar

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.

1 Reply
Taieb Avatar

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

Reply

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!

Reply
Mohamed Avatar
Mohamed Avatar Mohamed | posted 3 years ago

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...

https://gist.github.com/zsp...

Reply
erop Avatar

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 :(

Reply

Hey Egor,

Thanks for this tip! I see Canary is unstable yet, so probably this issue might be related to this.

Cheers!

Reply
Sung L. Avatar
Sung L. Avatar Sung L. | posted 4 years ago

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!

Reply

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!

Reply

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?

Reply

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!

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
    }
}
userVoice