Sign-in with the company account in an internal app: Active Directory step by step

In short

An app doesn't need its own passwords. At sign-in it asks the domain controller whether the username and password are correct, and reads permissions from Active Directory groups. The connection to the controller must be encrypted (LDAPS), the service account read-only, and groups should contain people directly, because nesting is easy to miss.

Key points
  • The domain controller checks the password. The app doesn't store it.
  • Access and roles follow AD groups, so a change in the directory takes effect at the next sign-in.
  • Encrypted connections to the controller only: LDAPS on port 636 or StartTLS.
  • One person, one account: username, UPN and email must all lead to the same user.

How it works in three steps

Sign-in with the company account means the app keeps no password list of its own. Employees type the same username and password as for their computer, and the app asks the company directory, Active Directory, about them.

  1. Finding the user

    The app connects to the domain controller with a service account and looks up the person by username, UPN or email. It gets their full identifier in the directory and the list of groups.

  2. Checking the password

    The app tries to connect to the controller as that user, with the password they typed. If the controller accepts the connection, the password is correct. The password itself is never stored.

  3. Granting permissions

    Based on the groups, the app decides whether the person may enter at all, which modules they can use and with what role. It does this at every sign-in, so a change in the directory takes effect at the next visit.

What the IT department has to prepare

Checklist0 of 6

Permissions from groups instead of granting them by hand

The simplest model that scales well has three layers: an entry group (who can sign in at all), module groups (where you can go) and role groups (how deep you can see inside). Module and role are two independent dimensions: someone can have access to a report without the right to see internal notes.

AD groupWhat it gives
APP-UsersEntry to the app. Anyone not in this group can't sign in at all.
APP-Module-ParkingAccess to one module, here: parking bookings.
APP-Role-AnalystA role in the modules: for example details an ordinary user doesn't see.
APP-Role-AdminFull administrative rights. With several roles, the higher one wins.
Tip

When someone has access to no module, show them a clear message after sign-in instead of a sign-in error. A sign-in error sends people off to reset a password that is actually fine.

Pitfalls that only show up in practice

Nested groups

The memberOf attribute returns direct memberships only. If someone is in a group through another group, the app won't see it unless it asks specifically (AD has a special matching rule for that). The simplest approach is to agree that the app's groups contain people directly.

Username, UPN or email

One day someone types jan.kowalski, another day jan.kowalski@company.com. Always create the app account under one identifier (for example sAMAccountName), or one person will end up with two accounts.

An empty password

Some servers treat a connection with an empty password as anonymous and report success. The app must reject an empty username or password before it asks the controller at all.

Attempt limits

A few failed attempts should lock sign-in in the app for a few minutes. Otherwise a password-guessing script will lock the employee's domain account across the whole company.

The controller's certificate

Encryption without checking the certificate protects against eavesdropping, but not against a fake server. Point the app at the authority's certificate file and turn verification on.

An example in Python

A sketch of the logic with the ldap3 library, which is pure Python and installs without extra programs, also on Windows. A real app adds timeouts, a sign-in log and attempt limits.

Sign-in over LDAPS (ldap3)
import os, ssl
from ldap3 import Server, Connection, Tls, SUBTREE
from ldap3.core.exceptions import LDAPException
from ldap3.utils.conv import escape_filter_chars

TLS = Tls(validate=ssl.CERT_REQUIRED, ca_certs_file="C:/certs/company-ca.pem")
SERVER = Server("dc01.company.local", port=636, use_ssl=True, tls=TLS)
BASE = "DC=company,DC=local"
ACCOUNT = "CN=svc-app,OU=Service,DC=company,DC=local"

def sign_in(login, password):
    if not login or not password:  # empty username or password: never ask the controller
        return None
    f = escape_filter_chars(login)
    with Connection(SERVER, ACCOUNT, os.environ["LDAP_SVC_PASSWORD"], auto_bind=True) as svc:
        svc.search(BASE, f"(|(sAMAccountName={f})(userPrincipalName={f})(mail={f}))",
                   SUBTREE, attributes=["sAMAccountName", "displayName", "memberOf"])
        if len(svc.entries) != 1:
            return None
        user = svc.entries[0]
    try:  # the controller checks the password: try to connect as this user
        Connection(SERVER, user.entry_dn, password, auto_bind=True).unbind()
    except LDAPException:
        return None
    return {"login": user.sAMAccountName.value, "name": user.displayName.value,
            "groups": list(user.memberOf.values)}
Tip

In Django such code goes into a custom authentication backend (the AUTHENTICATION_BACKENDS setting). At every sign-in the backend creates or updates the Django account and saves the groups as roles.

Terms from this guide

Active Directory

The directory of users, groups and computers in a company's Windows network. An internal app can take sign-in and permissions from it.

LDAP and LDAPS

The protocol an app uses to talk to the user directory: it looks up accounts and checks passwords. LDAPS is its encrypted version, usually on port 636.

Questions and answers

Does the app store employees' passwords?

No. The password goes only to the domain controller at the moment of sign-in. The app records the result: who signed in and when.

The company has Microsoft 365, not its own AD server. What then?

Then sign-in is usually done through Microsoft Entra ID and the OpenID Connect protocol, not LDAP. The principle is the same: Microsoft checks the password, and the app gets information about the user and their groups.

What happens when the domain controller is down?

Nobody new can sign in, and those signed in work until their session ends. That's why at least two controllers are configured.

Can sign-in work without encryption?

Technically yes, but then domain passwords travel across the network in plain text. Microsoft has recommended signing and encrypting LDAP connections for years, so it isn't an option for production.

Sources and documentation

The service related to this guide

Custom software and internal tools

Describe the problem

Question 1 of 5

What would you most like to improve?

Pick the closest one. It doesn't have to fit perfectly.

First summary

This is a first suggestion based on a few answers, not a diagnosis. Nothing is saved until you send the message.