🔒️(keycloak) added a setup guide for IDENTITY_PROVIDER

Removed the direct access grant on the rest-api client in the dev
realm.json

Repair make test-keycloak, which could not run against a freshly
imported realm. The fixtures need more rights than the rest-api service
account holds, so they now use the bootstrap admin service account. The
bootstrap admin user cannot serve here, because it has an empty profile
and Keycloak refuses the password grant.

Grant view-realm to the service account. set_realm_role reads a realm
role by id, which fails with 403 without that role.

Add a setup guide for the authentication provider and one for the
identity provider. Document the required client shape in env.md.
This commit is contained in:
Bastien Ogier
2026-08-14 20:29:31 +02:00
parent 06e8bdb93c
commit c46e2045bb
8 changed files with 292 additions and 23 deletions
+13
View File
@@ -8,10 +8,23 @@ and this project adheres to
## [Unreleased]
### Added
- Setup guides for the authentication provider and the identity provider
### Changed
- Bump keycloak to 26.7.1
### Fixed
- Grant view-realm to the Keycloak service account, needed by the 2FA toggle
- Run make test-keycloak against a freshly imported Keycloak realm
### Security
- Turn the direct access grant off on the Keycloak rest-api client
## [0.9.0] - 2026-07-22
### Added
+3 -1
View File
@@ -1,5 +1,7 @@
KC_BOOTSTRAP_ADMIN_USERNAME=admin
KC_BOOTSTRAP_ADMIN_PASSWORD=admin
KC_BOOTSTRAP_ADMIN_CLIENT_ID=bootstrap-admin
KC_BOOTSTRAP_ADMIN_CLIENT_SECRET=BootstrapAdminClientSecretForDev
KC_DB=postgres
KC_DB_URL_HOST=postgresql
KC_DB_URL_DATABASE=messages
@@ -10,4 +12,4 @@ KC_HOSTNAME_STRICT=false
KC_HOSTNAME_STRICT_HTTPS=false
KC_HTTP_ENABLED=true
KC_HEALTH_ENABLED=true
PROXY_ADDRESS_FORWARDING=true
PROXY_ADDRESS_FORWARDING=true
+125
View File
@@ -0,0 +1,125 @@
# Authentication Provider Setup Guide
Messages uses OpenID Connect in two distinct parts. Do not confuse them.
1. **Authentication.** Messages delegates each user login to an OIDC
provider. The provider can be ProConnect, Keycloak, or any
other OIDC provider. Every Messages instance needs one, this is
managed by the `OIDC_**` environment vars.
2. **Mailbox identities and Maildomains.** A mail domain decides how a user gets a
mailbox. This part is independent of the authentication provider.
This is managed by the `IDENTITY_PROVIDER` environment variable,
for now Messages only supports Keycloak.
This guide covers the first part only. If you are looking for the mailbox
and maildomain provisioning, see [identity-provider.md](identity-provider.md).
## What Messages Expects from the Provider
Messages acts as a standard OIDC relying party. Any compliant provider
works. Messages needs:
- the authorization code flow, with a confidential client;
- an `email` claim in the userinfo response. Messages matches a user to a
mailbox by this address;
- the five endpoints listed below. Most providers publish them at
`/.well-known/openid-configuration`.
## Setup Steps
1. **Create a client at your provider.**
Set the redirect URI, for example
`https://messages.example.fr/api/v1.0/callback/`.
Set the Logout redirect URI, for example
`https://messages.example.fr/api/v1.0/logout-callback/`.
2. **Set the client variables.** Set `OIDC_RP_CLIENT_ID` and
`OIDC_RP_CLIENT_SECRET` to the credentials of that client.
3. **Set the endpoint variables.** Read them from the provider's
`/.well-known/openid-configuration` document:
| Variable | Discovery document key |
|----------|------------------------|
| `OIDC_OP_AUTHORIZATION_ENDPOINT` | `authorization_endpoint` |
| `OIDC_OP_TOKEN_ENDPOINT` | `token_endpoint` |
| `OIDC_OP_USER_ENDPOINT` | `userinfo_endpoint` |
| `OIDC_OP_JWKS_ENDPOINT` | `jwks_uri` |
| `OIDC_OP_LOGOUT_ENDPOINT` | `end_session_endpoint` |
The browser reaches the authorization endpoint and the logout endpoint.
Give them a public URL. The backend reaches the other three. A private
URL is enough for those.
4. **Set the redirect hosts.** Add each host that serves Messages to
`OIDC_REDIRECT_ALLOWED_HOSTS`. Set `OIDC_REDIRECT_REQUIRE_HTTPS` to
`True` in production.
5. **Check the scopes.** `OIDC_RP_SCOPES` must request the claims Messages
needs. The default is `openid email`.
See the [OIDC Configuration](env.md#oidc-configuration) and
[OIDC Advanced Settings](env.md#oidc-advanced-settings) sections of
`env.md` for the full variable tables.
### ProConnect as the Authentication Provider
For ProConnect Integration you can create your application at
[https://partenaires.proconnect.gouv.fr](https://partenaires.proconnect.gouv.fr)
and can use the following variables :
```env
OIDC_RP_CLIENT_ID=XX
OIDC_RP_CLIENT_SECRET=XX
OIDC_OP_AUTHORIZATION_ENDPOINT=https://fca.integ01.dev-agentconnect.fr/api/v2/authorize
OIDC_OP_JWKS_ENDPOINT=https://fca.integ01.dev-agentconnect.fr/api/v2/jwks
OIDC_OP_LOGOUT_ENDPOINT=https://fca.integ01.dev-agentconnect.fr/api/v2/session/end
OIDC_OP_TOKEN_ENDPOINT=https://fca.integ01.dev-agentconnect.fr/api/v2/token
OIDC_OP_USER_ENDPOINT=https://fca.integ01.dev-agentconnect.fr/api/v2/userinfo
OIDC_AUTH_REQUEST_EXTRA_PARAMS={"acr_values": "eidas1"}
```
ProConnect requires a Level of Assurance in the authorization request.
`OIDC_AUTH_REQUEST_EXTRA_PARAMS` carries it, for example
`{"acr_values": "eidas1"}`. The mobile application depends on this value
too. See [mobile.md](mobile.md).
For ProConnect Production, contact the ProConnect team.
### Keycloak as the Authentication Provider
You can use Keycloak for this part too. Create a second client, next to the
service-account client of [identity-provider.md](identity-provider.md).
1. **Create the `messages` client.** Configure it as a confidential
client. Turn the standard flow on. Turn the direct access grants off.
Turn the service accounts off.
Set the redirect URIs to your own host names.
The dev realm (`src/keycloak/realm.json`) shows example values. Adapt
the values to your own host names. Do not reuse the example values.
2. **Set the login variables.** Set `OIDC_RP_CLIENT_ID` and
`OIDC_RP_CLIENT_SECRET` from the `messages` client. Set the five
endpoint variables from the realm, at
`https://<KEYCLOAK_URL>/realms/<realm>/.well-known/openid-configuration`.
## How a User Gets a Mailbox After the Login
The login alone does not give a user a mailbox. The mail domain decides
that, and it is a separate mechanism.
In short:
- a mail domain with `oidc_autojoin` gives each user connected through
the authentication provider a mailbox at login, you don't need to
setup anything else;
- a mail domain with `identity_sync` lets you create the mailboxes as you
need, but you'll then need an `IDENTITY_PROVIDER`, see
[identity-provider.md](identity-provider.md).;
- a mail domain with neither field can manage shared mailboxes only, the
admin must configure the access on every mailbox to an authenticated user
manually.
+2
View File
@@ -532,6 +532,8 @@ Used for provisioning-side operations against Keycloak (e.g. toggling
mandatory 2FA on a mail domain). Distinct from the OIDC login settings
above, which handle end-user authentication.
See [identity-provider.md](identity-provider.md) for the full setup guide.
| Variable | Default | Description | Required |
|----------|---------|-------------|----------|
| `IDENTITY_PROVIDER` | None | Identity-provider integration to enable (e.g. `keycloak`). Unset disables provisioning-side IdP calls. | Optional |
+117
View File
@@ -0,0 +1,117 @@
# Identity Provider Setup Guide
Messages uses OpenID Connect in two distinct parts. Do not confuse them.
1. **Authentication.** Messages delegates each user login to an OIDC
provider. The provider can be ProConnect, Keycloak, or any
other OIDC provider. Every Messages instance needs one, this is
managed by the `OIDC_**` environment vars.
2. **Mailbox identities and Maildomains.** A mail domain decides how a user gets a
mailbox. This part is independent of the authentication provider.
This is managed by the `IDENTITY_PROVIDER` environment variable,
for now Messages only supports Keycloak.
This guide covers the second part only. It shows how to set up Keycloak as
the store that holds the mailbox identities. If you are looking for the
user login, see [authentication-provider.md](authentication-provider.md).
## How a User Gets a Mailbox
A mail domain has two independent fields. They give three configurations:
| Mail domain | Who creates the mailbox | IDENTITY_PROVIDER needed |
|-------------|-------------------------|-----------------|
| `oidc_autojoin` on | Messages, at each login | No |
| `identity_sync` on | The administrator. Messages then pushes the mailbox to `IDENTITY_PROVIDER` | Yes |
| Neither field on | The administrator, shared mailbox only | No |
**With `oidc_autojoin`.** Each user who logs in with an address on the
domain gets a mailbox at once. Messages creates the mailbox locally. You do
_not_ need the `IDENTITY_PROVIDER` for this.
**With `identity_sync`.** The administrator creates the mailbox
`user1@example.fr`. Messages synchronizes it to Keycloak as a user. The
user then logs in through ProConnect, or through another provider
(the `OIDC_**` vars), with `user1@example.fr`. The user gets access
to the mailbox.
**With neither field.** The administrator creates shared mailboxes only.
Each user logs in through the OIDC provider. The administrator then gives
the user access to a shared mailboxes manually.
You therefore need this guide only when you want `identity_sync`.
## When the Login Address Differs from the Mailbox
Messages matches a user to a mailbox by the address. The two addresses can
differ. The administrator creates the mailbox `user1@aaa.fr`, but the user
logs in with `user1@bbb.fr`. Messages does not link them.
In this case, give the user an explicit access to the mailbox. Grant
`user1@bbb.fr` an access to the `user1@aaa.fr` mailbox.
## Keycloak Setup Steps
For now, Messages only supports Keycloak as an `IDENTITY_PROVIDER`.
1. **Get the Keycloak image.** Use the
[ghcr.io/suitenumerique/messages-keycloak](https://ghcr.io/suitenumerique/messages-keycloak)
image that this project publishes to the GitHub container registry.
You can also build the image from `src/keycloak`.
The image bundles providers that
Messages needs:
- the `map-group-attribute` script mapper, which puts group attributes
into the token claims;
- the `bulk-role-membership` admin extension, which makes a bulk
role-membership check faster.
A plain upstream `quay.io/keycloak/keycloak` image does not have these
providers. Do not deploy a plain upstream image.
2. **Deploy Keycloak.** Deploy the image with the
[st-cli](https://github.com/suitenumerique/st-ansible/tree/main/cli).
st-cli wraps the same collection in a simpler command-line tool. st-cli
is the recommended tool for a self-hoster.
You can also deploy it with
[st-ansible](https://github.com/suitenumerique/st-ansible) collection or
yourself with the docker compose examples.
3. **Create a realm.** Create a dedicated Keycloak realm for Messages, for
example `messages`.
4. **Create the `serviceaccount-messages` client.** This client holds the service account
that Messages uses for the provisioning calls.
Turn "Client authentication" on. Turn "Authorization" on.
Turn "Service accounts roles" on. Turn the "Standard flow" off.
You can setup the Root URL to the Messages URL.
5. **Assign the realm-management roles.** On the user in the "Service
account roles" tab, assign the client roles `query-users`,
`manage-users`, `view-users` and `view-realm`.
6. **Set the provisioning variables.** Set `IDENTITY_PROVIDER` to
`keycloak`. Set `KEYCLOAK_URL`, `KEYCLOAK_REALM` and
`KEYCLOAK_GROUP_PATH_PREFIX`. Set `KEYCLOAK_CLIENT_ID` to `serviceaccount-messages`.
Set `KEYCLOAK_CLIENT_SECRET` to the secret of that client. See the
[Identity Provider (Keycloak)](env.md#identity-provider-keycloak)
section of `env.md` for the full variable table.
7. **Enable `identity_sync` on each mail domain.** Enable this field on
every mail domain that Messages must sync to Keycloak. Set the field in
the Django admin, on the `MailDomain` change form.
8. **Verify the setup.** Run the resync command:
```bash
python manage.py identity resync-all
```
The command syncs each mail domain and each mailbox again. It reports
the number of synced domains and the number of synced mailboxes.
## The Authentication Provider
The steps above do not set up a user login. Messages needs an
authentication provider as well. You can use Keycloak for that too, or
keep another provider such as ProConnect. See
[authentication-provider.md](authentication-provider.md).
+3
View File
@@ -181,6 +181,7 @@ Messages uses OpenID Connect (OIDC) for user authentication. This is the only au
- Pre-configured with Messages realm and users
- Suitable for organizations wanting a self-hosted identity provider
- Configure via `deploy/env/production/keycloak.defaults`
- See the [identity provider guide](./identity-provider.md) to create the realm and the clients
2. **External OIDC Provider**
- Use any OIDC-compliant identity provider
@@ -191,6 +192,8 @@ Messages uses OpenID Connect (OIDC) for user authentication. This is the only au
**User Management:**
- Users are created automatically when they first log in via OIDC
- Mailboxes can be created automatically based on OIDC email addresses
- A mail domain with `oidc_autojoin` gives each user a mailbox at login. This needs no `IDENTITY_PROVIDER`.
- A mail domain with `identity_sync` pushes each mailbox to Keycloak. See the [identity provider guide](./identity-provider.md)
### 7. Production Deployment
+2 -2
View File
@@ -430,7 +430,7 @@
"disableableCredentialTypes" : [ ],
"requiredActions" : [ ],
"clientRoles" : {
"realm-management" : [ "query-users", "manage-users", "view-users" ]
"realm-management" : [ "query-users", "manage-users", "view-users", "view-realm" ]
},
"notBefore" : 0,
"groups" : [ ]
@@ -743,7 +743,7 @@
"consentRequired" : false,
"standardFlowEnabled" : false,
"implicitFlowEnabled" : false,
"directAccessGrantsEnabled" : true,
"directAccessGrantsEnabled" : false,
"serviceAccountsEnabled" : true,
"authorizationServicesEnabled" : true,
"publicClient" : false,
+27 -20
View File
@@ -31,8 +31,10 @@ CLIENT_ID = os.environ.get("KEYCLOAK_CLIENT_ID", "rest-api")
CLIENT_SECRET = os.environ.get(
"KEYCLOAK_CLIENT_SECRET", "ServiceAccountClientSecretForDev"
)
MASTER_ADMIN_USER = os.environ.get("KC_BOOTSTRAP_ADMIN_USERNAME", "admin")
MASTER_ADMIN_PASS = os.environ.get("KC_BOOTSTRAP_ADMIN_PASSWORD", "admin")
MASTER_CLIENT_ID = os.environ.get("KC_BOOTSTRAP_ADMIN_CLIENT_ID", "bootstrap-admin")
MASTER_CLIENT_SECRET = os.environ.get(
"KC_BOOTSTRAP_ADMIN_CLIENT_SECRET", "BootstrapAdminClientSecretForDev"
)
ENDPOINT_PATH = f"/realms/{TARGET_REALM}/bulk-role-membership/check"
ENDPOINT_URL = f"{KEYCLOAK_URL}{ENDPOINT_PATH}"
@@ -52,7 +54,7 @@ def _post_with_token(token, body, *, raw_data=None, content_type="application/js
)
def _password_token(realm, client_id, username, password, *, client_secret=None):
def _password_token(realm, client_id, username, password):
"""Fetch an access token via the OIDC password grant."""
data = {
"grant_type": "password",
@@ -60,8 +62,6 @@ def _password_token(realm, client_id, username, password, *, client_secret=None)
"username": username,
"password": password,
}
if client_secret is not None:
data["client_secret"] = client_secret
response = requests.post(
f"{KEYCLOAK_URL}/realms/{realm}/protocol/openid-connect/token",
data=data,
@@ -91,12 +91,27 @@ def main() -> int:
client_id=CLIENT_ID,
client_secret_key=CLIENT_SECRET,
)
admin_token_payload = openid.token(grant_type="client_credentials")
admin_token = admin_token_payload["access_token"]
admin_token = openid.token(grant_type="client_credentials")["access_token"]
# Master rights come from the bootstrap admin service account, not the
# bootstrap admin user: that user has an empty profile, so Keycloak asks
# it to fill one in and refuses the password grant.
master_openid = KeycloakOpenID(
server_url=KEYCLOAK_URL,
realm_name="master",
client_id=MASTER_CLIENT_ID,
client_secret_key=MASTER_CLIENT_SECRET,
)
master_token_payload = master_openid.token(grant_type="client_credentials")
master_admin_token = master_token_payload["access_token"]
# The fixtures create realm roles and users, which needs more than the
# three user roles the rest-api service account holds. That token stays
# for the endpoint calls below.
admin = KeycloakAdmin(
server_url=KEYCLOAK_URL,
realm_name=TARGET_REALM,
token=admin_token_payload,
token=master_token_payload,
verify=False,
)
@@ -139,16 +154,10 @@ def main() -> int:
],
}
)
# Take this token through admin-cli, the built-in public client, so
# the rest-api client keeps its direct-access grant switched off.
lowpriv_token = _password_token(
TARGET_REALM,
CLIENT_ID,
lowpriv_username,
lowpriv_password,
client_secret=CLIENT_SECRET,
)
master_admin_token = _password_token(
"master", "admin-cli", MASTER_ADMIN_USER, MASTER_ADMIN_PASS
TARGET_REALM, "admin-cli", lowpriv_username, lowpriv_password
)
print("Fixture ready: roles, users, low-priv token, master token")
@@ -284,10 +293,8 @@ def main() -> int:
# rejects this even if Keycloak's per-realm getRoleById ever leaked.
master_admin_client = KeycloakAdmin(
server_url=KEYCLOAK_URL,
username=MASTER_ADMIN_USER,
password=MASTER_ADMIN_PASS,
realm_name="master",
user_realm_name="master",
token=master_token_payload,
verify=False,
)
master_admin_role_id = master_admin_client.get_realm_role("admin")["id"]