Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Logout & Passing API Data to JS on Page Load

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Hey! We can log in and our JavaScript even knows when we log in, and who we are: it prints our username & a log out link that goes to /logout... which doesn't actually work yet... cause we haven't enabled that in Symfony.

Adding Logout

Wait... but what does "logging out" even mean in an API context? Whelp, like everything, it depends on how you authenticate. Because we're using a session cookie, logging out basically means removing the user information from the session. If you were using some sort of API token, it would mean invalidating that token on your authentication server - like, removing it from some database table for tokens... again, it depends on your setup. We'll talk more about that type of authentication a bit later - on a special security part 2 of this tutorial.

Anyways, no surprise that Symfony has built-in support for logging the user out of the session. In config/packages/security.yaml, under our firewall add logout: then, below, say path: app_logout. Just like app_login, this is the name of a route that we're going to create next. When a user accesses this route, they'll be logged out.

security:
... lines 2 - 12
firewalls:
... lines 14 - 16
main:
... lines 18 - 24
logout:
path: app_logout
... lines 27 - 39

To create that, open src/Controller/SecurityController.php and add public function logout() with @Route() above. Set the URL to /logout and name it app_logout.

Just like with the app_login route, the route just... needs to exist... otherwise the user will see a 404 when they go to /logout. As long as it does exist, when the user goes to /logout, the logout mechanism will intercept the request, remove the user from the session, then redirect them to the homepage... which is configurable.

This means that, unless we've messed something up, the controller will never be reached. Let's scream in case it somehow is executed: Throw an exception with:

should not be reached

... lines 1 - 11
class SecurityController extends AbstractController
{
... lines 14 - 29
/**
* @Route("/logout", name="app_logout")
*/
public function logout()
{
throw new \Exception('should not be reached');
}
}

Let's try the flow: move over, hit log out and... before it loads, you can see that we're currently logged in. And now... gone! We are anonymous.

Passing Data to JavaScript on Page Load

Before we keep going with all this API & security goodness, our app has a bug. If we log in... as soon as the AJAX call finishes, we've made our Vue.js frontend smart enough to update and say that we're logged in. But when we refresh, that's gone! Gasp! Our web debug toolbar knows we're logged in... but our JavaScript does not. And... it makes sense: when we first load the page... if you look at the HTML source - you can ignore all the web debug toolbar stuff down here... the entire application looks like this. There's no HTML or JavaScript data that hints to Vue that we're authenticated.

How can we fix that? There are basically two options. First, as soon as the page loads, we could make an AJAX request to some endpoint and say:

Hey! Who am I logged in as? Cause... I forgot?

To make that happen, we would need some sort of a /me endpoint: something that would return information about who we are. A useful, but not-so-RESTful endpoint.

The other option is quite nice: send the user data from your HTML into Vue on page load.

Open up the template for this page: templates/frontend/homepage.html.twig. Yep, nothing here but some small HTML to bootstrap the Vue app... though I am doing one interesting thing: I'm passing a prop to Vue called entrypoint. I'm not using this anywhere... but it's a cool example: entrypoint is the URL to our documentation "homepage". In theory, we could use that dynamic URL in our Vue app to figure out what other URLs we could call... we would use our API like a browser: surfing through links. Anyways, this shows a nice way to pass simple data into Vue.

And.. we could pass the current user's IRI as another prop. Inside Vue, we would then make an AJAX call to that URL to get the user data... so, no need for the /me endpoint. That's really the simplest option, though it does have a minor downside: there will be a slight delay on page load before our app knows who's logged in.

Serializing Data Directly to JavaScript

To avoid that AJAX call, we can dump that data directly into Vue. Check it out: start in src/Controller/FrontendController.php. This is the controller behind the homepage.html.twig template.. and it's not super impressive. Add a SerializerInterface $serializer argument... and then pass a new variable to the template called user. I want this to be the JSON-LD version of our user - the exact same thing we would get from making an AJAX request. Set this to $serializer->serialize() passing it $this->getUser() and jsonld for the format. If the user is not logged in, the new user variable will be null... but if they are logged in, we'll get our big, nice JSON-LD structure.

... lines 1 - 6
use Symfony\Component\Serializer\SerializerInterface;
... lines 8 - 11
class FrontendController extends AbstractController
{
... lines 14 - 16
public function homepage(SerializerInterface $serializer)
{
return $this->render('frontend/homepage.html.twig', [
'user' => $serializer->serialize($this->getUser(), 'jsonld')
]);
}
}

Now that we have this, create a script tag and set the data on a global variable... how about: window.user = {{ user|raw }}.

... lines 1 - 2
{% block body %}
<script>
window.user = {{ user|raw }};
</script>
... lines 7 - 12
{% endblock %}

Hey! Our user data is accessible to JavaScript!

Head over to CheeseWhizApp to use it. Generally speaking, I try not to use or reference global variables from my JavaScript. As a compromise, I like to only reference global variables from my top level component. If a deeper component needs it, I'll pass it down as a prop.

Create a new mounted() function - Vue will automatically call this after the component is "mounted" to the page - and if window.user, so, if it's not null, then this.user = window.user.

... lines 1 - 41
<script>
... lines 43 - 45
export default {
... lines 47 - 62
mounted() {
if (window.user) {
this.user = window.user;
}
}
}
</script>
... lines 70 - 79

It's that simple! Sneak over and refresh your browser. And... our JavaScript instantly knows we're logged in. If we log out... yep! Our app doesn't explode. Woo! Yea... this is kinda fun!

More Complex Authentication

Ok! Authentication... including logging out and the frontend is done! If you're feeling great about this approach for you app, awesome! But if you're screaming:

Ryan! My app is more complex... I have multiple APIs that talk to each other... or... my API will be exposed publicly... or I have some other reason that prevents me from this simple session-based authentication!

Then... don't worry. I've already gotten so many questions & feedback from the start of this tutorial that we're planning to create a separate, part 2 of this security tutorial where we'll talk about other common API use-cases and the right way to authenticate within those. We'll also talk about OAuth.

But in general, if you need a more custom authentication system - perhaps you can't use json_login because your login is more complex than just handling an email & password... or you're already planning some sort of token-based authentication - you can build that system by creating a custom Guard authenticator, which is something we talk about in our security tutorial.

Next, before we dive deep into denying and granting access to our API for different users, we need to talk about CSRF attacks and SameSite cookies. It turns out, if you use cookie-based authentication like we are, you may be vulnerable to CSRF attacks. Fortunately, there's a new, beautiful way to mitigate that.

Leave a comment!

60
Login or Register to join the conversation
Default user avatar
Default user avatar Chloé | posted 3 years ago | edited

Hi,

There's something I can't seem to understand... all of this json_login/logout thing works kind of magically to me. Everything works fine until "Passing Data to JavaScript on Page Load". The thing is, I wanted to train myself to make a front-end app completely independent from a Symfony Project, so I've developed my Vue.js on one side, and my API on another. This is why, of course, my Vue.js app doesn't have access at all to the web debug bar of Symfony, and I don't have either a twig template. And I really don't know how to tell my app that my user is authenticated... I've read there answer to Sung Lee, regarding the fact that the session cookie needs to be send on every AJAX request, but actually I don't understand how to get this session cookie, where it is stored... and thus, how to tell my Vue app that my user is authenticated (after page reload, because the first time it's okay, my app displays well that the user is authenticated).

Thanks a lot for your help!

2 Reply

Hey Chloé!

> There's something I can't seem to understand... all of this json_login/logout thing works kind of magically to me.

Ah yes :). This is the great thing... and terrible thing about the security system: it's very difficult to really understand how it works - I 100% agree... it just seems like magic!

> And I really don't know how to tell my app that my user is authenticated... I've read Diego Aguiar answer to Sung Lee, regarding the fact that the session cookie needs to be send on every AJAX request, but actually I don't understand how to get this session cookie, where it is stored... and thus, how to tell my Vue app that my user is authenticated (after page reload, because the first time it's okay, my app displays well that the user is authenticated).

Ok, let me see if I can help! There are many ways to authenticate (as we talk about in this tutorial), but yes, a session cookie is the way that I'd recommend - and it's not important that your Vue.js app and backend live in different codebases (but hopefully they *do* live on the same domain). When you make an authentication request into your backend (e.g. POST /login - the endpoint that uses json_login), use your network tools to check out what the response looks like. You *should* see a Set-Cookie header on the *response*, which is telling your browser to set (probably) a PHPSESSID cookie to some long string value. This is your backend communicating the session cookie back to your browser. By doing *nothing* else, when your Vue.js app sends AJAX requests, it should automatically send this session cookie and your requests should be authenticated (there are a few exceptions to this statement - like the fetch() function in JavaScript needs to be "told" explicitly" to "send credentials", which is a fancy way of telling it to "send the cookies" on the AJAX request - so it's possible you'll need to activate this, depending on which AJAX library you're using).

So, step 1 is to make sure this is happening: to make sure that, after you authenticate, that you can make AJAX requests to the backend and Symfony sees those requests as authenticated (because the session cookie is being set).

That still leaves one problem: when you refresh the page, how does your Vue.js app know that you're logged in? In a true SPA where you basically have a static index.html page can't cant use the trick we're using in this app, you have one, simple option: on page load, make an AJAX request immediately to determine if you're authenticated and to fetch some information about *who* you are. To do this, you will basically need some endpoint like /me, which would return either a 401 if not authenticated or it would return the serialized User information if you *are* authenticated. This isn't a very RESTful endpoint (not the end of the world), so I would probably just build this as a custom route/controller in Symfony, and not through API Platform. Use the serializer directly to serialize the User object into the jsonld format.

Let me know if this helps - excellent question!

Cheers!

1 Reply
Default user avatar

Hi Ryan!

Thanks a lot for your interesting answer! I was looking forward to testing it, but I was doing this project at work to learn a bit more how Symfony & Vue.js can work together (I'm following a work/study training program), and I'm at school for the moment and until next week and I've just tested cloning the project on my personal computer and I don't have the authorization since I'm not connected to the company network.
So I think I'll do it from scratch on my personal computer, or I'll wait until I'm back at work.
Either way, I'm going to copy your answer in some doc on my computer to go back to it easily! Even if I haven't applied it yet to my code, it helped me to see clearer in the process of the cookie-session authentication.
And about my two base codes... actually I think they're not on the same domain. I've used Docker to configure their environnement and each base code is on a different port of my virtual machine. Could it be a problem for the authentication? Is it recommended to have both base codes on the same port?

1 Reply

Hey Chloé!

> Either way, I'm going to copy your answer in some doc on my computer to go back to it easily! Even if I haven't applied it yet to my code, it helped me to see clearer in the process of the cookie-session authentication.

Awesome :). Let me know how it goes!

> And about my two base codes... actually I think they're not on the same domain. I've used Docker to configure their environnement and each base code is on a different port of my virtual machine. Could it be a problem for the authentication? Is it recommended to have both base codes on the same port?

The reason I was asking about this is "CORS" security. The "cookie" shouldn't be a problem. What I mean is, suppose you authenticate against your backend at http://localhost:8010. This will cause a cookie to be returned on the response and each subsequent request to the backend should include that cookie... even if your frontend is being served at http://localhost:8020. Basically, everything should work ok. The thing that might cause an issue is CORS - your JavaScript may refuse to even make an AJAX request to http://localhost:8010 (from http://localhost:8020) due to CORS. If that's the case, you'll just need to modify your backend Symfony app to set some CORS headers to allow this. Actually, API Platform comes with NelmioCorsBundle pre-installed, which allows you to add this config: https://github.com/nelmio/N...

Cheers!

1 Reply
Default user avatar
Default user avatar Chloé | weaverryan | posted 3 years ago | edited

Hey weaverryan !
Two weeks later I was finally able to try your solution ! And it works !!! Thank you so much, I'm so satisfied to finally be able to authenticate with a Symfony API.
And well noted for the CORS thing. Indeed it caused problem ; I thought it wouldn't because Nelmio bundle was already installed, but I forgot a parameter. Now everything's working ! Thanks again for having taken the time to answer my question, it helped me a lot.

1 Reply
Sebastian K. Avatar
Sebastian K. Avatar Sebastian K. | Chloé | posted 2 years ago

I have the same problem. What parameter did you forgot?

Reply

Hey Sebastian K.!

Exactly which problem are you getting? Are you having CORS problems? I don't exactly know what Chloé did, but it's very possible that it was necessary to configure the CORS_ALLOW_ORIGIN variable in .env. This is used in config/packages/nelmio_cors.yaml to control the "allowed origin"s.

Cheers!

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

Hey weaverryan ,

my Problem: I make a successful POST to /login, get Set-Cookie: PHPSESSID=e42d08bd1eeb1f1f51277927a81bdf75; path=/; domain=local.dev; secure; HttpOnly; SameSite=none but when I want to get the logged in User with /currentUser no additional header is sent and no logged in User is returned.

In my Application-Tab in the Chrome Dev Tools no cookie was created after login.

Here is a quick description of my setup:

Docker with 3 Container, VueJS (Port 80 to 80), Symfony 5 (80 to 8000) and MariaDB. Additional entry in /etc/hosts: local.dev, so local.dev is my Vue and local.dev:8000 is my API. No Reverse Proxy

I tried on Axios:
withCredentials: true in my Request,

In Symfony
allow_credentials: true in my CORS Config
'CORS_ALLOW_ORIGIN' => '*', in my .env.local.php

and


cookie_secure: true
cookie_samesite: none
cookie_domain: 'local.dev'

in my framework.yaml

Nothing works. $this->getUser(); is always null

And I (think I) tried everything from the comments of the 2 previous chapters. Doesn't seem like I am the only one

Reply
Default user avatar

Hey Sebastian!

I've easily retrieved the commit I posted around the same time as the comment, and it seems the parameter I had forgotten was the allow_credentials, which you have already. If it can be of any help, here's my config :

nelmio_cors:
defaults:
origin_regex: true
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
allow_headers: ['Content-Type', 'Authorization']
expose_headers: ['Link', 'Location']
allow_credentials: true
max_age: 3600
paths:
'^/': null


As Ryan suggests it may have also been the variable in .env (added in the same commit), mine was more "precise" : CORS_ALLOW_ORIGIN=^https?:\/\/my.domain.fr(:[0-9]+)?$ I think I remember that for some thing, using the star was too... "permissive" or something, and thus wasn't authorised, but I may remember wrong. 🤔

Good luck!

Reply

Hey Chloé,

Thank you for sharing your solution with others!

Cheers!

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

So, now it works. My mistake was to use the withCredentials: true-config only in my GET currentUser-request, but I have to set it with every request.

So either axios.defaults.withCredentials = true, or, if you use Axios with NuxtJS, with a Plugin (Described here)

The allow_credentials must also be set, otherwise you will get this CORS-error

Access to XMLHttpRequest at 'http://local.dev:8000/curre... from origin 'http://local.dev' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
Reply

Hey Sebastian K.!

Thanks for following up with the answer! I totally overlooked that fact that Axios (which *normally* sends cookies automatically) does not send cookies automatically when making a "cross-site" request - you need withCredentials: true to force that. And then, on the other side, your CORS headers (via the allow_credentials: true in nelmio_cors.yaml is needed to allow that to happen. Your reply helped clear all of this up in my mind as well :).

Thanks!

Reply

Hey Sebastian,

Glad to hear you got it working! And thank you for sharing your solution with others!

Cheers!

Reply

Hey @Chloé!

Ah, wonderful! Thanks for the update and the good news! Keep up the good work :).

Cheers!

Reply
Bálint Avatar

Hi weaverryan! I am planning to the a similar thing that Chloé did, but with also an option for Facebook OAuth using
KnpUOAuth2ClientBundle. So my question, is it possible to build a Symfony API that uses Facebook OAuth and session based authentication that can be used similarly with a separate domain Vue.js application?

Reply

Hi Bálint !

This is a tricky question :). Let's see:

> is it possible to build a Symfony API that uses Facebook OAuth and session based authentication

You have 2 options for this. Use KnpUOAuth2ClientBundle and use the traditional "authorization code flow" to authenticate the user. This would involve redirecting them to Facebook. The "pro" is that this would look no different than a "traditional web app" that authenticates with Facebook. The "con" is that it's not a true SPA, as you redirect the user aruond.

If you want to keep a true SPA, then you won't use KnpUOAuth2ClientBundle and you'll instead use the Facebook JS SDK to do the "client credentials" flow. Once you have the access token (in JavaScript), you would send an AJAX call - e.g. POST /authenticate/token - to your server, which would include the access token. Your server (via a custom Guard authenticator) would read that access token, validate it (by using it to make an API call to Facebook to get user info) and authenticate the user.

Here is some info about the different OAuth options based on your situation: https://auth0.com/docs/auth...

> that can be used similarly with a separate domain Vue.js application?

If you want to use session-based authentication (regardless of *how* you authenticate - login form, OAuth, etc), then you will be using a session cookie and you *should* (which is the default in Symfony) use a SameSite cookie. This means that your Vue app *must* be under the same root domain as your API. If it's not, then you won't be able to use session-based authentication.

Let me know if that helps!

Cheers!

Reply
Bálint Avatar
Bálint Avatar Bálint | weaverryan | posted 2 years ago | edited

Thank you for your detailer and quick answer, I really appreciate it. :) I have the impression based on the conversation of yours with weaverryan that this cookie based session authentication can work somehow on separate domains, so they finally could carry out with a email&pass login system to make it work. Is the oauth is different in some manner?

Reply

Hi Bálint!

There are only two ways that you can do cookie-based authentication across 2 different domains:

A) The domains aren't actually 2 different domains - they are sub-domains of the same top-level domain
B) Your allow your cookies to use SameSite: Lax. This option opens your site up to CSRF attacks, unless you implement CSRF protection on your API endpoints.

By the way, if your frontend and API live in different domains, then there are actually 2 parts to getting communication to work:

1) First, you need to allow AJAX requests in general. This has nothing to do with authentication or cookies: this is just "is YourJavaScriptFrontend.com allowed to make requests to YourJavaScriptBackend.com"). This is determined via CORS headers (see NelmioCorsBundle, which comes pre-installed with API Platform). Your API backend will need to allow your JavaScript frontend to make AJAX requests to it via CORS headers if they are not under the same domain.

2) Once you can make AJAX requests, there is the question of "how do I authenticate?". And this is where you have options like session cookies, API tokens, etc.

> so they finally could carry out with a email&pass login system to make it work.

Even if you *cannot* make use of session-based authentication, you *can* still do an email/pass login system. There are probably 2 main ways

A) The JavaScript frontend is "owned" by you (you are building it, it's not a 3rd party site that needs to communicate with you). If this is the case, you would first (no matter how you authentication) need to enable AJAX calls via CORS headers. Once you have, you could build a login form on your frontend that, on submit, sends an AJAX request to your API. Since we're assuming you cannot use session cookies, your API would process that request and return some sort of token. That token could be a JWT token or something simpler - see https://jolicode.com/blog/w... - but either way, they get back this token. Then, on every future request, that token is attached to AJAX requests to your API and *that* is used to authenticate your user.

B) If the JavaScript frontend (or mobile app) is not owned by you, then you don't want to allow the above setup, because it means that *your* users would be entering their email/pass directly into forms on 3rd party sites... which is not cool... because those 3rd party sites could - in theory - intercept the email/pass before sending it and save it somewhere. *This* is actually the purpose of OAuth, which is a much more complex thing to implement. OAuth is most appropriate when you need to allow 3rd party sites/apps to "perform actions on behalf of your users" without your users needing to enter their email/pass for your site into that 3rd party site/app. Think: Facebook login. But ultimately, once the OAuth flow is done, your JavaScript ends up with the same thing as in part (A): a token that is attached to all future requests to authenticate them.

On a high level, all (most) authentication is "token" based authentication :). Session-based authentication is where an Http only cookie is sent on every request. Hey, that's really a token! Or, in a more "traditional API authentication scheme", the token is sent as a header on your request. But in all cases, your users type their email/pass once time, and are given a token that temporarily allows them to make authenticated requests as that user.

Btw, if you have any choice, you're making your life a bit more difficult by having your API and frontend on different domains. If you *must* do this, I might even make AJAX calls from my frontend to my frontend server and have IT communicate to the backend API behind the scenes. But... there are 1000 valid setups and different requirements :p.

Cheers!

Reply
Youenn T. Avatar

Thanks your conversation helped me a lot ! :)

Reply
J-D Avatar

Hi everyone,

Following the course, I have been wondering about the behaviour following the authentication expiration and couldn't find anything.

I expect that at some point the authentication will expire and any axios request would receive an error as response. My first guest would be to intercept those responses and unset the user variable, triggering the display of the logging page again. Is that covered somewhere on SymfonyCasts?

edit: nevermind, found my solution: I used axios interceptor https://axios-http.com/docs... and tested the error code on response.

Reply
weaverryan Avatar weaverryan | SFCASTS | J-D | posted 1 year ago | edited

Hey J-D!

Sorry for the slow reply :). It's not really covered anywhere here... because it entirely depends on what your frontend is built in. But, it looks like you've got your solution anyways! The key, as you found is to (somehow) have a hook into ALL Ajax requests, looking for the "you got logged out" situation. Then, you redirect to the login page... or pop up a login modal (if you're using a frontend framework) or whatever is appropriate.

For others, we talked about how to handle this with Turbo Frames just a few days ago https://symfonycasts.com/sc...

Cheers!

Reply
J-D Avatar
J-D Avatar J-D | weaverryan | posted 1 year ago | edited

Hi weaverryan ,

No worries and thank you for the reply, I will check out that new video.

Agreed, it depends on the front end. Part of the reason I shared ours is to get a reaction on my solution in case I overlooked anything. ;)

Have a great day!

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

Hi!

While I'm re-writing the whole security system of my app to get rid of Lexik Bundle, I might have a problem with "'user' => $serializer->serialize($this->getUser(), 'jsonld')". If I "console.log" window.user, it appears that it includes ALL the fields of my User entity (including the password), and not only what I have configured to get with an API call like "/api/users/22".

Have I missed something? Did I misconfigured something in my User Entity? Is there a way to get from the public controller what API Platform is delivering from a classic Ajax call?

Reply

Hey Jean-tilapin

I believe you're using "groups" to control which fields should be exposed by the serialized. If that's the case, then, when working directly with the Serializer, you need to specify which groups should be allowed. You can find more information here https://symfonycasts.com/sc...

Cheers!

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

Thank you Diego!

Reply
Lorenzo M. Avatar
Lorenzo M. Avatar Lorenzo M. | posted 1 year ago

Hi there, and thanks for the super tutorials series !

Btw, does anybody knows if there's been a change with symfony 5.2 + api platform 2.6.2 ?
Because even though I *did* add the logout path in security.yaml and added the route inside the SecurityController (witch works fine for /login route), /logout route still returns a 404 (but the route strangely appears on bin/console debug:router).
I've done a composer require apache-pack, so this is not *apparently* linked to that :P

Reply

Hey Lorenzo,

Please, try to clear the cache first, does it help? If not, could you run:

$ bin/console debug:router | grep logout

And share the output with us. Because it's strange, if the route is there - it should work :)

Cheers!

1 Reply
Lorenzo M. Avatar
Lorenzo M. Avatar Lorenzo M. | victor | posted 1 year ago

Hey Victor,

yup I tried with clearing the cache, but nope still 404 :P

here's the debug:router | grep logout as you asked for :

app_logout ANY ANY ANY /logout

Symfony seems to be aware of the route however insomnia gives a 404 when I do a GET request to /logout.

Reply
Lorenzo M. Avatar

I tried with adding another custom route inside SecurityController that just says 'hello' and that worked, thus I think it is linked to security's logout. Probably due to the fact I wanted to try latest version of everything (+PHP8).

Reply

Hey Lorenzo,

Btw, if you downloaded the course code for this tutorial - keep in mind that it is not ready for PHP 8 yet. But in case you install a fresh Symfony version in order to follow this course - it should work, but as you figured out - there might be some pitfalls.

Cheers!

1 Reply
Lorenzo M. Avatar

Ok, I solved it,

Just in case, if you are using PHP8, for the logout route, continue using annotations instead of new attributes (#[Route(...)]), turn out it is recognized by symfony router *but* not by security for the moment :)

Take care, guys

Cheers!

Reply

Hey Lorenzo,

Ah, good catch! It would be tricky to guess this without seeing your code, so well done! And thanks for sharing your solution with others :)

Cheers!

1 Reply
Lorenzo M. Avatar

Oh an another work-around I find a bit more attractive, is to keep #[Route] annotation and add a simple `target` entry inside `security.yaml` just as mentioned here : https://symfony.com/doc/cur... (commented in the example)

It will allow you to create a custom "logged-out successfully" route with ease :P

Reply

Thanks for sharing it Lorenzo. I think that's a matter of taste :)

Reply
hanen Avatar

Hi everyone , I ve symfony 5.1project my question is how to avoid the redirect and get a json response after logout and
could you give me an example of using the logoutEvent on a listener or a subscriber? ..

Reply

Hey hanen!

I'm glad you mentioned Symfony 5.1 :). You're right that starting in that version, you can customize the logout behavior via a new LogoutEvent. Here is a blog post about it: https://symfony.com/blog/ne...

That blog actually shows an event listener... which is fine, but event subscribers are easier. I don't have an example of a LogoutEvent subscriber, but here is what an event subscriber looks like - https://symfonycasts.com/sc... - just listen to LogoutEvent - this class https://github.com/symfony/... - instead of RequestEvent.

Ultimately, in your method, you could do something like:


$event->setResponse(new JsonResponse(['authenticated' => false']));

... of whatever you actually want to return.

Let me know if that helps!

Cheers!

1 Reply
hanen Avatar

Hi Ryan,
Thanks for Symfony support, I resolve my problem after reading the below article, everything became clear.
https://symfonycasts.com/sc...
the Symfony login form will redirect the user to the login form /login..
I'm doing it in accordance with documentation:
https://symfony.com/doc/cur...
it might help ;)

Reply

Hello, I have a little doubt ... I have almost finished the course and I had to resume this video because I thought I had done something wrong ..., my question is the following. I have passed the user through the twig template and in effect I have been able to access the user who is currently logged in and also my React Frontend noticed the user logged in but in the javascript I am receiving all the user's data (password and roles fields included but the password is set to null, I thing is the plain password field...) and the browser has suggested that I should change the password , (haha, obviously) ... my question is, shouldn't this user who is being serialized hide the password and password fields? I do not have these fields exposed ... following the tutorial I have created the AdminContextBuilder, UserDataPersister, UserNormalizer and my User entity does not expose these fields in the API ... so I think it should hide those fields or this user bieng pass through the template is not being serialized correctly....am I doing something wrong here? because it seems to me that this should not be exposed..., you probably misunderstood the serialization and deserialization process :(... Greetings and thanks in advance

Reply

Hey Toshiro!

Ah, I agree! Those fields should *not* be exposed!

Are you exposing the user data the *exact* same way we do in this chapter? By calling $serializer->serialize($user, 'jsonld') in your controller passing this into Twig? Or are you doing something slightly different?

I just triple-checked my application - I re-tried this step and checked the HTML to see the result. It IS what I expected:


window.user = {"@context":"\/api\/contexts\/User","@id":"\/api\/users\/1","@type":"User","phoneNumber":null,"isMe":true};

Do you see @context in your result? It seems like your User object is being serialized almost outside/without API Platform. What happens if you serialize (in the same way) another @ApiResource object. Do you get the correct fields? Or also the wrong fields?

Cheers!

Reply

Hey weaverryan thanks for the replay!!... well answering to your questions...

1-) Are you exposing the user data the *exact* same way we do in this chapter?

yes I'm!! this is my HomeController:


namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\SerializerInterface;

class HomeController extends AbstractController
{
/**
* @Route("/{reactRouting}", name="app_home", requirements={"reactRouting"="^(?!api).+"}, defaults={"reactRouting": null})
*/
public function index(SerializerInterface $serializer)
{
return $this->render('home/index.html.twig', [
'controller_name' => 'HomeController',
'user'=> $serializer->serialize($this->getUser(), 'jsonld')
]);
}
}

I will check the others questions at home... and I will be back with the answers tomorrow!!. Thanks!!... anyway I will keep trying to solve the problem I'm probably missing something there haha...

EDIT:
Hey! @weaverryan I'm back again!! Answering the other questions @weaverryan...

Do you see @context in your result?

.. Yes I do... this is how window.user variable looks like...

window.user = {
@context: "/api/contexts/User"
@id: "/api/users/1"
@type: "http://schema.org/Person"
address: "string"
email: "marlonalgo@algo.com"
empresa: null
id: 1
isMe: true
password: null
persona: null
roles: ["ROLE_USER"]
salt: null
telephone: "46554"
tipoUsuario: "/api/tipo_usuarios/1"
turnos: []
username: "marlon"
__proto__: Object
}

and the other question

What happens if you serialize (in the same way) another @ApiResource object. Do you get the correct fields? Or also the wrong fields?

... well I did this in the HomeController just for test

public function __construct(TiposServiciosRepository $tiposServiciosRepository)
{
$this->tiposServiciosRepository = $tiposServiciosRepository;
}

/**
* @Route("/{reactRouting}", name="app_home", requirements={"reactRouting"="^(?!api).+"}, defaults={"reactRouting": null})
*/
public function index(SerializerInterface $serializer)
{
return $this->render('home/index.html.twig', [
'controller_name' => 'HomeController',
'user'=> $serializer->serialize($this->tiposServiciosRepository->findAll(), 'jsonld')
]);
}

and I get all my *tipoServicios* like this...:


(4) [{…}, {…}, {…}, {…}]
0:
@context: "/api/contexts/TiposServicios"
@id: "/api/tipos_servicios/1"
@type: "http://schema.org/Service"
descripcion: "Esto es una pequeña descripcion de lo que hace un servicio"
id: 1
image: null
nombre: "Reparación"
servicios: []
__proto__: Object
1: {@context: "/api/contexts/TiposServicios", @id: "/api/tipos_servicios/2", @type: "http://schema.org/Service", image: null, id: 2, …}
2: {@context: "/api/contexts/TiposServicios", @id: "/api/tipos_servicios/3", @type: "http://schema.org/Service", image: null, id: 3, …}
3: {@context: "/api/contexts/TiposServicios", @id: "/api/tipos_servicios/4", @type: "http://schema.org/Service", image: null, id: 4, …}
length: 4
__proto__: Array(0)
and indeed those were the fields I expected to see...Was this what you wanted me to try??

Reply

Hey! Toshiro I already solved the problem! :) ... digging into the documentation I discovered that you can pass a third argument to the serialize function in which you can define which group or groups you want to use when serializing the object! as explained HERE so the final result in mi HomeController look like this! :


/**
* @Route("/{reactRouting}", name="app_home", requirements={"reactRouting"="^(?!api).+"}, defaults={"reactRouting": null})
*/
public function index(SerializerInterface $serializer)
{
return $this->render('home/index.html.twig', [
'controller_name' => 'HomeController',
'user'=> $serializer->serialize($this->getUser(), 'jsonld',['groups' => 'user:read'])
]);
}

That solved the problem and now all my sensitive fields are hidden again!!! Thanks for your help!! Your questions gave me a clue where I could find out the solution!!!... Regards!!!

Reply

Hey Toshiro!

Woohoo! Nice work!

So, the only mystery is why you needed to do this but I didn't. Mostly, it makes sense: you're not inside an API "context" (this isn't an API Platform endpoint), so API Platform wouldn't add those groups for you. However, when looking at this exact issue before, I found that API Platform actually *does* still "hook in" and add the normalization groups... but I can't remember *where* that happens in the core code...

Anyways, your solution is the more explicit one: tell the serializer exactly what context & groups it should use :).

Cheers!

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

Hi!

Is there a way to customize the logout and change its behavior? I want it to just remove the user from the session and not redirect them.

Thanks!

Reply

Hey Mohammed!

Absolutely! And this was made easier in Symfony 5.1 - check this out - https://symfony.com/blog/ne...

If you create an event subscriber (which is what I prefer), then you won't need any services.yaml config. This event object has a setResponse() method - so you could use that to return an empty response or some JSON.

Cheers!

1 Reply
Mohammed Avatar

That solved my problem!

Thank you!

Reply
akincer Avatar
akincer Avatar akincer | posted 2 years ago

Going through this on latest symfony and aside from having to use ManagerRegistry in the repositories everything has worked exactly as the course describes until the logout function is hit and I get this:

The controller must return a "Symfony\Component\HttpFoundation\Response" object but it returned null. Did you forget to add a return statement somewhere in your controller?

Has something changed in Symfony 5 in how the logout operation is handled?

Reply

Hey Aaron,

Hm, it shouldn't. Please, look the stack trace closer, it should give you a hint from where Controller (on which line) there's no return statement that would return a Response. Probably you just forgot to return a response somewhere?

I hope this helps!

Cheers!

Reply
akincer Avatar

I should have known. A typo. As soon as I think there's something wrong I figure out that yes there is and it's not Symfony. It's me.

Reply

Hey Aaron,

Yeah, Symfony does its best to give you good hints about the error :) Glad you found the problem!

Cheers!

Reply
Ian Avatar

Hello,
First of all... thank you so much for the GREAT content. Your tutorials are clear and complete. And the support in the comments is great too!!

So... I am using an Angular front-end and did not like the logout system presented here for two reasons. 1/ it does not send anything back on a successful logout, (or unsuccessful I guess) and 2/ the automatic redirect was playing havoc with my Angular app. So I implemented my own logout using the LogoutSuccessHandlerInterface as described in the Symfony docs. From there I can send back a 200 response with some json ("ok": "User successfully logged out"), and from there my Angular app can handle the flow gracefully from there.
BUT... how can I handle a request to /logout if the user is not logged in? Having the server respond with something like status 418, ("message": "don't be silly, how can you log out if you are not logged in?") ?? Should I care?

Of course I can hide the link to /logout from the frontend but it seems strange that I can still enter the URL in a browser and not get an error back in this case.

Thanks !!

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