Automating Active Directory Access Troubleshooting with Node.js and TypeScript


Two users are supposed to have the same access. One can use the application and the other cannot. Now somebody has to work out which of dozens of directory groups is different.

That is exactly the kind of repetitive investigation that software should be doing for us.

The Access Problem That Looks Like an Application Bug

Access problems almost never arrive labelled as access problems. They arrive as “the app is broken for me.”

Someone opens a ticket. A page returns an error, or a section is missing, or a button does nothing. An engineer picks it up, reproduces it against their own account, and finds the application working perfectly. Nothing in the logs looks wrong, because from the application’s point of view nothing is wrong — it asked whether this identity was permitted to do something, got back “no”, and behaved correctly.

The meaningful difference is somewhere in directory group membership. It is real, it is discoverable, and it is genuinely tedious to find.

The standard approach is to pull up the affected user’s memberships and read them. And that is where the workflow quietly falls apart.

Why a Normal AD Lookup Wasn’t Enough

Looking at one user’s group memberships tells you what they have. It does not tell you what they are missing — and missing is the entire question.

A list of group names, on its own, is close to unreadable in a diagnostic sense. Names are long, similar to one another, often abbreviated, and frequently follow conventions that made sense to whoever created them years ago. Reading such a list and knowing which entry should be there but isn’t requires you to already hold the correct answer in your head. If you had that, you would not be troubleshooting.

So people do the obvious thing: they pull a second user who does work, and compare.

That instinct is correct. The execution is miserable. Two long lists, in two windows, scanned by eye, looking for the entries present in one and absent in the other. It is slow, it does not scale with list length, and it fails in a specific, dangerous way — a near-miss between two similarly named groups reads as a match, and the engineer confidently concludes the memberships are identical when they are not.

The useful question was never “what groups does this person have?” It was “what is different between this working user and this non-working user?”

Once I wrote the question down that way, the tool designed itself.

Compare a Broken User Against a Working User

The core idea is not sophisticated. A known-good user is a baseline. Diff against it.

Given two identities, the tool retrieves both sets of memberships and splits the result into three buckets:

  • Shared — groups both users have. Usually the overwhelming majority, and usually irrelevant to the investigation.
  • User A only — groups the working user has and the broken user does not. This is the bucket that contains the answer, most of the time.
  • User B only — groups the broken user has and the working user does not. Less often the cause, but worth seeing: it catches the cases where someone was placed in a group that actively conflicts with something.

The value is in the proportions. Two users in the same team might share a hundred groups and differ by three. Reading a hundred entries to notice three is the manual workflow. Reading three is the tool.

That is the whole insight, and it is worth being honest that the clever part is not the algorithm — it is deciding that comparison, rather than lookup, is the primitive worth building.

AD access comparison flow An engineer authenticates through SSO to an internal web tool. A Node.js and TypeScript service queries Active Directory, normalises the returned memberships, and compares two users, producing three result sets: shared groups, groups only User A has, and groups only User B has. Engineer SSO Internal web UI Node.js / TypeScriptservice Active Directory Normalise memberships Compare — intersection & difference Shared User A only User B only
Lookup and comparison flow. The three result sets are the point — the shared set is usually the largest and the least interesting.

The Architecture

Deliberately unremarkable, which for internal tooling is a feature:

  • Web interface — an internal page where an engineer enters one identity, or two for comparison mode.
  • Node.js and TypeScript service — handles directory interaction, normalisation and the comparison itself.
  • Active Directory — the source of user and membership information.
  • SSO — sits in front of the whole thing. Nobody reaches the tool, let alone the directory, without authenticating first.
  • Structured JSON between layers, so the frontend never parses a raw directory response.

TypeScript earns its place here specifically because directory data is messy. Modelling the normalised shape as a type, and converting at the boundary, means the comparison logic operates on something predictable instead of on whatever shape the directory happened to return that day. The types are documentation for a data structure that is otherwise easy to misremember.

Turning Group Membership Into Sets

Once you frame it as “what is different”, the implementation is set arithmetic. Intersection and difference — operations that have been well understood for a very long time.

The important work happens before the comparison. Directory responses need normalising into a predictable form: consistent casing, trimmed whitespace, a single stable identifier per group, and a decision about which attribute is the canonical name. Skipping this step is how you get a comparison that reports differences that are not real, which is worse than no tool at all, because it looks authoritative.

Illustrative — generic shapes, not any real directory schema:

type NormalisedGroup = { key: string; label: string };

const normalise = (raw: RawDirectoryEntry[]): Map<string, NormalisedGroup> =>
  new Map(
    raw.map((entry) => {
      const key = entry.identifier.trim().toLowerCase();
      return [key, { key, label: entry.displayName.trim() }];
    })
  );

function compareMemberships(
  a: Map<string, NormalisedGroup>,
  b: Map<string, NormalisedGroup>
) {
  const shared: NormalisedGroup[] = [];
  const onlyA: NormalisedGroup[] = [];
  const onlyB: NormalisedGroup[] = [];

  for (const [key, group] of a) (b.has(key) ? shared : onlyA).push(group);
  for (const [key, group] of b) if (!a.has(key)) onlyB.push(group);

  return { shared, onlyA, onlyB };
}

Keying on a normalised identifier while carrying the human-readable label separately matters more than it looks. You want to compare on the stable thing and display the readable thing. Comparing on display names is how you end up with false differences caused by a capitalisation change nobody made deliberately.

Designing the Output for Troubleshooting

This is where a tool like this succeeds or fails, and it has nothing to do with the backend.

Five screens of the Directory Explorer tool: dashboard, single-user lookup with group memberships, the two-user comparison form, comparison results split into unique and shared groups, and a group detail view.
The comparison result is the screen that matters — differences first, the shared set present but de-emphasised. Placeholder data throughout.

It would have been easy to render both lists side by side and call it done. That version would have been faster to build and would have solved almost nothing, because the hard part of the manual workflow was never retrieval — it was the comparison, and side-by-side lists hand that straight back to the human.

So the output leads with the differences. The exclusive sets come first, they are visually distinct from one another, and the shared set — usually the largest by a wide margin — is present but deliberately de-emphasised. It needs to be available, because occasionally you need to confirm something is shared rather than absent. It does not need to be the first thing you see.

The principle generalises: a diagnostic tool should present the conclusion, not the evidence. Evidence stays one click away for when the conclusion looks wrong.

Security Matters Even for Small Internal Tools

It is easy to treat an internal utility as exempt from scrutiny. It should not be. This one reads from the directory, and directory membership is genuinely sensitive — it describes your organisation’s access structure in one convenient list.

The constraints I worked to:

Authenticate before anything. SSO in front of the tool. There is no anonymous mode, no “just for testing” bypass. A troubleshooting utility that will happily enumerate anyone’s group membership for any visitor is not a utility, it is a reconnaissance endpoint.

Least privilege on directory reads. Whatever identity performs the lookups needs only read access to the specific attributes the tool actually uses.

Return only what the tool needs. Directory objects carry far more than group membership. Pulling everything and filtering in the frontend means the extra attributes still crossed the wire and still landed in logs.

Validate and normalise input. The same normalisation that makes comparison correct also narrows what reaches the directory query.

Do not leak raw directory errors. Error text from a directory can disclose structure — what exists, what does not, how things are named. Users get something useful; details stay in logs.

Keep it read-oriented. The tool tells you what the difference is. It does not fix it. That is not a limitation I plan to remove casually — turning a diagnostic utility into a permission-management surface changes its risk profile completely, and would need a deliberate authorisation design of its own rather than being bolted onto a lookup screen.

That last one is worth dwelling on. The obvious feature request for a tool like this is “add a button to fix it.” Resisting that is the security decision, not an oversight.

The Bigger Lesson: Automate the Investigation, Not Just the Query

The temptation with a project like this is to build a nicer window onto an existing system. Wrap the directory in a web UI, ship it, call it developer tooling.

That version would have been genuinely less useful, and the reason is precise: it automates the part that was already easy. Retrieving a user’s groups was never the bottleneck. Comparing them was, and comparison was the step still being done by a person, by eye, under time pressure, on a ticket somebody was waiting on.

The valuable move was noticing the shape of the manual workflow — pull the broken user, pull a working user, find the delta — and encoding that into the application, rather than encoding the data source.

I think that generalises well past Active Directory. When you look at a repetitive internal task, the question is not “which system is involved and can I put a UI on it?” It is “what decision is a person making over and over, and can the software make it instead?” Those produce very different tools, and only one of them saves anybody any time.

View the project in my Software Development Showcase.


Leave a Reply

Your email address will not be published. Required fields are marked *