Skip to content
  • About
    • What is Symfony?
    • Community
    • News
    • Contributing
    • Support
  • Documentation
    • Symfony Docs
    • Symfony Book
    • Screencasts
    • Symfony Bundles
    • Symfony Cloud
    • Training
  • Services
    • SensioLabs Professional services to help you with Symfony
    • Platform.sh for Symfony Best platform to deploy Symfony apps
    • SymfonyInsight Automatic quality checks for your apps
    • Symfony Certification Prove your knowledge and boost your career
    • Blackfire Profile and monitor performance of your apps
  • Other
  • Blog
  • Download
sponsored by SensioLabs
  1. Home
  2. Documentation
  3. Security
  4. How Does the Security access_control Work?
  • Documentation
  • Book
  • Reference
  • Bundles
  • Cloud

Table of Contents

  • 1. Matching Options
  • 2. Access Enforcement
  • Matching access_control By IP
    • Securing by an Expression
  • Restrict to a port
  • Forcing a Channel (http, https)

How Does the Security access_control Work?

Edit this page

How Does the Security access_control Work?

For each incoming request, Symfony checks each access_control entry to find one that matches the current request. As soon as it finds a matching access_control entry, it stops - only the first matching access_control is used to enforce access.

Each access_control has several options that configure two different things:

  1. should the incoming request match this access control entry
  2. once it matches, should some sort of access restriction be enforced:

1. Matching Options

Symfony creates an instance of RequestMatcher for each access_control entry, which determines whether or not a given access control should be used on this request. The following access_control options are used for matching:

  • path: a regular expression (without delimiters)
  • ip or ips: netmasks are also supported (can be a comma-separated string)
  • port: an integer
  • host: a regular expression
  • methods: one or many HTTP methods
  • request_matcher: a service implementing RequestMatcherInterface
  • attributes: an array, which can be used to specify one or more request attributes that must match exactly
  • route: a route name

6.1

The request_matcher option was introduced in Symfony 6.1.

6.2

The route and attributes options were introduced in Symfony 6.2.

Take the following access_control entries as an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# config/packages/security.yaml
parameters:
    env(TRUSTED_IPS): '10.0.0.1, 10.0.0.2'

security:
    # ...
    access_control:
        - { path: '^/admin', roles: ROLE_USER_PORT, ip: 127.0.0.1, port: 8080 }
        - { path: '^/admin', roles: ROLE_USER_IP, ip: 127.0.0.1 }
        - { path: '^/admin', roles: ROLE_USER_HOST, host: symfony\.com$ }
        - { path: '^/admin', roles: ROLE_USER_METHOD, methods: [POST, PUT] }

        # ips can be comma-separated, which is especially useful when using env variables
        - { path: '^/admin', roles: ROLE_USER_IP, ips: '%env(TRUSTED_IPS)%' }
        - { path: '^/admin', roles: ROLE_USER_IP, ips: [127.0.0.1, ::1, '%env(TRUSTED_IPS)%'] }

        # for custom matching needs, use a request matcher service
        - { roles: ROLE_USER, request_matcher: App\Security\RequestMatcher\MyRequestMatcher }

        # require ROLE_ADMIN for 'admin' route. You can use the shortcut "route: "xxx", instead of "attributes": ["_route": "xxx"]
        - { attributes: {'_route': 'admin'}, roles: ROLE_ADMIN }
        - { route: 'admin', roles: ROLE_ADMIN }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        https://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/security
        https://symfony.com/schema/dic/security/security-1.0.xsd">

    <srv:parameters>
        <srv:parameter key="env(TRUSTED_IPS)">10.0.0.1, 10.0.0.2</srv:parameter>
    </srv:parameters>

    <config>
        <!-- ... -->
        <rule path="^/admin" role="ROLE_USER_PORT" ip="127.0.0.1" port="8080"/>
        <rule path="^/admin" role="ROLE_USER_IP" ip="127.0.0.1"/>
        <rule path="^/admin" role="ROLE_USER_HOST" host="symfony\.com$"/>
        <rule path="^/admin" role="ROLE_USER_METHOD" methods="POST, PUT"/>

        <!-- ips can be comma-separated, which is especially useful when using env variables -->
        <rule path="^/admin" role="ROLE_USER_IP" ip="%env(TRUSTED_IPS)%"/>
        <rule path="^/admin" role="ROLE_USER_IP">
            <ip>127.0.0.1</ip>
            <ip>::1</ip>
            <ip>%env(TRUSTED_IPS)%</ip>
        </rule>

        <!-- for custom matching needs, use a request matcher service -->
        <rule role="ROLE_USER" request-matcher="App\Security\RequestMatcher\MyRequestMatcher"/>

        <!-- require ROLE_ADMIN for 'admin' route. You can use the shortcut route="xxx" -->
        <rule role="ROLE_ADMIN">
            <attribute key="_route">admin</attribute>
        </rule>
        <rule route="admin" role="ROLE_ADMIN"/>
    </config>
</srv:container>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// config/packages/security.php
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Config\SecurityConfig;

return static function (ContainerBuilder $containerBuilder, SecurityConfig $security) {
    $containerBuilder->setParameter('env(TRUSTED_IPS)', '10.0.0.1, 10.0.0.2');
    // ...

    $security->accessControl()
        ->path('^/admin')
        ->roles(['ROLE_USER_PORT'])
        ->ips(['127.0.0.1'])
        ->port(8080)
    ;
    $security->accessControl()
        ->path('^/admin')
        ->roles(['ROLE_USER_IP'])
        ->ips(['127.0.0.1'])
    ;
    $security->accessControl()
        ->path('^/admin')
        ->roles(['ROLE_USER_HOST'])
        ->host('symfony\.com$')
    ;
    $security->accessControl()
        ->path('^/admin')
        ->roles(['ROLE_USER_METHOD'])
        ->methods(['POST', 'PUT'])
    ;
    // ips can be comma-separated, which is especially useful when using env variables
    $security->accessControl()
        ->path('^/admin')
        ->roles(['ROLE_USER_IP'])
        ->ips([env('TRUSTED_IPS')])
    ;
    $security->accessControl()
        ->path('^/admin')
        ->roles(['ROLE_USER_IP'])
        ->ips(['127.0.0.1', '::1', env('TRUSTED_IPS')])
    ;

    // for custom matching needs, use a request matcher service
    $security->accessControl()
        ->roles(['ROLE_USER'])
        ->requestMatcher('App\Security\RequestMatcher\MyRequestMatcher')
    ;

    // require ROLE_ADMIN for 'admin' route. You can use the shortcut route('xxx') mehtod,
    // instead of attributes(['_route' => 'xxx']) method
    $security->accessControl()
        ->roles(['ROLE_ADMIN'])
        ->attributes(['_route' => 'admin'])
    ;
    $security->accessControl()
        ->roles(['ROLE_ADMIN'])
        ->route('admin')
    ;
};

For each incoming request, Symfony will decide which access_control to use based on the URI, the client's IP address, the incoming host name, and the request method. Remember, the first rule that matches is used, and if ip, port, host or method are not specified for an entry, that access_control will match any ip, port, host or method:

URI IP PORT HOST METHOD access_control Why?
/admin/user 127.0.0.1 80 example.com GET rule #2 (ROLE_USER_IP) The URI matches path and the IP matches ip.
/admin/user 127.0.0.1 80 symfony.com GET rule #2 (ROLE_USER_IP) The path and ip still match. This would also match the ROLE_USER_HOST entry, but only the first access_control match is used.
/admin/user 127.0.0.1 8080 symfony.com GET rule #1 (ROLE_USER_PORT) The path, ip and port match.
/admin/user 168.0.0.1 80 symfony.com GET rule #3 (ROLE_USER_HOST) The ip doesn't match neither the first rule nor the second rule. So the third rule (which matches) is used.
/admin/user 168.0.0.1 80 symfony.com POST rule #3 (ROLE_USER_HOST) The third rule still matches. This would also match the fourth rule (ROLE_USER_METHOD), but only the first matched access_control is used.
/admin/user 168.0.0.1 80 example.com POST rule #4 (ROLE_USER_METHOD) The ip and host don't match the first three entries, but the fourth - ROLE_USER_METHOD - matches and is used.
/foo 127.0.0.1 80 symfony.com POST matches no entries This doesn't match any access_control rules, since its URI doesn't match any of the path values.

Caution

Matching the URI is done without $_GET parameters. Deny access in PHP code if you want to disallow access based on $_GET parameter values.

2. Access Enforcement

Once Symfony has decided which access_control entry matches (if any), it then enforces access restrictions based on the roles, allow_if and requires_channel options:

  • roles If the user does not have the given role, then access is denied (internally, an AccessDeniedException is thrown).
  • allow_if If the expression returns false, then access is denied;
  • requires_channel If the incoming request's channel (e.g. http) does not match this value (e.g. https), the user will be redirected (e.g. redirected from http to https, or vice versa).

Tip

Behind the scenes, the array value of roles is passed as the $attributes argument to each voter in the application with the Request as $subject. You can learn how to use your custom attributes by reading How to Use Voters to Check User Permissions.

Caution

If you define both roles and allow_if, and your Access Decision Strategy is the default one (affirmative), then the user will be granted access if there's at least one valid condition. If this behavior doesn't fit your needs, change the Access Decision Strategy.

Tip

If access is denied, the system will try to authenticate the user if not already (e.g. redirect the user to the login page). If the user is already logged in, the 403 "access denied" error page will be shown. See How to Customize Error Pages for more information.

Matching access_control By IP

Certain situations may arise when you need to have an access_control entry that only matches requests coming from some IP address or range. For example, this could be used to deny access to a URL pattern to all requests except those from a trusted, internal server.

Caution

As you'll read in the explanation below the example, the ips option does not restrict to a specific IP address. Instead, using the ips key means that the access_control entry will only match this IP address, and users accessing it from a different IP address will continue down the access_control list.

Here is an example of how you configure some example /internal* URL pattern so that it is only accessible by requests from the local server itself:

1
2
3
4
5
6
7
8
# config/packages/security.yaml
security:
    # ...
    access_control:
        #
        # the 'ips' option supports IP addresses and subnet masks
        - { path: '^/internal', roles: PUBLIC_ACCESS, ips: [127.0.0.1, ::1, 192.168.0.1/24] }
        - { path: '^/internal', roles: ROLE_NO_ACCESS }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        https://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/security
        https://symfony.com/schema/dic/security/security-1.0.xsd">

    <config>
        <!-- ... -->

        <!-- the 'ips' option supports IP addresses and subnet masks -->
        <rule path="^/internal" role="PUBLIC_ACCESS">
            <ip>127.0.0.1</ip>
            <ip>::1</ip>
        </rule>

        <rule path="^/internal" role="ROLE_NO_ACCESS"/>
    </config>
</srv:container>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// config/packages/security.php
use Symfony\Config\SecurityConfig;

return static function (SecurityConfig $security) {
    // ...

    $security->accessControl()
        ->path('^/internal')
        ->roles(['PUBLIC_ACCESS'])
        // the 'ips' option supports IP addresses and subnet masks
        ->ips(['127.0.0.1', '::1'])
    ;

    $security->accessControl()
        ->path('^/internal')
        ->roles(['ROLE_NO_ACCESS'])
    ;
};

Here is how it works when the path is /internal/something coming from the external IP address 10.0.0.1:

  • The first access control rule is ignored as the path matches but the IP address does not match either of the IPs listed;
  • The second access control rule is enabled (the only restriction being the path) and so it matches. If you make sure that no users ever have ROLE_NO_ACCESS, then access is denied (ROLE_NO_ACCESS can be anything that does not match an existing role, it only serves as a trick to always deny access).

But if the same request comes from 127.0.0.1 or ::1 (the IPv6 loopback address):

  • Now, the first access control rule is enabled as both the path and the ip match: access is allowed as the user always has the PUBLIC_ACCESS role.
  • The second access rule is not examined as the first rule matched.

Securing by an Expression

Once an access_control entry is matched, you can deny access via the roles key or use more complex logic with an expression in the allow_if key:

1
2
3
4
5
6
7
8
9
10
# config/packages/security.yaml
security:
    # ...
    access_control:
        -
            path: ^/_internal/secure
            # the 'roles' and 'allow_if' options work like an OR expression, so
            # access is granted if the expression is TRUE or the user has ROLE_ADMIN
            roles: 'ROLE_ADMIN'
            allow_if: "'127.0.0.1' == request.getClientIp() or request.headers.has('X-Secure-Access')"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        https://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/security
        https://symfony.com/schema/dic/security/security-1.0.xsd">

    <config>
        <!-- ... -->
        <!-- the 'role' and 'allow-if' options work like an OR expression, so
             access is granted if the expression is TRUE or the user has ROLE_ADMIN -->
        <rule path="^/_internal/secure"
            role="ROLE_ADMIN"
            allow-if="'127.0.0.1' == request.getClientIp() or request.headers.has('X-Secure-Access')"/>
    </config>
</srv:container>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// config/packages/security.php
use Symfony\Config\SecurityConfig;

return static function (SecurityConfig $security) {
    // ...

    $security->accessControl()
        ->path('^/_internal/secure')
        // the 'role' and 'allow-if' options work like an OR expression, so
        // access is granted if the expression is TRUE or the user has ROLE_ADMIN
        ->roles(['ROLE_ADMIN'])
        ->allowIf('"127.0.0.1" == request.getClientIp() or request.headers.has("X-Secure-Access")')
    ;
};

In this case, when the user tries to access any URL starting with /_internal/secure, they will only be granted access if the IP address is 127.0.0.1 or a secure header, or if the user has the ROLE_ADMIN role.

Note

Internally allow_if triggers the built-in ExpressionVoter as like it was part of the attributes defined in the roles option.

Inside the expression, you have access to a number of different variables and functions including request, which is the Symfony Request object (see The HttpFoundation Component).

For a list of the other functions and variables, see functions and variables.

Tip

The allow_if expressions can also contain custom functions registered with expression providers.

Restrict to a port

Add the port option to any access_control entries to require users to access those URLs via a specific port. This could be useful for example for localhost:8080.

1
2
3
4
5
# config/packages/security.yaml
security:
    # ...
    access_control:
        - { path: ^/cart/checkout, roles: PUBLIC_ACCESS, port: 8080 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        https://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/security
        https://symfony.com/schema/dic/security/security-1.0.xsd">

    <config>
        <!-- ... -->
        <rule path="^/cart/checkout"
            role="PUBLIC_ACCESS"
            port="8080"
        />
    </config>
</srv:container>
1
2
3
4
5
6
7
8
9
10
11
12
// config/packages/security.php
use Symfony\Config\SecurityConfig;

return static function (SecurityConfig $security) {
    // ...

    $security->accessControl()
        ->path('^/cart/checkout')
        ->roles(['PUBLIC_ACCESS'])
        ->port(8080)
    ;
};

Forcing a Channel (http, https)

You can also require a user to access a URL via SSL; use the requires_channel argument in any access_control entries. If this access_control is matched and the request is using the http channel, the user will be redirected to https:

1
2
3
4
5
# config/packages/security.yaml
security:
    # ...
    access_control:
        - { path: ^/cart/checkout, roles: PUBLIC_ACCESS, requires_channel: https }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- config/packages/security.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        https://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/security
        https://symfony.com/schema/dic/security/security-1.0.xsd">

    <config>
        <!-- ... -->
        <rule path="^/cart/checkout"
            role="PUBLIC_ACCESS"
            requires-channel="https"
        />
    </config>
</srv:container>
1
2
3
4
5
6
7
8
9
10
11
12
// config/packages/security.php
use Symfony\Config\SecurityConfig;

return static function (SecurityConfig $security) {
    // ...

    $security->accessControl()
        ->path('^/cart/checkout')
        ->roles(['PUBLIC_ACCESS'])
        ->requiresChannel('https')
    ;
};
This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version
    We stand with Ukraine.
    Version:

    Symfony Security is backed by

    Symfony Code Performance Profiling

    Symfony Code Performance Profiling

    Check Code Performance in Dev, Test, Staging & Production

    Check Code Performance in Dev, Test, Staging & Production

    Symfony footer

    ↓ Our footer now uses the colors of the Ukrainian flag because Symfony stands with the people of Ukraine.

    Avatar of Korstiaan de Ridder, a Symfony contributor

    Thanks Korstiaan de Ridder (@korstiaan) for being a Symfony contributor

    1 commit • 2 lines changed

    View all contributors that help us make Symfony

    Become a Symfony contributor

    Be an active part of the community and contribute ideas, code and bug fixes. Both experts and newcomers are welcome.

    Learn how to contribute

    Symfony™ is a trademark of Symfony SAS. All rights reserved.

    • What is Symfony?

      • Symfony at a Glance
      • Symfony Components
      • Case Studies
      • Symfony Releases
      • Security Policy
      • Logo & Screenshots
      • Trademark & Licenses
      • symfony1 Legacy
    • Learn Symfony

      • Symfony Docs
      • Symfony Book
      • Reference
      • Bundles
      • Best Practices
      • Training
      • eLearning Platform
      • Certification
    • Screencasts

      • Learn Symfony
      • Learn PHP
      • Learn JavaScript
      • Learn Drupal
      • Learn RESTful APIs
    • Community

      • SymfonyConnect
      • Support
      • How to be Involved
      • Code of Conduct
      • Events & Meetups
      • Projects using Symfony
      • Downloads Stats
      • Contributors
      • Backers
    • Blog

      • Events & Meetups
      • A week of symfony
      • Case studies
      • Cloud
      • Community
      • Conferences
      • Diversity
      • Documentation
      • Living on the edge
      • Releases
      • Security Advisories
      • SymfonyInsight
      • Twig
      • SensioLabs
    • Services

      • SensioLabs services
      • Train developers
      • Manage your project quality
      • Improve your project performance
      • Host Symfony projects

      Deployed on

    Follow Symfony