Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

IS_AUTHENTICATED_ & Protecting All URLs

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

I mentioned earlier that there are two ways to check whether or not the user is simply logged in. The first is by checking ROLE_USER:

<!doctype html>
<html lang="en">
... lines 3 - 15
<body>
<nav class="navbar navbar-expand-lg navbar-dark navbar-bg mb-5">
... lines 18 - 21
<div class="collapse navbar-collapse" id="navbarNavDropdown">
... lines 23 - 34
<ul class="navbar-nav ml-auto">
{% if is_granted('ROLE_USER') %}
... lines 37 - 52
{% endif %}
</ul>
</div>
</nav>
... lines 57 - 74
</body>
</html>

I like this one because it's simple. It works because of how our getRoles() method is written:

... lines 1 - 10
class User implements UserInterface
{
... lines 13 - 66
/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
... lines 78 - 128
}

The only reason I'm even going to mention the second way is because I want you to know what it is if you see it, and, it leads us towards a few other interesting things.

IS_AUTHENTICATED_FULLY

Let's play a little bit in security.yaml. Under access_control add a new entry with path ^/account. Yes, this will be a totally redundant access control because we're already requiring ROLE_USER from inside the controller:

... lines 1 - 8
/**
* @IsGranted("ROLE_USER")
*/
class AccountController extends AbstractController
{
... lines 14 - 22
}

Just pretend that we don't have this controller code for a minute.

On your access_control, if you wanted to require the user to be logged in, you could use roles: ROLE_USER or IS_AUTHENTICATED_FULLY:

security:
... lines 2 - 40
access_control:
- { path: ^/account, roles: IS_AUTHENTICATED_FULLY }
... lines 43 - 45

OoooOOOoo.

Well, it's not really that fancy: it's just a special string that simply checks if the user is logged in or not. In our system, it's 100% identical to ROLE_USER.

Move over, go back to /account and... yep! Access is still granted.

Web Debug Toolbar & Access Control Checks

Oh, and I want to show you something cool! Click the little security icon on the web debug toolbar. This has some pretty sweet stuff in it. In addition to saying who you're logged in as and your roles, it also has a table down here with some lower-level info. But what I really want to show you is all the way at the bottom. Yes! The access decision log. This records every time that we checked whether or not the user had access to something on this page. The first check is for IS_AUTHENTICATED_FULLY from access_control. Granted! Then, two ROLE_USER checks and one ROLE_ADMIN check.

One of those ROLE_USER checks is from AccountController:

... lines 1 - 8
/**
* @IsGranted("ROLE_USER")
*/
class AccountController extends AbstractController
{
... lines 14 - 22
}

And the other comes from is_granted() in the template. The ROLE_ADMIN check also lives here:

<!doctype html>
<html lang="en">
... lines 3 - 15
<body>
<nav class="navbar navbar-expand-lg navbar-dark navbar-bg mb-5">
... lines 18 - 21
<div class="collapse navbar-collapse" id="navbarNavDropdown">
... lines 23 - 34
<ul class="navbar-nav ml-auto">
{% if is_granted('ROLE_USER') %}
<li class="nav-item dropdown" style="margin-right: 75px;">
... lines 38 - 40
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
... line 42
{% if is_granted('ROLE_ADMIN') %}
... line 44
{% endif %}
... line 46
</div>
</li>
... lines 49 - 52
{% endif %}
</ul>
</div>
</nav>
... lines 57 - 74
</body>
</html>

So, this is just a nice way to debug all the security checks happening on your page.

Requiring Login on Every Page

Anyways, we now know IS_AUTHENTICATED_FULLY is a way to check if the user is logged in. Though... because of the way our app is written, checking ROLE_USER does the same thing and... it's shorter to write.

But! This does touch on another interesting topic. This is a news site, so most of the pages will be accessible to anonymous users. We'll require login on just the pages that need it. Not all sites are like this, however. On some sites, you want to do the opposite: you want to require authentication for every page, or at least, almost every page. In those cases, a better strategy is to require login on all pages and then allow anonymous access on just a few pages.

We can do this by being clever with access_control. Try this: change the path to just ^/:

security:
... lines 2 - 40
access_control:
# if you wanted to force EVERY URL to be protected
- { path: ^/, roles: IS_AUTHENTICATED_FULLY }
... lines 44 - 46

Because this is a regular expression, it will match every URL and so every page now requires login.

If we refresh, we still have access. But now, log out!

Allowing the Login Page: IS_AUTHENTICATED_ANONYMOUSLY

Whoa! The page is broken! Like, crazy broken! localhost redirected too many times!? Yep, our security system is too awesome. Because we're now anonymous, when we try to access any page, we're redirected to /login. But guess what? /login requires authentication too! So what does Symfony do? It redirects us to /login!

We made security so tight that anonymous users can't even get to the login page! Here's the fix: add a new access_control - above the one for all URLs with path: ^/login. You can add a $ on the end to match only this URL exactly, not also /login/foo. Your call. For roles, use a second special string: IS_AUTHENTICATED_ANONYMOUSLY:

security:
... lines 2 - 40
access_control:
# but, definitely allow /login to be accessible anonymously
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
# if you wanted to force EVERY URL to be protected
- { path: ^/, roles: IS_AUTHENTICATED_FULLY }
... lines 46 - 48

This one is weird. Who has IS_AUTHENTICATED_ANONYMOUSLY? Everyone! If you're anonymous, you have it. If you're logged in, you have it too! So, why would we ever want to use a role that everyone has? Well, go refresh.

Because it fixes our problem! Remember: Symfony goes down each access_control one-by-one. As soon as it finds one that matches, it uses that one and stops. So when we go to /login, only the first access control is used and access is granted. Every other page will still require login. Booya!

IS_AUTHENTICATED_REMEMBERED

We've now learned two special "strings" that can be used in place of the normal roles: IS_AUTHENTICATED_FULLY and IS_AUTHENTICATED_ANONYMOUSLY. But, there is one more. Change "fully" to IS_AUTHENTICATED_REMEMBERED:

security:
... lines 2 - 40
access_control:
# but, definitely allow /login to be accessible anonymously
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
# if you wanted to force EVERY URL to be protected
- { path: ^/, roles: IS_AUTHENTICATED_REMEMBERED }
... lines 46 - 48

Go back to your site and log in. Because we just logged in, we have all three special strings: IS_AUTHENTICATED_FULLY, IS_AUTHENTICATED_REMEMBERED and, of course, IS_AUTHENTICATED_ANONYMOUSLY.

But now, imagine that you're using the "remember me" functionality. You close your browser, re-open it, and are still authenticated, but only thanks to the remember me cookie. Now, you would still have IS_AUTHENTICATED_REMEMBERED, but you would not have IS_AUTHENTICATED_FULLY. Fully means that you have authenticated during this session.

This allows you to do something really neat. If you use the remember me functionality you should protect all pages that require login with IS_AUTHENTICATED_REMEMBERED. This says that you don't care whether the user just logged in during this session or if they are logged in via the remember me cookie. Then you can protect more sensitive pages - like the change password page - with IS_AUTHENTICATED_FULLY:

security:
... lines 2 - 40
access_control:
# but, definitely allow /login to be accessible anonymously
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
# require the user to fully login to change password
- { path: ^/change-password, roles: IS_AUTHENTICATED_FULLY }
# if you wanted to force EVERY URL to be protected
- { path: ^/, roles: IS_AUTHENTICATED_REMEMBERED }
... lines 48 - 50

If a user tries to access that page, but is only authenticated with the remember me cookie, Symfony will redirect them to the login page so that they can become "fully" authenticated. Nice, right?

By the way, I'm showing you all of these examples for the IS_AUTHENTICATED strings inside access_control. But, you absolutely can use these in your controller or inside Twig.

Ok, because our site will be mostly public, I'll comment-out these examples:

security:
... lines 2 - 40
access_control:
# but, definitely allow /login to be accessible anonymously
#- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
# require the user to fully login to change password
#- { path: ^/change-password, roles: IS_AUTHENTICATED_FULLY }
# if you wanted to force EVERY URL to be protected
#- { path: ^/, roles: IS_AUTHENTICATED_REMEMBERED }
... lines 48 - 51

Next, let's learn how to find out who is logged in by fetching their User object.

Leave a comment!

6
Login or Register to join the conversation
Matteo S. Avatar
Matteo S. Avatar Matteo S. | posted 2 years ago

Why is checking for role ROLE_USER equivalent to checking for IS_AUTHENTICATED_FULLY and not IS_AUTHENTICATED_REMEMBERED?
I would expect the latter

Reply

Hey Matteo S.

Yeah in some cases probably you are right, however IS_AUTHENTICATED_... constants are system attributes, and ROLE_.. are object roles, so you may not have roles but you can be authenticated. Return back to your question, if I got correct place related to your question Ryan said in that moment using ROLE_USER and IS_AUTHENTICATED_FULLY is an equivalent check, and that is true, and in that place we are not speaking about REMEMBERED attribute, because it will be introduced later.

However I agree that it may confuse a little while watching video!

Cheers!

Reply

How to customize the form returned by "IS_AUTHENTICATED_FULLY"? I would like to make it to require just the password [twice] not the email. Thanks

Reply

Hey Alex,

If you're talking about Login form - it lives in templates/security/login.html.twig Twig template.

Cheers!

1 Reply

Yes, but is there any way to create another form that is returned by "IS_AUTHENTICATED_FULLY"? Thanks

Reply

Hey Alexandru!

Interesting question! When a user only has IS_AUTHENTICATED_REMEMBERED, and they try to access a page that requires IS_AUTHENTICATED_FULLY, then they will be redirected to the login page - e.g. /login. In that controller (because you are 100% in control of this controller & template), I would check to see if the user has "IS_AUTHENTICATED_REMEMBERED". If they do, pass a flag into Twig that says this and then render the form differently. Or, perhaps better, in your controller, render an entirely different template for those users :).

Now, the only trick is that you will also need to update your authenticator to be able to handle the new form. I've never done this before, but it shouldn't be too hard. Basically, inject the Security class into your authenticator so that you can check if the user has IS_AUTHENTICATED_REMEMBERED. If they do, then you know that they are submitting the "authenticated" version of the form. And you can change the logic in your authenticator based on this - e.g. get the User object via $this->security->getUser() instead of reading a submitting email field and querying the database.

Let me know if that helps!

Cheers!

1 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.0
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.1.4
        "symfony/console": "^4.0", // v4.1.4
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/framework-bundle": "^4.0", // v4.1.4
        "symfony/lts": "^4@dev", // dev-master
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.1.4
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/twig-bundle": "^4.0", // v4.1.4
        "symfony/web-server-bundle": "^4.0", // v4.1.4
        "symfony/yaml": "^4.0", // v4.1.4
        "twig/extensions": "^1.5" // v1.5.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.1.4
        "symfony/dotenv": "^4.0", // v4.1.4
        "symfony/maker-bundle": "^1.0", // v1.7.0
        "symfony/monolog-bundle": "^3.0", // v3.3.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.4
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.1.4
    }
}
userVoice