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:
| 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".