Security
This page says what the package does for the security of the login and the session, and what the application has to do.
What the package does
Secrets
The secrets (the client secret of Keycloak, the database) come from environment variables, never from the code or from a file of the repository:
AUTH_KEYCLOAK_CLIENT_SECRET=the-secret
AUTH_DATABASE_URL=sqlite:%kernel.project_dir%/var/users.db
HTTPS and the cookie
The cookie of the session is Secure (HTTPS only), HttpOnly (not readable by JavaScript) and SameSite=Lax (not sent in a POST from another site) by default. In development over HTTP only SESSION_COOKIE_SECURE=false is needed. The certificate of Keycloak is verified by default (AUTH_KEYCLOAK_HTTP_VERIFY=true): turn it off only in development.
Session fixation
The session id is renewed when a user logs in (the database and htpasswd providers, and the callback of Keycloak) and when a user logs out, keeping the data of the session. An id that was known before the login is of no use after it. It is done by SessionManagerInterface::regenerate(), which renews the session of the request, and the persistence of the session gives the new id to the client at the end of the request.
It requires the session of Mezzio\Session\SessionMiddleware (LazySession), which is renewed in place. A session that answers with another instance, like the plain Mezzio\Session\Session, can not be renewed for the request that is being handled, and regenerate() raises an AuthenticationException instead of letting the login go on with the old id.
Logout
The logout is a POST from the same origin. A logout that a link or an image of another site could trigger (a GET, or a POST from another site) would be a cross-site request forgery, so:
- The routes of the package accept only POST (
methods: [POST]). - The authentication only logs the user out of a POST that comes from the same origin, by what the browser says:
Sec-Fetch-Site(same-originornone) or, if there is none,Origin(same scheme, host and port). A request that says neither is not made by a browser, and a browser always sends one of them in a POST. - Any other request to the logout path is not a logout: the user keeps the session and goes to
AUTH_WEB_LOGOUT_REDIRECT_PATH.
In the pages, use a form:
<form method="post" action="/auth/logout">
<button type="submit">Log out</button>
</form>
CSRF in the Keycloak login
The state is checked when the user comes back, so a callback that this session did not start is rejected. The login also uses PKCE (a code that was stolen can not be exchanged by another client) and a nonce (an ID token can not be replayed in another login). The forms of the database provider (its login) and of the Keycloak provider (the one that makes an API token) have the protection of the forms of derafu/form: a CSRF token that is checked before the data is used, with the manager of derafu/csrf (see Database).
Tokens
The tokens that Keycloak gives are verified, not only read (their signature with the keys of the realm, issuer, expiration, audience, azp and nonce), and they are kept in the session, on the server: never send them to the client.
// ✅ Good: the session of the server keeps the tokens (the package does it).
// ❌ Bad: never put them in a response or in JavaScript.
return new JsonResponse(['token' => $accessToken]);
See Keycloak for what is verified and for the cache of the keys.
The user of the session is a copy
When a user logs in the session keeps what the provider said about it: its identity, its roles and its details. On every request the user is built from that copy, without asking the provider. So a change in the provider (a role that is taken away, a user that is disabled or deleted) is not seen until the copy is asked again, and the package asks again every AUTH_WEB_REFRESH_INTERVAL_SECONDS seconds:
| Keycloak | Database | |
|---|---|---|
| What is asked | A refresh of the tokens, and the user info of the new access token. | The user by its identity: its roles and details, with the queries of the configuration. |
| When | When the access token expires or, if it is before, every AUTH_WEB_REFRESH_INTERVAL_SECONDS seconds: what happens first. If the variable is not set, the expiration of the token is the only clock. |
Every AUTH_WEB_REFRESH_INTERVAL_SECONDS seconds, 300 (five minutes) if the variable is not set. |
| A role that changed | It is the one of the session until then (by default a token lasts 5 minutes in Keycloak: Realm settings > Tokens > Access Token Lifespan). | The same, until the interval passes. |
| A user that is gone | Keycloak refuses the refresh token (the user was disabled, or its session was ended): the session is closed and the user goes to log in again. | The identity is not in the table, or the user is not active (see Active users): the session is closed. |
The time between two checks is a single clock, kept in the session: the expiration of the token and the interval are not two timers, one of them decides, the one that comes first. A session that does not have the time of the last check (one that was made before it was kept) is asked at the next request, and has it from then on.
When the provider can not be asked. If Keycloak does not answer, is down or fails, or refuses for a reason that is not about the session (the application’s secret is not the one that Keycloak has), or if the database fails, the session is not closed: nothing is thrown away. The user is not let in without being verified (the request is the one of a user that is not logged in) and the next request asks again, so a short failure of the provider does not log everybody out. Only a refusal that says that the session is over (invalid_grant, invalid_token) closes it.
The requests to Keycloak have timeouts (AUTH_KEYCLOAK_HTTP_TIMEOUT_SECONDS and AUTH_KEYCLOAK_HTTP_CONNECT_TIMEOUT_SECONDS, 30 seconds by default), so a Keycloak that does not answer is not waited for with no end. Lower them if 30 seconds is too long for your pages.
Refresh tokens that Keycloak renews. If the realm renews the refresh token on each use (Revoke Refresh Token), a refresh token can be used once. Keep the sessions as they are by default (the session of PHP locks the session while a request has it, and Mezzio does not turn that off): two requests of the same session then go one after the other, and the second one finds the tokens that the first one saved. With a session that does not lock, two requests that refresh at the same time read the same token, the second one is refused, and the user may be logged out depending on which request saves last.
Passwords (database provider)
- The hash of a password that was made with an old algorithm or cost is made again at the login.
- A user that does not exist takes as long as one that does, so the time does not say which identities exist.
- The failed attempts are limited by identity and by network of the client, and a limited login does not check the credentials.
- The column of the password never goes in the user, and so never in the session.
See Database.
The page after the login
The page that was requested is remembered by its path and query only, never by its URL, and it is made a path of this site, so what is stored never takes the user to another site.
What is left to the application
-
HTTPS in production, and the headers that go with it. For example:
$response = $response ->withHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains') ->withHeader('X-Content-Type-Options', 'nosniff') ->withHeader('X-Frame-Options', 'DENY'); -
The address of the client behind a proxy: the limit of the failed logins counts by the
client_networkthatClientIpMiddlewareofderafu/httpleaves in the request, or by theREMOTE_ADDRof the connection without it. Behind a proxy, configure there which ones are trusted (HTTP_TRUSTED_PROXIES), or every user is the proxy. -
Not showing internal errors to the user in production: the exceptions of the package have messages that are translatable and meant to be shown, but the application decides what its error page says.
-
At least one role for every user (see Authorization).
-
The settings of the realm of Keycloak: a policy of passwords, brute force detection, and the timeouts of the sessions.
Keycloak: recommended settings
The client
See Keycloak.
The realm
- Password policy: strong passwords.
- Brute force detection: on.
- Session timeouts: set to what the application needs.
- SSL required:
externalorall.