LDAP Authentication Example for Internal Web Apps
Sign users in with an LDAPS bind, map directory groups to app roles and fail safely when the directory is down.
- Runs out of the box: yes
- Tests: yes
- CI: yes
- Docs: yes
- Used in production: no
Purpose#
A small Node.js web service that authenticates people against a corporate directory. It uses the search-then-bind pattern: a read-only service account finds the user's distinguished name from the login name, then the service binds as that user over LDAPS to verify the password. Nothing about the password is stored.
After a successful bind the service reads the user's group memberships and maps them to three application roles (viewer, editor, admin). Failed binds, locked accounts and an unreachable directory each produce a distinct audit event but the same generic message on the login form.
What you will learn#
- Explain why search-then-bind is preferred over building a user DN from the login name
- Configure an LDAPS connection with certificate verification and sensible timeouts
- Map directory groups to application roles in one reviewed table
- Return one generic login error while logging the precise failure reason
Media#
Technologies & features#
Lessons learned#
- Worked
A separate read-only service account for the DN lookup meant user binds never had to carry search permissions, and revoking the service account was a one-line change.
- Didn’t work
Building the DN as uid=<login>,ou=people failed for users who typed their UPN. Searching on both the login name and the UPN fixed it.
- Decision
Group membership is cached for five minutes per session. Shorter caches loaded the directory; longer ones delayed role removals.
- Limitation
Nested groups are not expanded. Roles come from direct membership only, so teams must add people to the mapped groups themselves.
Try it#
git clone https://git.example.internal/business-systems/ldap-auth-example.git
cd ldap-auth-example
docker compose up --buildThe compose file starts a demo directory seeded with three accounts. Open http://localhost:3000 and sign in as demo.editor with the password from demo/README.md.
Commands come from the owner’s README. Credentials are never stored here.


