Adding Authelia 2FA to Matrix Synapse with OpenID Connect
Table of Contents 📑
- Changelog
- 1. Read This Compatibility Gate First
- 2. Prerequisites and Scope
- 3. Protect the Existing Matrix Account Before Migration
- 4. Add the Matrix Group to Authelia
- 5. Generate OIDC Keys and the Client Secret
- 6. Enable Authelia’s OIDC Provider
- 7. Configure Synapse OIDC Without Disabling Passwords
- 8. Prove the Existing-Account Mapping
- 9. Review Sessions Without Losing E2EE Recovery
- 10. Verify Bots and Automation
- 11. Optional: Disable Matrix Password Login
- 12. Rollback
- 13. What This Setup Protects
- Conclusion
Synapse does not provide native TOTP login for local Matrix passwords. It can, however, delegate interactive login to an OpenID Connect Provider. This guide connects Authelia to Synapse so a human Matrix login can require an Authelia password plus TOTP or WebAuthn.
The difficult part is not the redirect. It is preserving the identity of an existing Matrix user. If automatic OIDC registration remains enabled, a careless first login can create a second account; a premature password shutdown can then lock you out of the original account. The migration below disables new OIDC registration and keeps the old login available until the mapping is proven.
Changelog
| Date | Change |
|---|---|
| 2026-07-17 | Initial Version: Added Authelia OIDC, Element X compatibility warning, safe existing-account linking, E2EE-aware session cleanup, bot-token checks, rollback, and password-login cutover. |
1. Read This Compatibility Gate First
| ⚠️ ELEMENT X REQUIRES A DIFFERENT ARCHITECTURE |
The If Element X is mandatory, stop here. Keep password login for now or design MAS with Authelia as its upstream OIDC Provider. Do not discover the incompatibility after disabling passwords. |
This is the most important correction to the seemingly simple “Authelia → Synapse → disable passwords” plan. Authelia itself is not the compatibility problem; the client-facing Matrix authentication architecture is.
2. Prerequisites and Scope
Complete these guides first:
- Traefik v3 and CrowdSec with Docker Compose
- Authelia with Docker Compose and Traefik
- Deploying a Matrix Synapse Server with Docker and Traefik
This article assumes:
- Authelia is available at
https://auth.your-domain.com; - Synapse is available at
https://matrix.your-domain.com; - the Matrix ID to preserve is
@your-username:your-domain.com; - the Authelia username is exactly
your-username; - Synapse is managed by
matrix-docker-ansible-deployin/opt/containers/matrix; - the existing Matrix password login remains enabled during migration.
The integration has four independent identifiers. Do not conflate them:
| Value | Example | Purpose |
|---|---|---|
| Matrix localpart | your-username | Left side of the existing Matrix ID. |
OIDC preferred_username | your-username | Candidate localpart used only when linking/creating the Matrix user. |
OIDC sub | opaque stable value | Permanent external identity key stored by Synapse. |
| OIDC provider ID | authelia | Becomes oidc-authelia in Synapse’s external-ID Admin API. |
The sub claim does not become the Matrix username. It is the opaque anchor that Synapse binds to the account after mapping. The mapping template uses preferred_username to find the existing localpart.
3. Protect the Existing Matrix Account Before Migration
Before changing authentication:
- Confirm the exact Matrix ID in the currently logged-in client.
- Secure the Matrix recovery key or recovery phrase.
- Keep at least one verified, working Element session.
- Review the client/device list and record which sessions are yours.
- Create consistent backups of Synapse’s database and configuration.
- Back up the Authelia configuration, database, and secrets as one set.
- Test the required Matrix clients against a non-critical account if possible.
| ⚠️ DO NOT LOG OUT EVERY E2EE SESSION YET |
Matrix access tokens issued before 2FA remain valid until revoked, so they must eventually be reviewed. But blindly logging out every device can also remove the easiest route to verify a new session and recover encrypted history. First prove that account recovery and the OIDC login work; then revoke unknown or deliberately retired sessions. |
4. Add the Matrix Group to Authelia
Edit /opt/containers/authelia/config/users_database.yml and add a dedicated group to every human allowed to log in to Matrix:
users:
your-username:
disabled: false
displayname: 'Your Name'
password: '$argon2id$v=19$m=65536,t=3,p=4$YOUR-EXISTING-DIGEST'
email: 'you@your-domain.com'
groups:
- 'admins'
- 'synapse-users'Synapse will require the synapse-users claim, so an authenticated Authelia user outside this group still cannot use this OIDC client.
5. Generate OIDC Keys and the Client Secret
Authelia needs an HMAC secret and at least one RSA signing key. Create them as protected files:
cd /opt/containers/authelia
sudo install -o 1000 -g 1000 -m 600 /dev/null secrets/OIDC_HMAC_SECRET
openssl rand -hex 64 | sudo tee secrets/OIDC_HMAC_SECRET >/dev/null
sudo openssl genrsa -out secrets/OIDC_JWKS_RSA_PRIVATE_KEY 4096
sudo chown 1000:1000 secrets/OIDC_JWKS_RSA_PRIVATE_KEY
sudo chmod 600 secrets/OIDC_JWKS_RSA_PRIVATE_KEYReplace 1000:1000 if the Authelia stack uses different PUID and PGID values.
Generate a random RFC 3986-safe plaintext client secret and its PBKDF2-SHA512 digest:
docker run --rm authelia/authelia:4.39.20 \
authelia crypto hash generate pbkdf2 \
--variant sha512 \
--random \
--random.length 72 \
--random.charset rfc3986The command prints two different values:
- Random Password: the plaintext secret, stored on the Synapse side as
client_secret; - Digest: the
$pbkdf2-sha512$...value, stored in Authelia’s client definition.
Do not swap them. Save the plaintext directly in your secret manager; do not paste it into a chat, issue, or Git repository.
6. Enable Authelia’s OIDC Provider
6.1. Enable the Configuration Template Filter
The official Authelia documentation recommends reading the private signing key from a file through its configuration template filter. Add these variables to the Authelia service in /opt/containers/authelia/compose.yml:
environment:
# Keep all variables from the base Authelia guide.
X_AUTHELIA_CONFIG_FILTERS: 'template'
AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE: /secrets/OIDC_HMAC_SECRETThe existing read-only ./secrets:/secrets:ro mount makes both OIDC files available without baking them into the image.
6.2. Add the Provider and Synapse Client
Append this block to /opt/containers/authelia/config/configuration.yml. Replace the client digest with the Digest from the previous section:
identity_providers:
oidc:
jwks:
- algorithm: 'RS256'
use: 'sig'
key: {{ secret "/secrets/OIDC_JWKS_RSA_PRIVATE_KEY" | mindent 10 "|" | msquote }}
clients:
- client_id: 'synapse'
client_name: 'Synapse'
client_secret: '$pbkdf2-sha512$REPLACE-WITH-THE-DIGEST'
public: false
authorization_policy: 'two_factor'
require_pkce: false
pkce_challenge_method: ''
redirect_uris:
- 'https://matrix.your-domain.com/_synapse/client/oidc/callback'
scopes:
- 'openid'
- 'profile'
- 'email'
- 'groups'
response_types:
- 'code'
grant_types:
- 'authorization_code'
access_token_signed_response_alg: 'none'
userinfo_signed_response_alg: 'none'
token_endpoint_auth_method: 'client_secret_basic'authorization_policy: 'two_factor' is the hard Authelia policy for this OIDC client. A logged-in one-factor Authelia session alone is not sufficient to authorize Synapse.
| ℹ️ WHY PKCE IS DISABLED FOR THIS CLIENT |
This follows the current official Authelia/Synapse integration profile: Synapse is a confidential client authenticating with |
Validate and restart Authelia:
cd /opt/containers/authelia
docker compose config --quiet
docker compose up -d
docker compose exec authelia authelia config validate --config /config/configuration.yml
docker compose logs --tail=150 autheliaCheck the discovery endpoint:
curl -fsS https://auth.your-domain.com/.well-known/openid-configuration | jq .issuerIt must report the exact HTTPS issuer URL used in the Synapse configuration.
7. Configure Synapse OIDC Without Disabling Passwords
Edit:
/opt/containers/matrix/inventory/host_vars/matrix.your-domain.com/vars.ymlAdd the following variables. Replace the client secret with the Random Password generated in section 5:
matrix_synapse_oidc_enabled: true
matrix_synapse_oidc_providers:
- idp_id: 'authelia'
idp_name: 'Authelia'
discover: true
issuer: 'https://auth.your-domain.com'
client_id: 'synapse'
client_secret: 'REPLACE-WITH-THE-RANDOM-PASSWORD'
scopes:
- 'openid'
- 'profile'
- 'email'
- 'groups'
allow_existing_users: true
enable_registration: false
user_profile_method: 'userinfo_endpoint'
user_mapping_provider:
config:
subject_template: "{{ user.sub }}"
localpart_template: "{{ user.preferred_username }}"
display_name_template: "{{ user.name }}"
email_template: "{{ user.email }}"
attribute_requirements:
- attribute: 'groups'
value: 'synapse-users'
# Keep local password login enabled throughout the migration.
matrix_synapse_password_config_enabled: true
# Keep normal Matrix registration disabled as a second, global control.
matrix_synapse_enable_registration: falseuser_profile_method: 'userinfo_endpoint' is the current Authelia-documented compatibility escape hatch for Synapse’s claim handling. The provider-level enable_registration: false is distinct from the global Matrix setting and makes this OIDC flow sign-in-only.
| ⚠️ ALLOW_EXISTING_USERS IS POWERFUL |
|
Apply the playbook using the same Ansible environment and conventions as the main Matrix guide:
cd /opt/containers/matrix
sudo ansible-playbook -i inventory/hosts setup.yml --tags=install-all,startAt this point both OIDC and the original password login should be available.
8. Prove the Existing-Account Mapping
8.1. Test in a Separate Browser Session
Keep the current verified Matrix client open. In a private browser window:
- Open the compatible Element Web login page.
- Choose the Authelia SSO option.
- Complete the password and second factor.
- Check the Matrix ID after login.
- Confirm it is exactly
@your-username:your-domain.com. - Confirm the expected joined rooms and account data are present.
- Verify the new E2EE session from the existing trusted session or recovery material.
Seeing an empty room list or a different Matrix ID means the mapping did not land on the existing account. Stop immediately and keep password login enabled.
| ℹ️ A WRONG MAPPING DOES NOT DELETE THE OLD ACCOUNT |
With this guide’s provider-level |
8.2. Verify the External ID with Synapse’s Admin API
Use a Synapse administrator token without writing it into shell history:
read -rsp 'Synapse admin token: ' SYNAPSE_ADMIN_TOKEN
echo
curl -fsS \
-H "Authorization: Bearer ${SYNAPSE_ADMIN_TOKEN}" \
'https://matrix.your-domain.com/_synapse/admin/v2/users/%40your-username%3Ayour-domain.com' \
| jq '{name, external_ids}'
unset SYNAPSE_ADMIN_TOKENThe result should show the original Matrix ID and an external entry resembling:
{
"auth_provider": "oidc-authelia",
"external_id": "an-opaque-stable-subject"
}The external_id is Authelia’s sub, not the username.
| ⚠️ DO NOT PATCH THE SYNAPSE DATABASE WITH SQL |
Do not use an ad-hoc |
9. Review Sessions Without Losing E2EE Recovery
OIDC 2FA protects new interactive logins. It does not retroactively add a second factor to existing Matrix access tokens.
After the mapping and E2EE recovery path are proven:
- Review every Matrix device/session.
- Revoke unknown sessions immediately.
- Retire old sessions you no longer need.
- Keep enough verified sessions or recovery material to bootstrap new E2EE clients.
- If you require a strict clean cutover, revoke all pre-2FA human sessions only after the new OIDC session is verified and recovery has been tested.
“Log out all other devices” is therefore a policy choice, not an automatic prerequisite. It closes old-token bypasses but must be sequenced around encrypted-session recovery.
10. Verify Bots and Automation
Before disabling password login, verify that Hermes bots, bridges, and other automations use existing Matrix access tokens rather than logging in with a username and password.
Disabling password_config.enabled prevents new password logins; it does not invalidate already-issued access tokens. Token-authenticated bots should continue to work. However, if a bot token is later lost or revoked, the bot cannot obtain a replacement through password login while the feature is disabled. Document a recovery path, such as temporarily re-enabling password login under controlled conditions.
Test each automation before and after the cutover:
- receive an allowed Matrix message;
- send a reply;
- restart the bot and confirm it reconnects with the stored token;
- confirm Matrix user and room allowlists remain enforced.
11. Optional: Disable Matrix Password Login
Only continue after all of the following are true:
- every required client is compatible with legacy Synapse OIDC;
- the OIDC login reaches the exact old Matrix ID;
- the Admin API shows
oidc-autheliaon that account; - E2EE recovery and session verification work;
- bot and bridge token authentication has been tested;
- a current rollback backup exists.
Then change the playbook variable:
matrix_synapse_password_config_enabled: falseApply and test again:
cd /opt/containers/matrix
sudo ansible-playbook -i inventory/hosts setup.yml --tags=install-all,startConfirm that:
- Authelia SSO still reaches the existing account.
- The second factor is required in a genuinely new Authelia session.
- Direct Matrix password login is rejected.
- Hermes and other token-based integrations still work after restart.
| 📝 YOU MAY KEEP PASSWORD LOGIN AS A RECOVERY ROUTE |
Running OIDC and local passwords in parallel is a valid compatibility phase, but the local password remains a 2FA bypass for new logins. If you keep it, treat it as an explicit risk decision: use a long unique password, tightly control who has one, monitor sessions, and periodically retest the OIDC path. |
12. Rollback
If the cutover fails:
- Set
matrix_synapse_password_config_enabled: trueagain. - Re-apply the playbook.
- Use the retained verified session or local password to regain the original account.
- Do not delete the OIDC external-ID binding unless you have identified a mapping error.
- Inspect Synapse and Authelia logs before retrying.
sudo journalctl -u matrix-synapse.service --since '30 minutes ago'
cd /opt/containers/authelia
docker compose logs --since=30m autheliaIf a separate account was accidentally created, deactivate it only after confirming that it is not the original account and contains no data you need.
13. What This Setup Protects
The final login path is:
Element Web/Desktop
│
▼
Synapse ──OIDC redirect──> Authelia
▲ │
│ ├─ password
└──── authorization code ─└─ TOTP or WebAuthnThis protects new human SSO logins against password-only compromise. It does not make the following disappear:
- already-issued Matrix access tokens;
- a compromised verified Element session;
- loss of the Matrix recovery key;
- an administrator who can change Authelia claims or Synapse mappings;
- a stolen bot token;
- application or host compromise.
That is not a flaw in OIDC; it is the normal boundary between authentication, sessions, authorization, and host security.
Conclusion
Authelia can add real TOTP or WebAuthn enforcement to compatible Synapse SSO logins without replacing the existing Matrix account. The safe sequence is what matters: preserve E2EE recovery, enable OIDC alongside passwords, map preferred_username to the exact existing localpart, verify the persistent sub binding, review old tokens, test bots, and only then decide whether password login should disappear.
For Element X, use the compatibility gate rather than forcing this legacy path: evaluate MAS with Authelia upstream or retain a supported login method.
📚 🔐 🧩 ↩️





