---
title: "Active Directory and LDAP sign-in, directory synchronisation and OAuth2 bearer tokens"
url: https://www.hmailserver.co.uk/topic/28263/active-directory-and-ldap-sign-in-directory-synchronisation-and-oauth2-bearer-tokens
author: "Progressiverobot"
published: 2026-09-27T19:45:13.731Z
updated: 2026-09-27T19:45:13.731Z
category: "REST API & integrations"
tags: ["security","administration","windows","linux","control-panel"]
site: hMailServer Community Forum
---

# Active Directory and LDAP sign-in, directory synchronisation and OAuth2 bearer tokens

*Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) changes three things this guide relies on. A Control Panel signed in to a server on another computer no longer reads or writes its own computer's `hMailServer.ini` for the `[LDAP]` values, which it cannot read from the server, where it showed that file or the defaults as the server's; the sidebar is regrouped, and Ctrl+K takes every old page title to its page. With SMTP logging on, 6.3.6 and every earlier version that offered them wrote the bearer token of `AUTH XOAUTH2` and `AUTH OAUTHBEARER` and the lines of SCRAM-SHA-256, OAUTHBEARER and GSSAPI into the SMTP log in the clear; 6.3.7 hides every line of a SASL exchange but the user name of `AUTH LOGIN` and the authentication identity of `AUTH PLAIN`. An account given the `serveradmin` role with a list of domains is, over COM as over REST, confined to those domains and no longer the server's administrator, so a script that runs the directory synchronisation's preview and apply as such an account needs the role given with no list. See [the 6.3.7 announcement](/topic/28277). hMailServer 6.3.8 (2 November 2026) signs a name in to an account only when it is the account's stored address, compared in lower case after a domain alias and the default domain, for the name an OAuth2 bearer token carries as for a password (work item 104), so a name that differs from the stored address in more than the case of the letters A to Z, or has white space at either end, may no longer sign in. On Windows an `OAuth2PublicKeyFile` whose path holds characters outside ASCII may be refused with `HM7536` (one outside the code page, which worked up to 6.3.7, is refused), and then the key verifies no token: give the file, and the folders above it, an ASCII name before upgrading. See [the 6.3.8 announcement](/topic/28278).*



This topic covers three ways an organisation's directory or identity provider vouches for somebody on hMailServer 6.3.4's mail protocols: checking a password against Active Directory or another LDAP directory, creating mailboxes from that directory, and accepting OAuth2 bearer tokens. **Know this first:** 6.3.4 keeps settings in the database, but the `[LDAP]` section stays in `hMailServer.ini` on each server, with no REST route and no environment name; the server re-reads it within two seconds of a change, with no restart. The OAuth2 keys are ordinary `[Settings]` keys, stored like any other.

### Three mechanisms, three questions

| Mechanism | The question it answers | Control Panel page (Windows) |
|---|---|---|
| LDAP directory authentication | Is this the password of the directory user the account is linked to? | **Access & abuse protection → Directory authentication (LDAP)** |
| Directory synchronisation | Which mailboxes should exist, according to the directory? | **Access & abuse protection → Directory synchronisation** |
| OAuth2 bearer tokens | Did the identity provider sign this token, and which mailbox does it name? | **Access & abuse protection → Authentication** |

All three are off by default, and an upgrade switches none of them on: the `[LDAP]` section is absent until you add it, and `OAuth2Enabled` defaults to 0.

### Where the `[LDAP]` section lives

- **Windows.** The Control Panel's page writes the `hMailServer.INI` of its own machine. Connected to another machine, its Save is disabled, although "the connection test still works with the values typed here". Work on the server, over Remote Desktop.
- **Linux.** No Control Panel and no COM. The file is `/etc/hmailserver/hMailServer.ini`, `0640 root:hmailserver`: `sudoedit /etc/hmailserver/hMailServer.ini`.
- **Not REST or `hmctl`.** The REST ini routes and `hmctl ini set` handle `[Settings]` keys only.
- **Per server.** On a warm standby each node has its own `[LDAP]`; keep the two alike by hand.
- **`ServicePassword`** is a line of that file. It is not among the `[Settings]` secrets 6.3.4 seals under the stored-secrets key; the Control Panel's field for it is write-only.

### The sign-in keys

| Key | Default | What it does |
|---|---|---|
| `Enabled` | 0 | The master switch |
| `Server` | empty | The directory host; required |
| `Port` | 0 | 0 means 636 for LDAPS, 389 otherwise |
| `Security` | 2 | 2 LDAPS, 1 StartTLS, 0 plain; anything else is LDAPS |
| `VerifyCertificate` | 1 | Check the directory's certificate |
| `AllowUnprotectedPassword` | 0 | Permit a simple bind over a plain connection |
| `BindMethod` | 0 | 0 simple; 1 Negotiate (Kerberos or NTLM through Windows SSPI) |
| `SearchBase` | empty | Required unless `UserDnTemplate` is set or the bind is Negotiate |
| `UserSearchFilter` | `(&(objectCategory=person)(objectClass=user)(sAMAccountName=%u))` | How the user's entry is found |
| `UserDnTemplate` | empty | Bind directly, without searching |
| `ServiceUsername`, `ServiceDomain`, `ServicePassword` | empty | What the search binds with |
| `TimeoutSeconds` | 10 | Bounds every directory operation; 1 to 90 |
| `FallbackToWindowsLogon` | 0 | Also try Windows `LogonUser`; Windows only |

**LDAPS with `VerifyCertificate=1` is the default and the recommended configuration**, proven end to end against a Windows Server 2025 domain controller. `Server` must be the name in the directory's certificate: the same setup pointed at the controller's IP address, with the right password, is refused, and `HM5920` reports a failure "unrelated to the credentials supplied". On Windows, put the issuing CA in `LocalMachine\Root`; when this was proven the service ran as LocalSystem, and a `CurrentUser\Root` import served it nothing. The bind needs no extra privilege under `NT SERVICE\hMailServer`, which a new 6.3.4 installation runs as. On Linux the certificate must chain to the system trust store.

**StartTLS** (`Security=1`) connects on 389 and upgrades before anything is sent. **Plain LDAP** is labelled *Unprotected LDAP - cleartext (lab networks only)*: there a simple bind is refused (`HM5921`) rather than sent, unless `AllowUnprotectedPassword` is 1.

**Simple or Negotiate.** A simple bind sends the password to the directory, so it belongs on LDAPS or StartTLS. Negotiate, Windows only, never sends it; it worked on 389 against a controller that had no LDAPS certificate yet, and suits a mail server not joined to the domain.

**Search or template.** A search binds with the service credentials, finds the user under `SearchBase` with `UserSearchFilter`, then binds as the user. It asks for two entries to see ambiguity: more than one match is a configuration fault and authenticates nobody. `UserDnTemplate` skips the search, so no service credential need be stored at all; with a simple bind, that is the configuration the feature was proven with.

### Linking an account to the directory

The directory is asked only about linked accounts; others keep their own password. The webmail's sign-in shares the IMAP and POP3 logon, so it takes the directory password too.

- **Control Panel:** the account dialog's **Windows directory** tab, with the domain (`ADDomain`) and user name (`ADUsername`); **Browse Active Directory** searches the forest to fill them. Since 6.3.4 the tab has that name; it was called Directory, and the new **Directory entry** tab is the organisation's address book, unrelated to Active Directory.
- **REST:** `PUT /api/v1/accounts/{address}` with `ad_enabled`, `ad_domain` and `ad_username`.
- **COM (Windows):** `IsAD`, `ADDomain` and `ADUsername`.

**What a link changes.** The account's stored password is no fallback: in the proof run, a linked account's local password was refused. Linked accounts bypass local hashing, so they are exempt from the minimum-hash policy and from password expiry, and cannot sign in with SCRAM-SHA-256; their clients use `PLAIN` or `LOGIN` over TLS. App passwords work for them: the directory holds the password, but an app password is local, which is what makes a second factor possible for a directory account at all. The person cannot change the password in the webmail (`POST /api/v1/me/password` answers 409), and *Forgotten your password?* cannot reset it, because this server does not hold it.

**Without LDAP, on Windows**, a linked account is checked by `LogonUser`, which needs the mail server's host joined to the domain. On a host that is not joined, Windows answers every domain name, real or misspelt, as a wrong password, so the account can never sign in; `HM5911` says so, naming the domain and the computer. An empty domain, `.` or the computer's own name means a **local Windows account**. **Never leave the domain empty by accident:** with `[LDAP] Enabled` off, an account linked with an empty domain and the user name `administrator` would accept the server's local Administrator password.

**`FallbackToWindowsLogon`** never applies to a wrong password: a directory's rejection deliberately does not fall through to `LogonUser`, because that would double every failed attempt and trip a lockout policy on a single wrong password. Leave it off on a host that is not domain-joined.

### Setting it up on Windows

1. For LDAPS or StartTLS, put the directory's issuing CA in `LocalMachine\Root` on the mail server.
2. On the mail server, open **Directory authentication (LDAP)**. Switch directory authentication on (`Enabled`); until then a linked account is checked by Windows `LogonUser`, not with these settings. Fill in the server, the security, the bind, and either the search or `UserDnTemplate`; the first card asks *Is this configuration complete?*
3. **Test the connection** with a directory user name, its AD domain and its password. It tries the values typed on the page.
4. Save. The server re-reads `[LDAP]` within two seconds.
5. Link one account and sign in with the right password and a wrong one. Over POP3 the proof run got `+OK Mailbox locked and ready` and `-ERR`, and the log showed the bind and the rejection separately.
6. Link the others by hand, or with directory synchronisation.

### Setting it up on Linux

The only directory path is an LDAP simple bind over LDAPS or StartTLS, proven against OpenLDAP's slapd.

1. Make sure the directory's certificate chains to the system trust store, and use the name it carries.
2. Add the section with `sudoedit /etc/hmailserver/hMailServer.ini`, filling in the angle brackets. Without a `UserSearchFilter` line the search uses Active Directory's filter, `(&(objectCategory=person)(objectClass=user)(sAMAccountName=%u))`, which finds nobody in another directory such as OpenLDAP; there, write a filter that matches its user entries, with `%u` for the name:

```ini
[LDAP]
Enabled=1
Server=<the host name the directory's certificate carries>
Security=2
VerifyCertificate=1
BindMethod=0
SearchBase=<the DN the users are under>
UserSearchFilter=<that filter, unless the directory is Active Directory>
ServiceUsername=<the search account>
ServicePassword=<its password>
```

3. Two seconds later the server has it. Link an account over REST (`ad_enabled`, `ad_domain`, `ad_username`) and sign in with the right password and a wrong one.

Linux has no Negotiate (`BindMethod=1` is refused as a directory failure, not a wrong password: `HM6420`, reported once), no `LogonUser` (a linked account while `[LDAP] Enabled` is off is refused with `HM6421`, which names the settings to set; `FallbackToWindowsLogon` refuses with `HM6404`), and no GSSAPI bind.

### When the directory cannot answer

So that an outage does not read as users mistyping, infrastructure failures are reported apart from credential rejections, throttled per error and endpoint, and `TimeoutSeconds` bounds the wait.

| Code | Meaning |
|---|---|
| `HM5920` | The directory could not be used (a certificate not matching `Server`, say). Reported at High: unrelated to the credentials, and every account on that directory is affected |
| `HM5921` | A simple bind would have sent the password unprotected, so the logon was refused first |
| `HM5911` | Windows `LogonUser`: the host is not joined to the domain; at most once a minute per problem |

### Directory synchronisation: preview, then apply

Synchronisation reads the directory, works out which mailboxes should exist, and only when asked creates and updates them; it was verified end to end against a Windows Server 2025 domain controller. The planner writes nothing and the applier decides nothing, so a preview cannot promise what the apply would not do.

- **Per-domain opt-in.** A domain takes part only if its Active Directory domain name is set (`ADDomainName` in the domain dialog, `ad_domain_name` over REST), so a search base one level too high cannot provision into unrelated hosted domains.
- **Nothing is ever deleted**, account or message. The most it does is clear `Active`, only when asked, and never on a truncated or empty enumeration or for a domain in which nothing was seen.
- **No password.** A made account is marked `IsAD` and signs in through the directory from the start.
- **No existing account claimed.** An account that is not linked, or is linked to another AD domain, is reported, not changed.
- **Paged.** Active Directory answers a search beyond `MaxPageSize` (1000) with the first page and a success status, so the enumeration pages; `SyncMaxUsers` is the ceiling.

| `[LDAP]` key | Default |
|---|---|
| `SyncFilter` | `(&(objectClass=user)(objectCategory=person)(mail=*)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))` |
| `SyncMailAttribute` | `mail`; `proxyAddresses` works, preferring the capitalised `SMTP:` primary |
| `SyncUsernameAttribute`, `SyncDisplayNameAttribute` | `sAMAccountName`, `displayName` |
| `SyncMaxUsers` | 5000; at most 100000 |
| `SyncScheduleMinutes` | 0, off; 15 at least, 10080 (seven days) at most |

On Windows:

1. Get authentication working first. Synchronisation binds as the service account, honouring `BindMethod`, so Negotiate works where there is no LDAPS certificate.
2. Set the Active Directory domain name on each mail domain that takes part.
3. Open **Directory synchronisation**, check the selection keys, choose the domain and whether absent accounts are marked inactive, and run **Preview**. It changes nothing.
4. Read the report, including the existing accounts it reports and leaves alone.
5. **Apply**. The button stays disabled until a preview of the same options has run.

Behind the page are `Settings.PreviewDirectorySync(DomainName, DisableMissing)` and `Settings.ApplyDirectorySync`, for a server administrator and callable from a script. From a Control Panel on another machine the six selection boxes are read-only and, up to 6.3.6, show their defaults; since 6.3.7 the Control Panel no longer reads its own computer's file for the `[LDAP]` values, which it cannot read from the server, so it no longer shows them as the server's. Preview and Apply still run inside the server.

**The unattended schedule** (`SyncScheduleMinutes`) runs at start and then every so many minutes. It creates and updates but **never disables**: unattended is where nobody reads the report that would show a mailbox about to be taken away. The schedule is one of the tasks the server registers as it starts, and only if `SyncScheduleMinutes` is set then, so restart the service once after first setting it. **On Linux**, with no COM, there is no preview or apply, and whether the schedule runs there is not documented: treat it as untested.

### OAuth2 bearer tokens on IMAP, POP3 and SMTP

With `OAuth2Enabled=1`, SMTP, IMAP and POP3 offer `XOAUTH2` and `OAUTHBEARER` (RFC 7628), by default only over TLS (`OAuth2RequireTLS=1`). The server verifies the token's signature itself, takes the login name from the claim `OAuth2UsernameClaim` names (`email`), and signs in to that mailbox; a `user=` the client asserts must match. **The mailbox must already exist**: a token never makes one.

| Key | Default | What it does |
|---|---|---|
| `OAuth2JwksUrl` | empty | The provider's JWK Set; the token's `kid` picks the key. https, or plain http to loopback only |
| `OAuth2JwksCacheSeconds` | 3600 | How long the set is kept |
| `OAuth2PublicKeyFile` | empty | A PEM RSA or EC public key; the fallback |
| `OAuth2HmacSecret` | empty | The HS256 key |
| `OAuth2AllowedAlgorithms` | `RS256` | HS256, RS256 and ES256 recognised; `none` never |
| `OAuth2Issuer`, `OAuth2Audience` | empty | Expected `iss` and `aud`; empty turns the check off |
| `OAuth2IntrospectionUrl` | empty | RFC 7662 endpoint asked whether a verified token is still active |
| `OAuth2IntrospectionClientId`, `OAuth2IntrospectionClientSecret` | empty | Who this server introspects as, over HTTP Basic |
| `OAuth2IntrospectionCacheSeconds` | 300 | Verdict reuse, never past the token's `exp` |
| `OAuth2IntrospectionFailOpen` | 0 | 1 accepts a token the endpoint cannot judge, logging each |

An unknown `kid`, which is what a rotation looks like, fetches the set again at once, at most once a minute; a failed fetch keeps the keys held. `HttpProxy`, when set, carries these fetches.

**Where to set them.** Each applies when saved: on the Control Panel's **Authentication** page, with `PUT /api/v1/settings/ini/{name}` and the administrator password, with `hmctl ini set`, or with COM `Settings.SetIniSetting`. Not in `hMailServer.ini`: since 6.3.4 the database holds them, and a line edited in the file is not used; [Settings: when a save takes effect](/topic/28222) explains the store.

1. Choose the key source: `OAuth2JwksUrl`, so a rotation needs no copied file; or `OAuth2PublicKeyFile`; or `OAuth2HmacSecret` with HS256 allowed.
2. Set `OAuth2Issuer` and `OAuth2Audience` to what the provider writes into its tokens. Left empty, neither is checked: any token the provider's keys signed is accepted, whatever client it was issued to.
3. Optionally set introspection, so a revoked token stops working before it expires.
4. Only then set `OAuth2Enabled` to 1.
5. On Windows, read the **OAuth2 / XOAUTH2** item of the Control Panel's **External setup** list.

**Why that order.** Switched on without usable key material, the mechanisms are still advertised, every token fails, and failed bearer sign-ins feed the auto-ban, so a correct client can have its address banned.

On Linux before the REST listener is up, `hmailserver --set-setting` stores a key; a secret is given by name alone and typed at a prompt. A value stored this way reaches the running server at its next save, reinitialise or restart. Set the issuer and audience too, as step 2 says, putting your provider's values in place of these examples:

```sh
sudo hmailserver --set-setting OAuth2JwksUrl=https://idp.example.com/keys
sudo hmailserver --set-setting OAuth2Issuer=https://idp.example.com
sudo hmailserver --set-setting OAuth2Audience=hmailserver
sudo hmailserver --set-setting OAuth2Enabled=1
sudo systemctl restart hmailserver
```

**Since 6.3.4:** a bearer token meets the domain's and the account's country policy, as a password does (a stolen token used to sign in from a refused country); an SMTP session signed in with a token can send more than one message (the second used to be refused with 550 "Login credentials no longer valid"); and `OAuth2HmacSecret` and `OAuth2IntrospectionClientSecret` are sealed at rest once the installation has a stored-secrets key, which an upgrade does not make until an administrator runs `--create-stored-secrets-key`.

**Tested how.** The regression fixtures play the identity provider on the loopback, to make it misbehave: RS256 and ES256, the JWK Set and its rotation, introspection and fail-open. A real provider's tokens are not yet part of them.

This is inbound only. The `OutboundOAuth2` keys and `FetchOAuth2Hosts` are the server's own token for relaying through and collecting from Microsoft 365; [Routes, relays and smart hosts](/topic/28269) covers relaying with it.

### Where these end: SCIM, single sign-on and Kerberos

**The LDAP backend authenticates; SCIM provisions.** Synchronisation reads the directory; SCIM is the other direction, Entra ID or Okta calling `/scim/v2` under an API key whose role holds `provision`, and it removes accounts, which synchronisation never does. Single sign-on, OpenID Connect or SAML 2.0, opens the webmail and the Control Deck only, ending in `POST /api/v1/session`; the protocols' bearer tokens are a separate, older mechanism with the `OAuth2` settings above, and single sign-on does not reach mail clients. On Windows, SASL `GSSAPI` (`GssapiEnabled`) lets a mail client sign in with a Kerberos ticket, mapped to the account linked to that directory user. [SCIM provisioning and single sign-on](/topic/28267) sets those up; [Signing in](/topic/28253) and [Connecting a mail client](/topic/28255) show the person's side.

### What it does not do

- **No Negotiate, Windows logon or GSSAPI bind on Linux.** [hMailServer on Linux](/topic/28189) lists the rest.
- **No LDAP server here**, and the webmail's organisation directory is typed in, not imported from Active Directory.

### Where to read more

- Wiki: [Settings Reference](https://gitlab.com/hmailserver/hmailserver/-/wikis/Settings-Reference) sections 9.2 and 11, [Capabilities and Configuration](https://gitlab.com/hmailserver/hmailserver/-/wikis/Capabilities-and-Configuration), [Control Panel Reference](https://gitlab.com/hmailserver/hmailserver/-/wikis/Control-Panel-Reference), [Installing on Linux](https://gitlab.com/hmailserver/hmailserver/-/wikis/Installing-on-Linux), [Security Hardening](https://gitlab.com/hmailserver/hmailserver/-/wikis/Security-Hardening), [Sign-in and Identity](https://gitlab.com/hmailserver/hmailserver/-/wikis/Sign-in-and-Identity), [Architecture](https://gitlab.com/hmailserver/hmailserver/-/wikis/Architecture)
- Documents: [SettingsApply.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/SettingsApply.md), [ServiceAccount.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/ServiceAccount.md), [Scim.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/Scim.md), [PasskeysAndSingleSignOn.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/PasskeysAndSingleSignOn.md), [TestLab.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/TestLab.md)
- The [Roadmap](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/Roadmap.md) rows on LDAP, which record how each part was proven
- On this forum: [the stored-secrets key](/topic/28241), [auto-ban](/topic/28239) and [the warm standby](/topic/28232)
- The public documentation's [Creating accounts from Active Directory](https://www.progressiverobot.com/hmailserver-documentation/#64-creating-accounts-from-active-directory) and [Modern authentication](https://www.progressiverobot.com/hmailserver-documentation/#172-modern-authentication)

Report a security flaw in any of this by [SECURITY.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/SECURITY.md)'s routes: a [confidential GitLab issue](https://gitlab.com/hmailserver/hmailserver/-/issues/new?issue[confidential]=true), or email to contact-project+hmailserver-hmailserver-86730559-issue-@incoming.gitlab.com, which GitLab files as one.
