Skip to content
V
For developers
Architecture, conventions, the core and security
Security / Authentication and authorisation

Authentication#

Two independent routes.

Route Where With what
Session website and administration the sign-in form
JWT API a token in the header

Sessions#

Sign-in is by username, not email. app/Core/Security/UserAuthenticator.php.

An email in the sign-in field does not work

The system looks the user up by username. It is the most common support question — mention it in the text next to the form.

JWT for the API#

The token is obtained from the sign-in endpoint and sent in a header:

Authorization: Bearer <token>
Item Note
Lifetime limited, refreshed by a refresh token
Rotation the refresh token changes on use
Reuse detection using an old refresh token invalidates the chain
Deny list a signed-out token stops working immediately

Rotation with reuse detection

A stolen refresh token is recognised by being used twice. The system then invalidates the whole branch — the attacker and the legitimate user both have to sign in again.

A key for third parties#

For server-to-server integrations there is a key in the X-API-Key header.

The key does not belong in a URL or in client-side code

URLs are logged and stay in browser history. The key belongs in a request header sent from a server.

Sign-in through an outside service#

app/UI/Front/System/Components/LoginExternalComponent/ and the providers in the administration.

A provider's secret is not displayed in the administration

The field is a password field; empty means "keep". Do not display it, not even in a log.

What the API requires#

Verification runs as the very first thing in every Api presenter, in this order:

Step The question What happens when it fails
CORS is this browser allowed? the preflight request is answered right away
JWT who are you? without a valid token, 401
Permission what may you do? without a privilege, an empty result

Exceptions are marked in the code itself with the #[PublicAction] attribute — sign-in, token refresh, the activation email, forgotten password, and the return addresses of payment gateways.

A missing permission is not an error — it is SILENTLY EMPTY

This is today's most commonly hit trap. An endpoint with no privilege record returns neither a 403 nor anything in the response to log; it returns an empty list. When something "does not work and there is no error anywhere", look for the privilege first, not for a bug in the code.

A public action needs a brake of its own

Whatever sits behind #[PublicAction] is not protected by a token. Sign-in, token refresh and forgotten password therefore have rate limits; a new public action must get one too, or it can be called without limit.

Auditing#

Sign-in attempts are written to log/auth.log and to a table.

The audit log is never rewritten

Not even when cleaning up test data. It is a record of what actually happened; rewriting it would make it a forgery.

Switches#

config/Shared/security.neon.

A disabled protection reminds you of nothing

Turn a check off for debugging and production will not flag it. Put it back at once, not "later".