Matrix homeserver (Synapse and MAS)
Matrix clients such as Element sign in to the Matrix network via your homeserver, not directly via LuxID. The homeserver (or its authentication service) acts as the OIDC client; LuxID acts as the OIDC provider. Users authenticating via LuxID receive a Matrix account on your homeserver.
There are two ways a Matrix deployment can talk to an upstream OIDC provider like LuxID:
- Synapse built-in OIDC - configured directly in
homeserver.yamlunderoidc_providers. This is the original mechanism and still works, but it is now considered the legacy path. - Matrix Authentication Service (MAS) - the next-generation, standalone authentication component for Matrix. MAS handles all authentication for the homeserver and brokers to upstream providers under
upstream_oauth2. This is the recommended path for new deployments and the direction the Matrix project is moving towards.
Pick the tab that matches your deployment. From LuxID's perspective both are just registered OIDC clients holding a Client ID and Client Secret and presenting a registered Redirect URI.
- Synapse (built-in OIDC)
- MAS (Matrix Authentication Service)
The Synapse homeserver acts as the OIDC client directly. Users authenticating via LuxID receive a Matrix account on your homeserver.
Configure homeserver.yaml
Add or extend the oidc_providers list in your homeserver.yaml:
oidc_providers:
- idp_id: luxid
idp_name: "Sign in with LuxID"
discover: true
issuer: "https://login.luxid.lu"
client_id: "<your Client ID from your LuxID registration>"
client_secret: "<your Client Secret from your LuxID registration>"
scopes: ["openid", "profile", "email"]
user_mapping_provider:
config:
localpart_template: "{{ user.sub }}"
display_name_template: "{{ user.given_name }} {{ user.family_name }}"
email_template: "{{ user.email }}"
With discover: true, Synapse retrieves all endpoint URLs from https://login.luxid.lu/.well-known/openid-configuration. You do not need to specify authorization_endpoint, token_endpoint, or userinfo_endpoint separately.
After editing homeserver.yaml, the Synapse redirect URI will be https://<your-homeserver>/_synapse/client/oidc/callback. Register this URI with LuxID as an allowed Redirect URI before restarting Synapse.
Restart Synapse and test
Restart Synapse:
systemctl restart matrix-synapse
Open the Element (or any Matrix client) login screen pointing at your homeserver. A "Sign in with LuxID" button should appear alongside the standard username/password form. Click it to complete the test.
Synapse production checklist
- Replace the
login-uat.luxid.luissuer withlogin.luxid.lu(or remove it and rely ondiscover: truewith the production issuer). - Register the production Synapse callback URL with LuxID.
- Rotate the Client Secret from UAT.
- Confirm
discover: trueresolves to the production discovery document. - Review your Matrix server's
allow_guest_accessandregistration_shared_secretsettings - adding an OIDC provider does not automatically disable local password registration.
The Matrix Authentication Service (MAS) is a standalone service that handles all authentication for your homeserver. In MAS terminology, LuxID is an upstream OAuth 2.0 provider: clients authenticate to MAS ("downstream"), and MAS in turn brokers to LuxID ("upstream"). The flow to LuxID is a standard OIDC authorization code flow with PKCE.
Configure the upstream provider
Add LuxID to the upstream_oauth2.providers list in your MAS configuration:
upstream_oauth2:
providers:
- id: "01ARZ3NDEKTSV4RRFFQ69G5FAV" # a ULID you generate; it becomes part of the callback URL
human_name: "LuxID"
issuer: https://login-uat.luxid.lu # use https://login.luxid.lu in production
client_id: "<your Client ID from your LuxID registration>"
client_secret: "<your Client Secret from your LuxID registration>"
token_endpoint_auth_method: client_secret_post
pkce_method: always
scope: "openid profile"
claims_imports:
localpart:
action: require
template: "{{ user.sub }}"
displayname:
action: suggest # set to "force" if LuxID should be authoritative for the display name
template: "{{ user.name }}"
email:
action: ignore # set to "suggest"/"require" only if email is in your Claim Template
The claims_imports actions control how each upstream claim maps onto the Matrix user:
| Action | Meaning |
|---|---|
require | The claim must be present; provisioning fails if it is missing. Use for localpart. |
suggest | Pre-fill the value but let the user change it during account creation. |
force | Always set the value from the claim; the user cannot override it. |
ignore | Do not import the claim. Use ignore for email when email is not in your Claim Template. |
email is set to ignoreLuxID releases only the claims declared in your Application's Claim Template, regardless of the scope your client sends - LuxID ignores the runtime scope parameter as a privacy guardrail. If email is not in your Claim Template, it will not appear in the token, so importing it would fail. Keep email: ignore unless email is part of your agreed Claim Template. See Configure LuxID - available claims.
How MAS builds the callback URL
You will notice there is no explicit callback URL in the MAS configuration. MAS derives it automatically from the provider id (a ULID), in the form:
https://<your-mas-host>/upstream/callback/<provider-id>
For the example above on a host mas.example.org, the callback would be https://mas.example.org/upstream/callback/01ARZ3NDEKTSV4RRFFQ69G5FAV. Register this exact URL with LuxID as an allowed Redirect URI before testing. Because the provider id is the routing key, changing it changes the callback URL - keep it stable once registered.
The full sequence:
Restart MAS and test
Restart MAS so it picks up the new provider, then open the Element (or any Matrix client) login screen pointing at your homeserver. A "Continue with LuxID" option should appear. Complete the flow and confirm a Matrix account is provisioned with the expected localpart and display name.
MAS production checklist
- Change
issuerfromhttps://login-uat.luxid.lutohttps://login.luxid.lu. - Register the production callback URL (
https://<your-mas-host>/upstream/callback/<provider-id>, same providerid, production host) with LuxID. - Rotate the Client Secret from UAT.
- Confirm
pkce_method: alwaysand thattoken_endpoint_auth_methodmatches what was agreed with LuxID (client_secret_postin the example). - Decide the
displaynameaction deliberately (suggestvsforce) before provisioning real users.
Matrix-specific considerations
These apply regardless of whether you use Synapse built-in OIDC or MAS, because both provision Matrix IDs the same way.
Localpart and MXID: The localpart template controls the local part of the user's Matrix ID (@localpart:your.server). Using {{ user.sub }} (the LuxID sub claim) is safe and stable because sub is permanent and unique per user per application. Do not use {{ user.email }} as the localpart - email addresses can change, but Matrix IDs are permanent once created.
User de-duplication on re-login: The homeserver maps OIDC subjects to Matrix accounts on first login. On subsequent logins it recognises the sub claim and re-uses the existing Matrix account rather than creating a new one. This works correctly as long as the provider identifier (idp_id in Synapse, the provider id in MAS) and the sub value remain stable. Do not change the provider identifier after accounts have been provisioned.
Irreversibility of localparts: Matrix IDs cannot be renamed or reassigned. If you change the localpart template, existing users' Matrix IDs are not updated. Plan your localpart strategy carefully before provisioning real users.
Using a pre-computed localpart claim
If sub is not the localpart you want - for example, you need a deterministic, human-meaningful Matrix localpart computed on LuxID's side - LuxID can release a custom, partner-specific claim for this purpose. Custom claims are declared in your Application's Claim Template and reviewed by LuxID like any other claim; once released, you reference the claim name in your localpart template (Synapse localpart_template or MAS claims_imports.localpart.template), for example "{{ user.your_custom_localpart_claim }}". Request a custom claim via LuxID - it is not part of the standard OIDC claim set. See Configure LuxID - available claims for the standard set.
Registering and coexisting callbacks
LuxID validates the redirect_uri against an allowlist of registered URIs (exact match) - the URI is sent by your client at authorization time, not fixed on LuxID's side. A practical consequence: you can register more than one callback at the same time. This is what makes a zero-downtime migration from Synapse built-in OIDC to MAS possible:
- Keep the existing Synapse callback (
https://<your-homeserver>/_synapse/client/oidc/callback) registered. - Add the new MAS callback (
https://<your-mas-host>/upstream/callback/<provider-id>) alongside it. - Both remain active in parallel. As long as the
redirect_urisent is in the allowlist, either path works - cut over to MAS at your own pace. - Once MAS is validated in production, ask LuxID to remove the old Synapse callback.
To add or remove redirect URIs, contact LuxID. For the full rules on exact-match validation, HTTPS, and ports, see Redirect URIs and domains.