We use cookies on this site to enhance your user experience. By clicking "OK, I Agree" or using our site, you consent to the use of cookies unless you have disabled them.
View our cookie policy to learn more.

When our AJAX call to authenticate is successful, our app naturally sends back a session cookie, which all future AJAX calls will automatically use to become authenticated. So then... if our API response data doesn't need to contain a token... what should it contain?

One option is to return the authenticated User object as JSON. For example... I think one of my users in the database is id 5 - so if you go to /api/users/5.json, we could return this JSON. Or even better we could return the JSON-LD representation of a User.

This has the benefit of being useful: our JavaScript will then know some info about who just logged in. But... if you want to get technical about things... this solution isn't RESTful: it sort of turns our authentication endpoint into what looks like a "user" resource. But, don't let that get in your way: if you do want to return the User object as JSON, you can serialize it manually and return it. I'll show you how to use the serializer to do this in the next chapter.

What if we returned the IRI - /api/users/5 - which is also the URL that a client can use to get more info about that user? Let's try that!

At the bottom of the controller, return a new Response() - the one from HttpFoundation - with no content: literally pass this null. Returning an empty response is totally valid, as long as you use a 204 status code, which means:

The request was successful... but I have nothing to say to you!

So... where are we putting the IRI? On the Location header! That's a semi-standard way for an API to point to a resource. For the IRI string... hmm... how can we generate the URL to /api/users/5? Typically in Symfony, we create the route and then we can generate a URL to that route by using its internal name. But... now, API Platform is creating the routes for us. Is there a way with API Platform to say:

Hey! Can you generate the IRI to this exact object?

Yep! And it's a useful trick to know. Add an argument to your controller with the IriConverterInterface type-hint. Now, set the Location header to $iriConverter->getIriFromItem() - which is one of a few useful methods on this class - and pass $this->getUser().

Back on our browser, refresh the homepage. By the way, you can see that the Vue.js app is reporting that we are not currently authenticated... even though the web debug toolbar says that we are. That's because our backend app & JavaScript aren't working together on page load to share this information. We'll fix that really soon.

When we log in this time... we get a 204 status code! Yes! And the logs contain a big array of headers with... location: "/api/users/6".

This gives our JavaScript everything it needs: we can make a second request to this URL if we want to know info about the user. We're going to do exactly that.

Back in PhpStorm, open up CheeseWhizApp.vue. This is the main Vue file that's responsible for rendering the entire page - you can see the CheeseWhiz header stuff on top. And... further below, it embeds the LoginForm.vue component.

This also holds the logic that prints whether or not we're authenticated... via a user variable. We're not going to get too much into the details of Vue.js, but when we render the LoginForm component, we pass it a callback via the v-on attribute.

This basically means that, inside of LoginForm.vue, once the user is authenticated, we should dispatch an event called user-authenticated. When we do that, Vue will execute this onUserAuthenticated method. That accepts a userUri argument, which we then use to make an AJAX request for that user's data. On success, it updates the user property, which should cause the message on the page to change and say that we're logged in.

Phew! Let me show you what this looks inside LoginForm.vue. Uncomment the last three lines in the callback. This dispatches the user-authenticated event and passes it the user IRI that it needs. The userUri variable doesn't exist, but we know how to get that: response.headers.location. I'll take out my console.log().

My bad! I forgot that all headers are normalized and lowercased. Make sure the location header has a lowercase "L". Refresh the whole page one more time, put in the email, password and... watch the left side. Boom! It says:

You are currently authenticated as quesolover Log out.

At this point we're using session-based authentication, which is the best solution in many cases. And because we're relying on cookies for authentication, our authentication endpoint can really return... whatever is useful! Note that this also avoids the need for the very un-RESTful /me endpoint that some API's like Facebook expose as a, sort of "cheating" way for a client to get information about who you are currently logged in as.

Next - if we refreshed right now, our JavaScript would forget that we're logged in. Silly JavaScript! Let's leverage the serializer to communicate who is logged in from the server to JavaScript on page load.

Leave a comment!

2020-05-20weaverryan

Ah, ha! Good find! I should have thought about the possibility of localhost vs 127.0.0.1 - an easy mistake to make.

Cheers!

2020-05-18Pedro Serra

Hi weaverryan, thanks for your answer! I double checked it and I was using the frontend in http://localhost and the backend in http://127.0.0.1 and it looks like Google Chrome detected it as different domains. Thanks for the link too, a very well written and clarifying article!

Are your backend and frontend on different host names? I believe this is what you're looking for right here: http://www.redotheweb.com/2... - the short answer is that your CORS configuration needs to allow this :). The cookie SHOULD remain HttpOnly: we want our AJAX calls to send this cookie, but we do not want our AJAX calls to be able to read it directly.

First of all, many thanks for your help! Now, after adding the 'Location' header to the 'expose_headers' array in the nelmio_cors config, I send the request to the "/login" endpoint and it works fine, if I inspect the response I can see that I receive the "Location" header with the user's URI and "Set-Cookie" header with the session cookie, but the problem is that the "Set-Cookie" header is HttpOnly and it can't be read on the frontend. Have you faced the same problem? How can it be solved?

> so with the me endpoint, it means that the browser would have to send that request everytime the user refreshes the page

Yep!

> Would it make sense instead to store a cookie on the frontend with the user model and then delete it when user calls the logout endpoint? Is there some sort of security concerns this way?

You do need to be a bit careful here... but as long as you don't rely on the cookie (or local storage) as *proof* that the user is authenticated AND you don't store anything sensitive there, you're probably ok. You could also use that locally "cached" data for initial page load and then STILL send a request to /me in the background to refresh the data. That would give you the data instantly but it would also stay up-to-date.

Another option here is to do something like this - https://symfonycasts.com/sc... - where you dump the user information on page load and read that in JavaScript. That doesn't / may not work for a single-page app, but is an easy method otherwise.

Cheers!

2019-12-08Gabriel Caruana

Quick question, so with the me endpoint, it means that the browser would have to send that request everytime the user refreshes the page. Would it make sense instead to store a cookie on the frontend with the user model and then delete it when user calls the logout endpoint? Is there some sort of security concerns this way?

Basically instead of calling the /me endpoint on refresh I just get the user model from cookie if it s there. Then I have an request interceptor that deletes the cookie anytime an endpoint returns 401.

Thanks for sharing your experience! I was surprised you needed cookie_domain: 'symfony.localhost'... since your backend is running on symfony.localhost already... but I'm just taking a guess here. Mostly, what you did makes total sense :).

Cheers!

2019-12-05Gabb

For all those having CORS issues:

So I have the symfony application running on a docker container with domain `symfony.localhost`.Then I have the angular dev application served to `angular.symfony.localhost`. Edit your hosts file and add `127.0.0.1 angular.symfony.localhost`

Then run the angular serve with the following command `ng serve --host="angular.symfony.localhost"`

This still doesnt do anything, you need to install the NelmioCorsBundle on the symfony application. Inside nelmio_cors.yaml under `expose_headers` add Location to the array, to be able to access the Location Header from the angular app.

Then in framework.yaml under session, add cookie_domain: 'symfony.localhost'.

Lastly make sure that when you call the `me` endpoint or any other endpoint that you need to send the cookie, set `withCredentials: true` inside the request, in my case on angular:

Thanks a lot Sung! I've spend more than an hour stucked on this problem, looking all over the internet to find a solution... Only then I had the idea to look in the comments of the tutorial I was reading (!). What's funnier is I had already installed NelmioCorsBundle... so I just had to add the 'Location' header to the 'expose_headers' array. So simple, yet I would not have thought about it! Thanks again, this made my day.

And thank you a lot to whomever wrote these tutorials on ApiPlatform/Security, they're really helpful and so simple to follow!

2019-09-13Victor Bocharsky

Hey Sung,

Oh, CORS... thanks for sharing this info with others!

Cheers!

2019-09-12Sung Lee

Thanks for your help, Victor!I figured out how to get headers.location value in my case. As it is CORS request, my local Symfony runs on https://127.0.0.1:8000/ and my ReactJS app runs on http://localhost:3000, I need to install NelmioCorsBundle to expose Location header. Hope this helps if anyone trying to access Location info from a separate app.

Cheers! :)

2019-09-11Victor Bocharsky

Hey Sung,

Do you mean you manually set the "location" header to the response in Symfony application but do not see it in the "console.log(response.headers)". Hm, what if you do "console.log(response)"? Do you see location value somewhere? Well, it's weird... but if you see it in the Chtome Network tab, most probably you have an issue with axios, try to ask this question to axios maybe? Maybe they handle location header a bit different, like setting it to a special property, not sure.

I hope this helps!

Cheers!

2019-09-06Sung Lee

I noticed that in the Network tab, location value is set in Response Header. However, it is not available from axios console.log(response.headers).

2019-09-06Sung Lee

I followed the Symfony side, but I used React.js (Next.js specifically) and Axios for the frontend part. When I do console.log(response.headers), the result doesn't have location information. It only has the following values: