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

DateChange
2026-07-17Initial 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 matrix-docker-ansible-deploy project currently warns that newer clients such as Element X support SSO only through Matrix Authentication Service (MAS), not through legacy Synapse OIDC. The direct Synapse-to-Authelia setup in this article is suitable only if every required client supports Synapse’s legacy SSO flow, such as the compatible Element Web/Desktop path you have tested.

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:

  1. Traefik v3 and CrowdSec with Docker Compose
  2. Authelia with Docker Compose and Traefik
  3. 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-deploy in /opt/containers/matrix;
  • the existing Matrix password login remains enabled during migration.

The integration has four independent identifiers. Do not conflate them:

ValueExamplePurpose
Matrix localpartyour-usernameLeft side of the existing Matrix ID.
OIDC preferred_usernameyour-usernameCandidate localpart used only when linking/creating the Matrix user.
OIDC subopaque stable valuePermanent external identity key stored by Synapse.
OIDC provider IDautheliaBecomes 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:

  1. Confirm the exact Matrix ID in the currently logged-in client.
  2. Secure the Matrix recovery key or recovery phrase.
  3. Keep at least one verified, working Element session.
  4. Review the client/device list and record which sessions are yours.
  5. Create consistent backups of Synapse’s database and configuration.
  6. Back up the Authelia configuration, database, and secrets as one set.
  7. 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_KEY

Replace 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 rfc3986

The 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_SECRET

The 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 client_secret_basic, while this integration does not use PKCE. Do not generalize this setting to public browser or mobile OIDC clients.

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 authelia

Check the discovery endpoint:

curl -fsS https://auth.your-domain.com/.well-known/openid-configuration | jq .issuer

It 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.yml

Add 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: false

user_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

allow_existing_users: true permits an OIDC identity whose mapped localpart matches a pre-existing Matrix user to bind to that account. This is necessary for the migration, but it also means control of Authelia’s usernames and claims is security-critical. Restrict the client to synapse-users, keep public Matrix registration disabled, and do not let users claim another person’s preferred_username.

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,start

At 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:

  1. Open the compatible Element Web login page.
  2. Choose the Authelia SSO option.
  3. Complete the password and second factor.
  4. Check the Matrix ID after login.
  5. Confirm it is exactly @your-username:your-domain.com.
  6. Confirm the expected joined rooms and account data are present.
  7. 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 enable_registration: false, an unknown mapping should fail instead of creating a new Matrix user. If automatic OIDC registration was enabled elsewhere and a mismatch created a separate account, it still did not erase the original one. A lockout happens only if you then disable the original login route or discard its remaining sessions. Correct the mapping and retry while the original account is still accessible.

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_TOKEN

The 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 INSERT INTO user_external_ids ... as the normal migration method. Synapse provides a supported User Admin API for external IDs. If manual pre-binding is genuinely necessary, use that API, preserve every existing account field and external ID from a preceding GET, and take a database backup first. A partial PUT can replace array-valued fields, so this is a break-glass procedure, not a copy-and-paste shortcut.

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:

  1. Review every Matrix device/session.
  2. Revoke unknown sessions immediately.
  3. Retire old sessions you no longer need.
  4. Keep enough verified sessions or recovery material to bootstrap new E2EE clients.
  5. 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-authelia on 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: false

Apply and test again:

cd /opt/containers/matrix
sudo ansible-playbook -i inventory/hosts setup.yml --tags=install-all,start

Confirm that:

  1. Authelia SSO still reaches the existing account.
  2. The second factor is required in a genuinely new Authelia session.
  3. Direct Matrix password login is rejected.
  4. 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:

  1. Set matrix_synapse_password_config_enabled: true again.
  2. Re-apply the playbook.
  3. Use the retained verified session or local password to regain the original account.
  4. Do not delete the OIDC external-ID binding unless you have identified a mapping error.
  5. 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 authelia

If 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 WebAuthn

This 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.

📚OFFICIAL AUTHELIA + SYNAPSE GUIDE 🔐SYNAPSE OIDC DOCUMENTATION 🧩MATRIX PLAYBOOK OIDC NOTES ↩️GENERAL AUTHELIA GUIDE