feat: add ldap-auth-advanced plugin (core authentication) - #13762
feat: add ldap-auth-advanced plugin (core authentication)#13762janiussyafiq wants to merge 5 commits into
Conversation
Add the ldap-auth-advanced plugin (priority 2541), Kong ldap-auth-advanced parity, core authentication: search-then-bind (AD sAMAccountName shape), service-account or anonymous search bind, user_dn Consumer association, LDAPS/StartTLS, LDAP filter-injection defense, strict 401-vs-500 auth/transport error split, multi-auth compatibility. Bump lua-resty-ldap 0.1.0 -> 0.3.0 (pure-Lua client/protocol/filter; the released client is lazy-connect, so the plugin binds first and classifies a (nil, err) return as a transport failure). Note: under 0.1.0 the ldap-auth plugin's tls_verify was a silent no-op (the library read verify_ldap_host); 0.3.0 fixes the field name, so tls_verify:true configs begin real certificate verification. Also regenerate t/certs/localhost_slapd_cert.pem: the old cert has CN=test.com but no subjectAltName, and nginx's hostname verification does not fall back to CN, so any real tls_verify handshake against it fails with "certificate host mismatch". The new cert (same key, same test CA) adds SAN DNS:test.com, DNS:localhost, IP:127.0.0.1. Group collection/authorization and credential caching/anonymous-consumer follow in separate PRs; docs follow up separately.
- strip client-supplied X-Consumer-Username/X-Consumer-Custom-ID/ X-Credential-Identifier (with X-Authenticated-Groups) before any auth work, so consumer_required=false cannot pass spoofed identity upstream - accept RFC 4512 numeric-OID attribute types and tighten ;option validation in the attribute schema pattern - fall back to Authorization when Proxy-Authorization is present but does not parse into credentials for header_type - classify directory failures by operation and result code: 401 only for invalidCredentials on the user bind plus the not-found/ambiguous cases (including sizeLimitExceeded, the >size_limit ambiguity); service-bind rejections and search operational failures are 500 - look up Consumers via consumer.find_consumer() so a secret-ref user_dn resolves (and unresolved references fail closed) - register ldap-auth-advanced user_dn in plugin_unique_key_attrs so the Admin API rejects duplicate user_dn values
| title = "work with route or service object", | ||
| properties = { | ||
| -- connection | ||
| ldap_uri = { type = "string" }, -- "host[:port]" |
There was a problem hiding this comment.
can we add min and max lengths here? (and other relevant fields)?
There was a problem hiding this comment.
Pull request overview
This PR introduces a new core authentication plugin, ldap-auth-advanced, implementing search-then-bind LDAP authentication (including LDAPS/StartTLS, Consumer user_dn mapping, and filter-injection defenses) and updates the CI LDAP fixtures and plugin registration so it’s available and testable within APISIX.
Changes:
- Added
apisix/plugins/ldap-auth-advanced.luaplus a comprehensive LDAP integration test suite (t/plugin/ldap-auth-advanced.t). - Registered the plugin across admin/plugin listing and default config examples, and enforced Consumer unique-key duplication checks for
user_dn. - Updated the OpenLDAP CI container setup (bootstrap LDIF + ACL hook) and bumped
lua-resty-ldapto0.3.0.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
apisix/plugins/ldap-auth-advanced.lua |
New advanced LDAP auth plugin implementation (search-then-bind, TLS options, Consumer association). |
t/plugin/ldap-auth-advanced.t |
New end-to-end test coverage for the plugin, including injection defenses and multi-auth behavior. |
apisix/consumer.lua |
Adds ldap-auth-advanced to the auth-plugin unique key map for Consumer duplicate checks. |
apisix/cli/config.lua |
Registers ldap-auth-advanced in the built-in plugin list. |
t/admin/plugins.t |
Extends admin plugin list/priority assertions to include the new plugin. |
t/admin/consumers.t |
Adds Consumer duplicate user_dn coverage for ldap-auth-advanced. |
conf/config.yaml.example |
Documents the plugin in the example plugin list with correct priority ordering. |
apisix-master-0.rockspec |
Bumps lua-resty-ldap dependency to 0.3.0-0. |
ci/pod/docker-compose.plugin.yml |
Updates OpenLDAP service config and mounts new bootstrap fixtures/hooks. |
ci/pod/openldap/ad.ldif |
Provides a deterministic LDAP directory tree for CI (including advanced-plugin fixtures). |
ci/pod/openldap/enable-anon-bind.sh |
Configures OpenLDAP to allow unauthenticated bind and installs ACLs used by tests. |
t/certs/localhost_slapd_cert.pem |
Updates test cert material (notably SANs) to support hostname verification paths. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Reject empty usernames and passwords while parsing a credential header,
rather than after a header has already been chosen. In Lua "" is truthy,
so an unusable Proxy-Authorization such as "ldap OnBhc3M=" (":pass") or
"ldap dXNlcjo=" ("user:") satisfied the caller's truthiness check, won
proxy priority, and returned 401 without ever trying a valid
Authorization. Both headers now share the rule, which makes the later
checks in rewrite() redundant.
Also stop rejecting a trailing "~" as an unusable username. RFC 4515
UTF1SUBSET (%x5D-7F) includes "~", so "admin~" is a legal assertion
value and a legal directory user: it must reach the search and 401 as
not-found instead of being turned away up front. That depends on the
filter escaping fix in lua-resty-ldap, so TEST 46 fails until the
rockspec is bumped to the release carrying it.
0.3.1 escapes `~` as `\7e` when building a search filter. RFC 4515 UTF1SUBSET (%x5D-7F) includes `~`, but the bundled filter grammar rejects it raw at either end of a value, so a legal directory user such as `admin~` could not be searched for. This is what TEST 46 expects. 0.3.1 also relaxes its own pins from `lpeg = 1.0.2-1` to `lpeg >= 1.0.2` (and `lua_pack` likewise). The exact pin downgraded the lpeg that `graphql` and `jsonpath` resolve, so APISIX now keeps its own version.
membphis
left a comment
There was a problem hiding this comment.
Two issues need to be addressed before this can merge:
- [P2] Avoid three LDAP operations on every request
The successful rewrite path currently performs a service-account/anonymous bind, a user search, and a user bind (0 -> 3 LDAP protocol operations per request). Connection pooling does not remove these round trips.
Please add a small local TTL cache for successful authentication results. Cache hits should reuse the resolved canonical user_dn and skip LDAP entirely; only cache misses should run the full bind -> search -> bind flow. The cache key must include the relevant plugin configuration and a non-reversible credential discriminator—do not key only by username or retain plaintext passwords—and the TTL must be short and bounded.
- [P1] Add a gateway lifecycle shell regression
This PR adds ldap-auth-advanced to the default plugin registry, but the current coverage is limited to Perl .t tests. Please add a required-CI t/cli/test_*.sh regression that starts the real gateway, exercises plugin loading/configuration with bounded readiness polling, verifies externally observable behavior and plugin state, covers the reverse transition where applicable, and cleans up all processes and temporary configuration.
Description
Adds the
ldap-auth-advancedplugin (priority 2541), bringing Kongldap-auth-advancedparity for core authentication: search-then-bind (ADsAMAccountNameshape), service-account or anonymous search bind,user_dnConsumer association, LDAPS/StartTLS, LDAP filter-injection defense, and a strict 401-vs-500 auth/transport error split. This is part 1 of 3 (core authentication); group collection/authorization and credential caching/anonymous-consumer follow in separate PRs. Prior discussion in #8958.This PR also bumps
lua-resty-ldap0.1.0 -> 0.3.0. Under 0.1.0, the existingldap-authplugin'stls_verifyoption was a silent no-op (the library read a different field name); 0.3.0 fixes this, sotls_verify: trueconfigs now perform real certificate verification. This also required regeneratingt/certs/localhost_slapd_cert.pem(same key, same test CA) to add asubjectAltName(DNS:test.com, DNS:localhost, IP:127.0.0.1) — the old cert hadCN=test.combut no SAN, and nginx's hostname verification does not fall back to CN, so the newly-realldap-authTLS-verify test started failing a certificate host-mismatch check that the 0.1.0 bug had been silently masking.Which issue(s) this PR fixes:
Related: #8958
Checklist
lua-resty-ldapbump activates realtls_verifycertificate verification, previously a silent no-op under 0.1.0 -- see description)