Users from LDAP cannot login on Portal

Issue Description

when a user tries to login on the portal an additional login mask pops up, but does not work.

I have one user besides the default admin user which works. The difference is, that this user was first logged in version 16.1, and there is an entry for “Authentication Credentials” with “Password for authentication to account” in the user record.

All other users were created with versions greater 16.1 an miss this field.

Creation of that entry is not possible, it gave the error:

“Cannot set credentials for accounts in an external directory.”

also I can’t see LDAP traffic in this case.

Login on imap is of course possible for that user(s), just the portal does not work

Expected Behavior

User should be able to login

Actual Behavior

the following error pops up:

Token exchange failed: 400 Bad Request

Back to login

Stalwart Version

v0.16.x

Installation Method

Binary (Linux)

Database Backend

MySQL

Blob Storage

Filesystem

Search Engine

Internal

Directory Backend

LDAP

I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.

on

I have searched this support forum (open and closed topics) and confirm this is not a duplicate.

on

I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.

on

Let me research this issue with LDAP users and portal login.

Based on my research, I can identify two likely causes for this issue.

The portal login uses OAuth/token exchange, which is a different authentication flow from IMAP. For LDAP users, the portal login works through bind authentication (since credentials can’t be stored locally for external directory accounts, as you saw with the “Cannot set credentials for accounts in an external directory” error). Here are the two most probable causes:

1. The filterLogin filter doesn’t match your users

During portal login, Stalwart runs a filterLogin LDAP search to find the user’s DN before attempting a bind. The default filter is (&(objectClass=inetOrgPerson)(mail=?)). If your LDAP users have a different object class (e.g., posixAccount) or if they use a uid attribute rather than mail as login name, the search returns nothing and the token exchange fails with 400. IMAP works because many clients send the password directly and the bind path differs.

Check your LDAP directory settings in the WebUI under Settings > Authentication > Directories and verify that filterLogin matches your actual LDAP schema. For example, for posixAccount users by uid:

{
  "@type": "Ldap",
  "filterLogin": "(&(|(objectClass=posixAccount)(objectClass=inetOrgPerson))(uid=?))"
}

2. The attrSecretChanged attribute is missing or unrecognised

In bind-authentication mode (default: bindAuthentication: true), Stalwart reads the attrSecretChanged attribute (default: ["pwdChangeTime"]) to manage OAuth token validity. If that attribute doesn’t exist on your LDAP user entries, the token exchange may also fail. You can either ensure that attribute is present in your directory, or configure attrSecretChanged to point to an attribute that your LDAP server does populate.

Recommended steps:

  1. Enable debug logging and watch for LDAP-related events (auth.failed, etc.) during a portal login attempt to pinpoint exactly where it fails.
  2. Confirm your filterLogin filter actually returns results for the affected users by testing it directly against your LDAP server (e.g., with ldapsearch).
  3. Adjust the filterLogin (and optionally attrSecretChanged) in your Directory configuration to match your LDAP schema.

Docs reference: LDAP Authentication and the Directory object reference.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong. If you found it useful, mention @helpbot in any reply to ask follow-up questions and the bot will respond again.

Ahh, as I have not attribute for changed secrets currently (i will look into implementing it), the trick was to use instead the “modifyTimestamp” Attribute instead

Good find! Using modifyTimestamp as the attrSecretChanged value is a practical workaround when your LDAP directory doesn’t populate pwdChangeTime. It’s a standard operational attribute that virtually all LDAP servers (OpenLDAP, Active Directory, etc.) expose on every entry, so it will always be present and Stalwart can use it to track when an entry was last modified and manage OAuth token validity accordingly.

To configure it, set attrSecretChanged to ["modifyTimestamp"] in your LDAP Directory object (Settings > Authentication > Directories in the WebUI).

Just be aware that modifyTimestamp updates on any modification to the LDAP entry, not only password changes, so users may occasionally get their OAuth tokens invalidated when unrelated attributes are updated. For tighter behaviour, implementing pwdChangeTime (or an equivalent password-change tracking attribute) in your LDAP directory is the better long-term solution.

Docs reference: LDAP Authentication - Bind authentication and the attrSecretChanged field reference.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong. If you found it useful, mention @helpbot in any reply to ask follow-up questions and the bot will respond again.