Skip to main content

ldap-auth

ldap-auth

Authenticates a request's HTTP Basic credentials against an LDAP server. The bind DN is assembled from the presented username and the configured directory location, then a simple bind is attempted with that DN and the presented password: a successful bind authenticates the request. Place it early in the request pipeline, before the upstream node.

Configuration

KeyTypeDefaultDescription
base_dnstring— (required)Base DN the bind DN is built under, e.g. ou=users,dc=example,dc=org.
ldap_uristring— (required)LDAP server URI, e.g. ldap://ldap.example.org:389 (or ldaps://…:636).
uidstringcnRDN attribute that prefixes the username in the bind DN.
use_tlsbooleanfalseNegotiate StartTLS on the connection after connecting.
tls_verifybooleanfalseVerify the LDAP server's TLS certificate.
realmstringldapRealm advertised in the WWW-Authenticate: Basic challenge.
timeout_msnumber10000Whole-operation deadline for the connect + bind.
- id: auth
type: ldap-auth
config:
base_dn: ou=users,dc=example,dc=org
ldap_uri: ldap://ldap.example.org:389
uid: cn
use_tls: false
tls_verify: false

base_dn and ldap_uri are required; a missing or blank value is rejected at config load.

Behavior

  1. The Authorization: Basic <base64(user:pass)> header is parsed. The scheme match is case-insensitive; the payload is standard-base64 decoded, split on the first :, and all whitespace is stripped from both the username and the password.
  2. The bind DN is assembled as <uid>=<username>,<base_dn> (e.g. cn=alice,ou=users,dc=example,dc=org).
  3. The plugin connects to ldap_uri and attempts a simple bind with the DN and password, under the timeout_ms deadline.

On a successful bind the context passes through the success port, with the authenticated username exposed to downstream nodes as context.message["user"].

On a missing header, malformed credentials, empty username/password, or a bind rejection, the plugin rejects and exits through the denied port:

  • context.response.status_code = 401
  • WWW-Authenticate: Basic realm="<realm>" challenge header
  • Body: {"error": "unauthorized", "message": "<reason>"} with content-type: application/json

A connection error or a connect+bind timeout is a genuine infrastructure failure, not a credential decision — it stays on the error port instead, with error code LDAP_AUTH_PROVIDER_ERROR. The prepared response is a 502 {"error": "provider_error", "message": "<reason>"} with no WWW-Authenticate challenge — an LDAP outage must not make the browser re-prompt for a password that was never checked.

Breaking change

Before v0.8 this provider-failure response reused the denied shape (401 + WWW-Authenticate: Basic, {"error": "unauthorized"}, error code LDAP_AUTH_FAILED), so an unreachable LDAP server was indistinguishable from a rejected credential (and a browser would re-prompt for the password). It is now the shared 502 {"error": "provider_error", "message": "<reason>"} response with no challenge header, the same shape every provider-backed auth plugin prepares (openid-connect, cas-auth, ldap-auth, authz-keycloak, authz-casdoor). The error code was renamed from LDAP_AUTH_FAILED to LDAP_AUTH_PROVIDER_ERROR — "failed" is the denial word. Match on the error port / the error code, or on the 502, instead of the old status.

Ports

ldap-auth declares three output ports: success, denied (a deliberate credential rejection is prepared), and error (the LDAP server was unreachable, or the connect+bind deadline elapsed — a genuine infra failure, not a rejection). Like success, denied is a mandatory port: the policy compiler rejects any policy that leaves it unwired. Wire ldap-auth.denied straight to client so the prepared 401 reaches the caller instead of continuing into upstream; wire error to an error-handler (or leave it unwired for the default 500) since that path represents the node failing to do its job, not a policy decision:

edges:
- from: ldap-auth.success
to: upstream.in
- from: ldap-auth.denied
to: client.in

Behavior notes

  • Bind-auth only, not search-then-bind. The bind DN is built directly from uid + base_dn; the plugin never performs a directory search to locate the user's entry — uid/base_dn fully determine the DN.
  • No consumer resolution. The plugin performs pure bind authentication: on a successful bind the request continues and the username is written to context.message["user"]; no consumer identity is attached and no consumer is required.
  • Empty passwords are rejected up front. A blank password would otherwise trigger an unauthenticated (anonymous) bind that many directories accept, silently authenticating anyone. featherbit rejects empty username/password before contacting the server.
  • use_tls negotiates StartTLS on the given URI. For implicit TLS, use an ldaps:// URI directly.

Errors

The node returns the Context with an error, so the graph engine routes through the error port and appends the error to context.errors. The status below is the one prepared on context.response; wire error to client (or an error-handler) for the caller to see it.

CodeStatusWhen
LDAP_AUTH_PROVIDER_ERROR502The LDAP connection failed, or the bind timed out. Wrong credentials are a denied, not this.