Symfony2: logging out
Tagged with: [ PHP ] [ security ] [ symfony2 ]
One of the “golden rules” of symfony2 is to never hardcode urls or paths inside your code or templates. And letting symfony deal with the generation of your urls and paths makes your life a lot easier as a developer. But one of the things I see regularly is that people are still hardcoding their logout urls like using “/logout”. But logging out is actually a bit more complex than it might seem, and using a simple /logout might work for most cases, but there are better ways to deal with this.
A little background information about symfony2 Security component (and bundle)
A fact that most people will know, is that you can have multiple “secured” areas inside a single project. For instance, you can have secure area for your normal registered users, which might be located at /secure. And maybe you have a separate administration area for the site owners located at /admin. And maybe you have a dedicated API that allows for other kind of users located at /api. It’s even possible to define a “secured area” that does not need any security at all. This is something you see for instance with the web development toolbar. However, its quite possible to move this all into one big secure area, and have different ways of figuring out who can access what. Although the more complex your application will become, the more benefit having multiple areas can give you.
Every different secured area is called a firewall and will take care of authenticating users. Each firewall is completely separate from others: if you are authenticated by one firewall, it does not mean you are automatically authenticated by other firewalls, and there will only be one firewall currently active (the one that matches your current URL). And it make sense that each firewall is different, as they can authenticate a completely different userbase and even different ways on where these users are stored (for instance, your API firewall could authenticate through oauth tokens, while your normal /secure firewall could have a login screen where it authenticates against users inside your database).
This also means that every firewall would have different ways to log out, and for some of them even, logging out does not even apply. Let’s take a look at an example of a security.yml file:
This is the “firewall” section of a security.yml. Notice that I define three different firewalls: dev, *superadminstuff** and main. The “dev” firewall does not use any authentication (security=false), meaning everybody is allowed in. (which are the web debug toolbar, plus the /js, /css and /img directories). It’s just there to make sure that paths like /js, aren’t covered by the “main” firewall.
The next firewall is one where we protect the /admin pages. Notice that we use http_basic as our main form of authentication, meaning that our browser will pop-up a dialog where we need to fill in our name and password. (not very secure though, as the username and password will be send over as pretty much as plain text). The browser will send the username and password on each request you make to the application. Symfony2 can authenticate this information based on the provider which is “memory_user_provider”, a section of the security.yml that I haven’t showed, but basically defines a few standard users with some passwords directly in the configuration instead of a database.
Notice that there is no real way to log out of a http-basic authenticated firewall: the only way to “logout” is to have the browser stop sending your name and password on every request. Clearing your browser cache, restarting your browser usually helps, and some web developer tools might be helpful here.
The last firewall is called “main” and protects everything else (as the pattern is “^/”, meaning everything). Instead of using http basic, it uses a form_login method for authenticating users. In our case we are using the FOSUserBundle, which already has got some login forms and handling so you don’t have to write your own (you only need to customize it a bit).
Now what happens, is that when we hit a page inside our firewall and we haven’t authenticated ourselves yet, that Symfony2 will automatically redirect us to a login page. It will find this page by inspecting the login_path key in the form_login section of our firewall. If not specified, this will actually resolve to /login, but you are free to change this to any other url or even a route you like. Once you have logged in, symfony2 will store the authenticated user and roles inside its session so you will be automatically authenticated once you hit the firewall again.
Logging out of a firewall is quite easy too: the only thing we need to do is enter the “logout” page of the firewall we want to logout from. But which page is this?
In our security.yml example, you see that we have a “logout: true” defined. Also notice that this “logout” is NOT underneat the form_login section, but directly underneath the firewall name.
By specifying logout: true, it tells symfony2 to use the default logout settings, which actually defaults to a whole lot more:
logout: csrf_parameter: _csrf_token csrf_token_generator: ~ csrf_token_id: logout path: /logout target: / success_handler: ~ invalidate_session: true delete_cookies: name: path: null domain: null handlers: []
As you can see, we can specify the path of where our logout actually resides. Like with the login_path, this can also contain a relative URL (it must start with a /), or a route. But something strange will happen here: by default, a logout listener will be triggered before the actual controller/action is called, and will perform the logout and redirects you to the given target. If you have your own custom handlers (as defined by the handlers in your logout configuration), and your handlers will NOT return a HTTP response object, the actual route is called. So by default your controller/action will not be called, BUT have to be present (the symfony2 router must be able to resolve this page). This is also the reason why you find a strange logout action in the FOSUserBundle that triggers an exception, since it will never be actually called.
Logging out
So, how do we deal with our logouts? It make sense not to use the hardcoded URL. Even if you use a route instead of a url, you might modify the route inside the security.xml configuration, and your logout functionality will stop working. What you really want, is have twig use the url or (resolved) route that is configured in your security.xml. Fortunately, the securityBundle has a custom twig extension that can help you out here: the logout_url() and logout_path() functions. These functions expect a firewall id (like “main”, “dev”, or “superadminstuff” in our cases), and it will generate the correct URL for them.
This will fetch the real logout path from the given firewall, and as a bonus also add a csrf-token to it in case you have configured it. Instead of your twig knowing the actual logout url, we now only need to know which firewall we are actually using.
However, this still assumes that your twig template knows more that in should: Afterall, you have to specify the key manually. Most of the time this is ok, but sometimes you don’t (especially if you are reusing a twig-based menu for instance). However, it’s possible to fetch name of the current firewall, albeit in a strange manner:
Inside the security context token, there is a way to fetch the provider key, which is the name of the firewall you have defined. However, a problem with this is that the app.security global twig variable is deprecated from Symfony 2.6 and will be removed in Symfony 3.0. I’m sure that by that time, other (maybe even better) ways are available of generating the logout paths in a correct way.
Hope this helps!