The Special IS_AUTHENTICATED_ Strings

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

If we simply need to figure out whether or not the user is currently logged in, we check for ROLE_USER:

<!DOCTYPE html>
<html>
... lines 3 - 14
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light px-1">
<div class="container-fluid">
... lines 18 - 26
<div class="collapse navbar-collapse" id="navbar-collapsable">
... lines 28 - 38
{% if is_granted('ROLE_USER') %}
... lines 40 - 43
{% endif %}
</div>
</div>
</nav>
... lines 48 - 52
</body>
</html>

This works.... just because of how our app is built: it works because in getRoles(), we make sure that every logged in user at least has this role:

... lines 1 - 12
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 15 - 81
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
... lines 90 - 154
}

Checking if Logged In: IS_AUTHENTICATED_FULLY

Cool. But it does make me wonder: is there a more "official" way in Symfony to check if a user is logged in? It turns out, there is! Check for is_granted('IS_AUTHENTICATED_FULLY'):

<!DOCTYPE html>
<html>
... lines 3 - 14
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light px-1">
<div class="container-fluid">
... lines 18 - 26
<div class="collapse navbar-collapse" id="navbar-collapsable">
... lines 28 - 38
{% if is_granted('IS_AUTHENTICATED_FULLY') %}
... lines 40 - 43
{% endif %}
</div>
</div>
</nav>
... lines 48 - 52
</body>
</html>

By the way, anything we pass to is_granted() in Twig - like ROLE_USER or IS_AUTHENTICATED_FULLY - we can also pass to the isGranted() method in the controller, or denyAccessUnlessGranted()... or to access_control. They all call the security system in the same way.

I bet you noticed that IS_AUTHENTICATED_FULLY does not start with ROLE_. Yup! Roles must start with ROLE_... but this string is not a role: it's handled by an entirely different system: a part of the security system that simply returns true or false based on whether or not the user is logged in.

So, in practice, this should have the same effect as ROLE_USER. When we refresh... yup! No change.

Access Decision Log in the Profiler

Oh, but click the security link in the web debug toolbar to jump into the profiler. Scroll down to the bottom to find something called the "Access decision log". This is super cool: Symfony keeps track of all the times that the authorization system was called during the request and what the result was.

For example, this first check was for ROLE_ADMIN, which is probably coming from access_control: because we went to /admin, this rule matched and it checked for ROLE_ADMIN. The next check is again for ROLE_ADMIN - that's probably to show the admin link in Twig - and then there's the check for IS_AUTHENTICATED_FULLY to show the log in or log out link. Access was granted for all three of these.

Remember Me Authed: IS_AUTHENTICATED_REMEMBER

In addition to IS_AUTHENTICATED_FULLY, there are a couple of other special strings that you can pass into the security system. The first is IS_AUTHENTICATED_REMEMBERED, which is super powerful... but can be a bit confusing.

Here's how it works. If I am logged in, then I always have IS_AUTHENTICATED_REMEMBERED. That... so far should sound identical to IS_AUTHENTICATED_FULLY. But, there's one key difference. Suppose I log in, close my browser, open my browser, and refresh... so that I'm logged in thanks to a remember me cookie. In this situation, I will have IS_AUTHENTICATED_REMEMBERED but I will not have IS_AUTHENTICATED_FULLY. Yup, you only have IS_AUTHENTICATED_FULLY if you logged in during this browser session.

We can see this. Head over to your browser, open your debugging tools, go to Application and then Cookies. Oh... my remember me cookie is gone! This... was a mistake I made. Log out... then go to security.yaml.

Earlier, we switched from using our custom LoginFormAuthenticator to form_login. That system totally works with remember me cookies. But we also removed the checkbox from our login form. And, inside of our authenticator, we were relying on calling enable() on the RemmeberMeBadge to force the cookie to be set:

... lines 1 - 26
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator
{
... lines 29 - 39
public function authenticate(Request $request): PassportInterface
{
... lines 42 - 44
return new Passport(
... lines 46 - 56
[
... lines 58 - 61
(new RememberMeBadge())->enable(),
]
);
}
... lines 66 - 81
}

Whelp, the core form_login authenticator definitely adds the RememberMeBadge, which advertises that it opts into the "remember me" system. But it does not call enable() on it. This means that we either need to add a checkbox to the form... or, in security.yaml, add always_remember_me: true:

security:
... lines 2 - 16
firewalls:
... lines 18 - 20
main:
... lines 22 - 37
remember_me:
... lines 39 - 40
always_remember_me: true
... lines 42 - 54

Let's log back in now: abraca_admin@example.com, password tada and... got it! There's my REMEMBERME cookie.

Ok: because we just logged in - so we "logged in during this session", we are "authenticated fully". But, if I closed my browser, which I'll imitate by deleting the session cookie - and refresh... we do stay logged in, but we are now logged in thanks to the remember me cookie. You can see that via the RememberMeToken.

And look up here! We have the "Log in" and "Sign up" links! Yup, we are now not IS_AUTHENTICATED_FULLY because we did not authenticate during this session.

This is a long way of saying that if you use remember me cookies, then most of the time you should use IS_AUTHENTICATED_REMEMBERED when you simply want to know whether or not the user is logged in:

<!DOCTYPE html>
<html>
... lines 3 - 14
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light px-1">
<div class="container-fluid">
... lines 18 - 26
<div class="collapse navbar-collapse" id="navbar-collapsable">
... lines 28 - 38
{% if is_granted('IS_AUTHENTICATED_REMEMBERED') %}
... lines 40 - 43
{% endif %}
</div>
</div>
</nav>
... lines 48 - 52
</body>
</html>

And then, if there are a couple of parts of your site that are more sensitive - like maybe the "change password" page - then protect those with IS_AUTHENTICATED_FULLY. If the user tries to access this page and only has IS_AUTHENTICATED_REMEMBERED, Symfony will actually execute your entry point. In other words, it will redirect them to the login form.

Refresh the page and... yes! The correct links are back.

PUBLIC_ACCESS & access_control

Ok, there are a few other strings special similar to IS_AUTHENTICATED_REMEMBERED, but only one more that I think is useful. It's called PUBLIC_ACCESS... and it returns true 100% of time. Yup, everyone has PUBLIC_ACCESS, even if you're not logged in.

So... you might be thinking: how could that ever possibly be useful? Fair question!

Look again at access_control in security.yaml. To access any URL that starts with /admin, you need ROLE_ADMIN:

security:
... lines 2 - 50
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
... lines 53 - 54

But pretend that we had a login page at the URL /admin/login.

Let's actually create a dummy controller for this. Down at the bottom of AdminController, add public function adminLogin()... with a route - /admin/login - and, inside, return a new Response() with:

Pretend admin login page that should be public

... lines 1 - 10
class AdminController extends AbstractController
{
... lines 13 - 57
/**
* @Route("/admin/login")
*/
public function adminLogin()
{
return new Response('Pretend admin login page, that should be public');
}
}

Log out... and go to /admin/login. Access denied! We're redirected to /login. And really, if /admin/login were our login page, then we would get redirected to /admin/login... which would redirect us to /admin/login... which would redirect us to /admin/login... which would... well you get the idea: we would get stuck in a redirect loop. Yikes!

In security.yaml, we want to be able to require ROLE_ADMIN for all URLs starting with /admin... except for /admin/login. The key to do that is PUBLIC_ACCESS

Copy the access control and paste above. Remember: only one access_control matches per request and it matches from top to bottom. So we can add a new rule matching anything starting with /admin/login and have it require PUBLIC_ACCESS... which will always return true!

security:
... lines 2 - 50
access_control:
- { path: ^/admin/login, roles: PUBLIC_ACCESS }
- { path: ^/admin, roles: ROLE_ADMIN }
... lines 54 - 55

Thanks to this, if we go to anything that starts with /admin/login, it will match only this one access_control... and access will be granted!

Try it: go to /admin/login and... it loads!

Next: we've talked about roles and we've talked about denying access in various different ways. So let's turn to the User object: how we can ask Symfony who is logged in.

Leave a comment!

This tutorial also works great for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4.1 || ^8.0.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.3", // v3.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^2.1", // 2.4.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
        "doctrine/orm": "^2.7", // 2.10.1
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.1
        "pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
        "pagerfanta/twig": "^3.3", // v3.3.0
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "scheb/2fa-bundle": "^5.12", // v5.12.1
        "scheb/2fa-qr-code": "^5.12", // v5.12.1
        "scheb/2fa-totp": "^5.12", // v5.12.1
        "sensio/framework-extra-bundle": "^6.0", // v6.2.0
        "stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
        "symfony/asset": "5.3.*", // v5.3.4
        "symfony/console": "5.3.*", // v5.3.7
        "symfony/dotenv": "5.3.*", // v5.3.8
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/form": "5.3.*", // v5.3.8
        "symfony/framework-bundle": "5.3.*", // v5.3.8
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/property-access": "5.3.*", // v5.3.8
        "symfony/property-info": "5.3.*", // v5.3.8
        "symfony/rate-limiter": "5.3.*", // v5.3.4
        "symfony/runtime": "5.3.*", // v5.3.4
        "symfony/security-bundle": "5.3.*", // v5.3.8
        "symfony/serializer": "5.3.*", // v5.3.8
        "symfony/stopwatch": "5.3.*", // v5.3.4
        "symfony/twig-bundle": "5.3.*", // v5.3.4
        "symfony/ux-chartjs": "^1.3", // v1.3.0
        "symfony/validator": "5.3.*", // v5.3.8
        "symfony/webpack-encore-bundle": "^1.7", // v1.12.0
        "symfony/yaml": "5.3.*", // v5.3.6
        "symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.3
        "twig/string-extra": "^3.3", // v3.3.3
        "twig/twig": "^2.12|^3.0" // v3.3.3
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.3.*", // v5.3.4
        "symfony/maker-bundle": "^1.15", // v1.34.0
        "symfony/var-dumper": "5.3.*", // v5.3.8
        "symfony/web-profiler-bundle": "5.3.*", // v5.3.8
        "zenstruck/foundry": "^1.1" // v1.13.3
    }
}