LDAP Directory/set rejects documented attrSecretChanged field

Issue Description

I am trying to configure an LDAP directory with bindAuthentication: true through the JMAP Directory/set API.

The LDAP provider exposes the password-change timestamp as pwdChangedTime. Stalwart documents attrSecretChanged as the LDAP directory field used to configure which LDAP attribute should be used as the password-change marker for token invalidation.

However, when creating the LDAP directory through JMAP Directory/set, Stalwart rejects attrSecretChanged with invalidPatch.

This appears to be a mismatch between the documented LDAP Directory configuration and the JMAP Directory/set validation/schema

Expected Behavior

The following LDAP Directory payload should be accepted:

{
@type”: “Ldap”,
“url”: “ldap://ldap.example.internal:389”,
“baseDn”: “dc=example,dc=internal”,
“bindDn”: “cn=ldap-service,ou=users,dc=example,dc=internal”,
“bindAuthentication”: true,
“attrSecretChanged”: [“pwdChangedTime”],
“filterLogin”: “(&(objectClass=person)(mail=?))”,
“filterMailbox”: “(&(objectClass=person)(mail=?))”
}

Expected behavior: Stalwart should persist the LDAP directory and use pwdChangedTime as the password-change marker for LDAP bind authentication.

Relevant documentation: the LDAP Directory object documents attrSecretChanged as the configurable password-change marker attribute, with default [“pwdChangeTime”].

Actual Behavior

Actual Behavior

JMAP Directory/set rejects the payload with:

{
“methodResponses”: [
[
“x:Directory/set”,
{
“accountId”: “d333333”,
“notCreated”: {
“new1”: {
“type”: “invalidPatch”,
“description”: “Invalid value for object property”,
“properties”: [“attrSecretChanged”]
}
}
},
“c1”
]
]
}

The deviation is that attrSecretChanged is documented as a valid LDAP Directory field, but the JMAP API rejects it during directory creation.

Reproduction Steps

  1. Run Stalwart v0.16.7.
  2. Use an LDAP provider that exposes a password-change timestamp attribute named pwdChangedTime.
  3. Create an LDAP directory through JMAP Directory/set.
    Include the field:
    “attrSecretChanged”: [“pwdChangedTime”]
    Submit the request.
  4. Observe that the directory is not created and JMAP returns invalidPatch for attrSecretChanged.

Relevant Log Output

Waiting for Stalwart JMAP at https://stalwart.example.internal:443/jmap … API ready (HTTP 200) [auth-ldap] reconciling LDAP directory for ldap://ldap.example.internal:389 baseDn=dc=example,dc=internal … [auth-ldap] creating LDAP directory … {“methodResponses”:[[“x:Directory/set”,{“accountId”:“d333333”,“notCreated”:{“new1”:{“type”:“invalidPatch”,“description”:“Invalid value for object property”,“properties”:[“attrSecretChanged”]}}},“c1”]],“sessionState”:“ca2c46e8”} [auth-ldap] LDAP directory creation failed

Stalwart Version

v0.16.x

Installation Method

Docker

Database Backend

PostgreSQL

Blob Storage

S3-compatible

Search Engine

Internal

Directory Backend

LDAP

Additional Context

The LDAP backend is an Authentik LDAP outpost. With LDAP bind authentication, Stalwart cannot read the user password hash. Therefore it needs a password-change marker attribute to invalidate OAuth/WebUI tokens after password changes.

Stalwart’s documented default is pwdChangeTime, but Authentik LDAP outpost emits the password-change timestamp as pwdChangedTime. This should be configurable through attrSecretChanged.

Currently, since JMAP rejects attrSecretChanged, the only workaround is to make the LDAP provider emit a compatibility alias named pwdChangeTime. It would be cleaner if Stalwart accepted the documented attrSecretChanged field through JMAP Directory/set.

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

Let me investigate this by checking the JMAP Directory/set schema and the LDAP directory configuration in the source code.

attrSecretChanged is a valid Directory/set property; the rejection is a payload-shape problem, not a missing field. All the attr* fields are maps, and over the JMAP API they’re sent as objects, not arrays. So instead of [“pwdChangedTime”] use:

  "attrSecretChanged": {"pwdChangedTime": true}

(same for attrClass, attrEmail, attrMemberOf, etc.). Your reading of the default (pwdChangeTime) and your Authentik pwdChangedTime are both fine once sent in object form. The docs showing the array form for these fields are wrong for the JMAP API, I’ll get them corrected.