mirror of
https://github.com/suitenumerique/messages.git
synced 2026-09-29 13:14:59 +02:00
🔒️(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:
@@ -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
|
||||
|
||||
Vendored
+3
-1
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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 |
|
||||
|
||||
@@ -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).
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user