09/02/2026

is_granted in Twig: roles and IS_AUTHENTICATED

The three kinds of thing is_granted accepts

  • A role

    ROLE_ADMIN and friends, subject to role_hierarchy - which is why your admin passes a ROLE_USER check.

  • An authentication state

    IS_AUTHENTICATED_FULLY, IS_AUTHENTICATED, IS_REMEMBERED, IS_IMPERSONATOR. Not roles, and grantable to nobody.

  • A voter, about one object

    The second argument. The only form that can answer "may this person edit THIS post", because the answer depends on the post.

is_granted() looks like a role check because that is how most people first meet it. It is not. It asks the authorization system a question, and roles are only one of the things it can be asked.

{% if is_granted('ROLE_ADMIN') %}          {# a role #}
{% if is_granted('IS_AUTHENTICATED_FULLY') %}  {# an authentication state #}
{% if is_granted('EDIT', post) %}          {# a voter, about one object #}

Getting these confused does not usually produce an error. It produces a check that passes when it should not, which is worse.

The authentication states

These are not roles and cannot be granted to anybody. They describe how the current visitor arrived.

IS_AUTHENTICATED_FULLY means they signed in during this session, by entering credentials. Use it for anything that should not be reachable by somebody who merely walked up to an unlocked laptop.

IS_AUTHENTICATED means signed in by any means, remember-me included. It is the honest replacement for is_granted('ROLE_USER'), which people reach for when they mean "is anyone logged in" and which only works because almost every user happens to hold that role.

IS_REMEMBERED is true only when the session came from a remember-me cookie and not from a password. It is the one to use for the "not you? sign in properly" prompt.

IS_IMPERSONATOR is true while an administrator is viewing the site as another user. If your application supports impersonation, a banner guarded by this is the difference between a support tool and an incident.

Why your admin passes a ROLE_USER check

Role hierarchy is configured, not inherited from anything in the code, and it surprises people the first time:

# config/packages/security.yaml
security:
    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

With that in place, a user whose record holds only ROLE_ADMIN passes is_granted('ROLE_USER'). That is the point of the feature, and it is also why a template that hides something from administrators by checking for ROLE_USER hides nothing at all.

The hierarchy is one-directional. ROLE_USER does not gain anything from ROLE_ADMIN existing, so a check for the more specific role stays specific.

Voters, for anything about a particular thing

A role answers "what is this person allowed to do". It cannot answer "may this person edit this post", because the answer depends on the post. That is a voter, and it is the second argument:

{% if is_granted('EDIT', post) %}
    <a href="{{ path('post_edit', {id: post.id}) }}">Edit</a>
{% endif %}

The attribute is a string you choose, not a framework constant. EDIT, DELETE and VIEW are conventional; nothing enforces them.

Where the check belongs

A template check hides a link. It does not protect anything, because the URL behind the link is still there. Template checks are for what the reader sees; the route itself needs guarding too, either in access_control or in the controller:

# config/packages/security.yaml
security:
    access_control:
        - { path: ^/admin, roles: ROLE_ADMIN }

If a page is worth hiding in a template, it is worth denying in one of those two places as well. The template is the courtesy; the other is the security.

Asking about somebody other than the current user

Every check above is about whoever is signed in, because that is what is_granted() reads. Symfony 7.3 added a second function for the case where the subject is a different user entirely:

{% if is_granted_for_user(teammate, 'ROLE_SALES_EXECUTIVE') %}
    <span class="badge">Can approve quotes</span>
{% endif %}

It runs the same voters and the same role hierarchy against the user you hand it. That matters for any screen that lists people and shows what each of them can do — a team page, an admin user list — where the previous answer was to reimplement the role logic in PHP and let the two drift.

On Symfony 7.2 and earlier the function does not exist, and the isGrantedForUser() method behind it does not either.

Questions this keeps raising

What is the difference between IS_AUTHENTICATED_FULLY and IS_AUTHENTICATED?

IS_AUTHENTICATED_FULLY is true only when the visitor signed in during this session by supplying credentials. IS_AUTHENTICATED is true for that and also for a session restored from a remember-me cookie. Use FULLY for anything sensitive enough that you want a fresh sign-in, and IS_AUTHENTICATED for the ordinary "is anybody logged in" case.

Should I check for ROLE_USER to see if someone is logged in?

No. It works only because nearly every user record happens to carry that role, which makes it a convention your code is depending on rather than a fact the framework guarantees. IS_AUTHENTICATED asks the question you actually mean.

Why does is_granted('ROLE_USER') return true for my admin?

Because role_hierarchy in security.yaml says ROLE_ADMIN includes ROLE_USER. Anybody holding the parent role passes a check for the child. If you meant "ordinary users only", the hierarchy cannot express that and you need to test for the absence of the admin role explicitly.

Does hiding a link with is_granted protect the page?

No. It removes the link from the page and nothing more. The URL still resolves for anyone who types it, so the route needs its own guard in access_control or in the controller. The template check is there so people are not shown doors they cannot open.