Route Protection

A path is protected by configuration, not by code. The access rules (AccessRules, the one authority on who may enter which path) know which paths are protected and which roles each one asks; the authentication asks them whether the path of the request needs a user, and if it does and the user is the anonymous one, it does not let the request go on. A path is also protected when its route declares roles (see below). See How it works.

Protected paths

AUTH_PROTECTED_PATHS is a JSON that is a list of paths, or a map from a path to the roles that it needs:

# Everything under /dashboard and /profile needs a user.
AUTH_PROTECTED_PATHS='["/dashboard", "/profile"]'

# Everything under /admin needs the role admin; /staff needs editor or admin.
AUTH_PROTECTED_PATHS='{"/dashboard": [], "/admin": ["admin"], "/staff": ["editor", "admin"]}'
  • A path is protected if the path of the request is the path of the rule or is below it, by segments: /admin protects /admin, /admin/users and /admin/users/1, and not /admin-panel or /administer. To protect those too, write them.
  • In the list form a path needs a user, whoever it is, and nothing else: it is authentication only, a decision of the site that is explicit (a route with no roles is never protected by default). In the map form the roles of the path are the ones that Authorization grants; a role as a string ("admin") is the same as a list of one, and [] is the same as the list form.
  • The roles of a path in the map form win over the roles of the route that it matches: the site decides over the routes that it imports, for example the content of a package. If the path has no roles, the roles of the route count.
  • When a path is under several rules, the most specific one (the one that has more segments) is the one that counts, whatever the order in which they were written: with {"/api": ["api"], "/api/human_resources": ["rrhh"]}, /api/human_resources/x needs rrhh and /api/billing needs api. A more specific rule that has no roles (list form) says that only a user is needed under it, even if the one above asks for roles. The root, /, is the rule of every path that has no other.
  • With AUTH_ENABLED=false no path is protected.

How the paths are compared

The path of the request and the paths of the rules are compared in their canonical form (see Url), and without telling the case apart, so the way a path is written does not decide whether it is protected. With /api/index protected, these are all protected: /api//index, //api/index, /api/./index, /api/%69ndex, /api/index/ and /API/Index.

The case is ignored on purpose: a server that reads files from a file system that does not tell it apart (the default one of macOS and Windows) serves /Academy/x with the file of /academy/x, and a rule must not depend on that.

The pipeline gives the authentication the canonical path (what the access rules compare): RouterMiddleware of derafu/http refuses a path that has no safe form (/api/../x, /api%2Fx) with a 400, and replaces the others with their canonical form before the authentication runs. If a path that has no safe form gets here anyway (a pipeline that does not have the router before the authentication), it is treated as protected: it needs a user, and no role is checked.

A rule that is not a path (an empty one, or one with .., an escaped slash or a control character) is an error of the configuration, a ConfigurationException, when the configuration is made: a rule that protects nothing, without anybody noticing, is worse than an error. A rule can be written in any way that has a canonical form (admin/ is /admin).

There are no regular expressions, wildcards or exclusions: for a route that needs its own rule, the route itself can decide (see Authorization).

A route with roles is protected

A route that declares roles: needs a user even if its path is not in AUTH_PROTECTED_PATHS. Otherwise those roles would be ignored in silence, which would leave open a path that its own route asks to close.

The login and the logout are public

The login path (/auth/login, or the callback of Keycloak) and the logout path are the way in and the way out, so they are handled before looking at what is protected: a protected path that includes them (/auth protecting /auth/login, for example) does not turn them away.

What a user that is not authenticated gets

Request Answer
A path of the API (AUTH_API_PATHS, /api by default, or below it: /api/items, not /apiary) A JSON response with the status 401: {"status": 401, "title": "...", "detail": "..."}, translated. It is never a redirect, a client of an API can not use a login page. It carries a WWW-Authenticate header. See API.
Any other path, Keycloak A redirect to Keycloak. The page is remembered (its path and query), and the user goes back to it after the login.
Any other path, database The error “You must be logged in to access the requested page {path}” as a flash message, a redirect to AUTH_WEB_UNAUTHORIZED_REDIRECT_PATH (the login page), and the page remembered.
The logout path The session is closed, and the user goes to AUTH_WEB_LOGOUT_REDIRECT_PATH.

A user that is authenticated but is not granted by the roles of the path gets a response with the status 403 (see Authorization).

The page that is remembered

The page that was requested before the login is kept in the session by its path and query, never by its URL: what is stored never takes the user to another site, because the host of a request is not to be trusted. A path that starts with two slashes (or a slash and a backslash), which a browser reads as another site, is made a path of this one. It is used once, after the login, and then it is removed.

On this page

Last updated on 10/10/2026 by Anonymous