Authorization

Derafu Auth keeps authentication (who is asking: AuthenticationManager) apart from authorization (may they enter: AuthorizationManager, with the rules of access in AccessRules): the roles of the user, the roles that a path or a route needs, and the middleware of Mezzio that puts them together. See How it works. The permissions of an application, if it has them, are its own.

The roles of the user

$user->getRoles() has the roles that the provider knows:

  • Keycloak: the roles of the realm and the roles of the client of the application (not the ones of other clients). See Keycloak.
  • Database: the first column of each row of the query AUTH_DATABASE_USER_SQL_GET_ROLES. See Database.
  • Htpasswd: the groups of the user in the .htgroup file. See Htpasswd.
  • Anonymous user: the role anonymous.

The user also has hasRole(), hasAnyRole() and hasAllRoles().

The middleware

The middleware of the authorization (Derafu\Auth\Authorization\AuthorizationMiddleware, the one that a pipeline names) goes after the one of the authentication (Derafu\Auth\Authentication\AuthenticationMiddleware). It takes the user from the request and asks Derafu\Auth\Authorization\AuthorizationManager::isGranted($role, $request) for each role of the user; the first role that is granted lets the request go on.

Authorization does not look at who is authenticated: that was decided before, by the authentication (see Route Protection). It only decides which roles a request needs, and whether the role it is asked about is one of them.

The roles that a request needs

  1. If the protected path that matches (AUTH_PROTECTED_PATHS, the map form) has roles, those are the roles. The site decides over the routes that it imports.
  2. Otherwise, the roles that the matched route declares (roles: in the route, see Routing).
  3. If there are none, nothing is needed: the path is public, or it is protected and only asks for a user. Any role is granted.

When roles are needed, the role is granted only if it is one of them.

What a user that is not granted gets

The middleware does not make a response: it throws an AuthorizationException with the code 401 if there is no user in the request, and with 403 if no role of the user is granted. The application answers it as any other error (see Error Handling): an error page in HTML for a browser (the error.html.twig of the site, with the status 403 and the title Forbidden), and a problem in JSON for a client of the API, with the status and the title that the code says.

The message says which roles give access, so the person knows what to ask for:

You do not have access to /academy/curso-x. These roles give access: course-x, course-all.

It names the roles that the path asks (the ones of the protected path, or else the ones of the route). If the path only asks for a user and the user has no roles, it says that instead: You do not have access to /private: your user has no roles. The names of the roles are shown to whoever is denied, so do not give a role a name that says something that should not be said.

A user without roles

The middleware loops over the roles of the user, so a user with none is denied in every path that goes through the authorization, even if the path needs no roles in particular (there is no role that can be granted). This is how the middleware of Mezzio works, and no role means no permission: it is not something that the package fixes with a role by default.

Make sure that every user has at least one role: a role in Keycloak, or a row in the roles of the database provider.

The standard and what Derafu Auth does

The authorization of Mezzio (mezzio-authorization-rbac and mezzio-authorization-acl) is default deny: the route is the resource, a configuration says which roles can access it, and the user is granted when one of their roles is. Derafu Auth keeps that, and it adds two things on purpose.

Case Standard (Mezzio RBAC and ACL) Derafu Auth
No user in the request 401 401
A user without roles 403 403
A role that the path or the route asks for 200 200
Roles, but none that the path or the route asks for 403 403
A route that no role has permission for 403: nobody has it Protected path without roles (the list form, or []): 200 for a user that has a role. It only asks for a user. A path that is not protected is public.
A role that nobody declared An exception, a 500 Not an error: it grants nothing, and the other roles of the user count
No route matched 200 200
Where the roles of a resource are In the configuration of the package, by the name of the route In the route (roles:) or in AUTH_PROTECTED_PATHS by path prefix; the site wins over the route
Who asks for a user Only the middleware, when it is missing The authentication: a protected path, and also a route that declares roles, even if its path is not listed
Role hierarchy and assertions Yes (parents of a role, assertions) No

The two additions:

  • A path that is protected without roles only asks for a user. The standard has no way to ask only for authentication, because a resource always has roles. Here ["/dashboard"] is an explicit decision of the site, and it never comes by default in the routes (a route with no roles is public, as in the standard). A user without roles is still denied there (see above).
  • Roles that nobody declared are not an error. The roles of a provider like Keycloak come from outside and are not all known to the application (offline_access, default-roles-*).

The roles of a path

The map form of AUTH_PROTECTED_PATHS (see Route Protection):

AUTH_PROTECTED_PATHS='{"/dashboard": [], "/admin": ["admin"], "/staff": ["editor", "admin"]}'

This is for the paths of routes that the application does not write, for example the content of a package that is imported: the package does not know the roles of your site, and the site puts them from outside.

The route decides

A route of the application can have its own roles, in routes.yaml. The authorization asks the route when the protected path has none, and the authentication protects the path of a route that has roles, even if the path is not in AUTH_PROTECTED_PATHS:

reports:
    path: /dashboard/reports
    roles: [analyst, admin]
    handler: App\Controller\ReportsController::index

The router only keeps the roles of the route (getRoles(), hasRole() and hasAnyRole()); what it means for a route to have them, or not, is decided here.

Checking in the code

The user is in the request, and the controller can ask:

use Mezzio\Authentication\UserInterface;

$user = $request->getAttribute(UserInterface::class);

if (!$user->hasRole('admin')) {
    return new JsonResponse(['error' => 'Forbidden'], 403);
}

Authorization also has two methods for the cases where the roles are not the ones of the path:

// Any of the roles (false if no role is asked).
$authorization->isGrantedAny(['editor', 'admin'], $request);

// All of the roles (true if no role is asked, but only for a user).
$authorization->isGrantedAll(['editor', 'admin'], $request);

They only look at the user of the request, not at the path: without a user nothing is granted, whatever is asked (being granted all of no roles is for a user, not for nobody), also in a path that is not protected.

Permissions

Roles are enough for most applications: the application maps its permissions to roles (user:write is for admin and editor) and checks the role. If more granularity is needed, Keycloak can put the permissions of the user in a claim, which comes as a detail of the user.

Configure Keycloak

  1. In Client scopes, create a scope permissions and add it to the client.
  2. In its Mappers, create a mapper of type User Attribute, with the user attribute permissions, the token claim name permissions and the claim JSON type String.
  3. Give the user the attribute permissions, with a value like user:read,user:write.
  4. Ask for the scope:
AUTH_KEYCLOAK_WEB_SCOPES='["openid", "profile", "email", "permissions"]'

Use them

$permissions = explode(',', (string) $user->getDetail('permissions'));

if (!in_array('user:write', $permissions, true)) {
    return new JsonResponse(['error' => 'Forbidden'], 403);
}

The claim comes in the access token, which the package verifies, so it can be trusted as much as the roles. A structure like resource:action (user:read, report:generate) keeps the permissions easy to read and to grant.

On this page

Last updated on 10/10/2026 by Anonymous