fix(auth): canonical username is the attribute value the user matched (Auth-009)

ExtractUserName returned the first value of a possibly multi-valued
UserNameAttribute (default cn), so jsmith could sign in as "John Smith"
depending on server ordering. It now prefers the value equal to the typed
name ignoring case (still canonicalising case) and falls back to the first
value only when none matches.

(scadaproj#46)

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 15:18:25 -05:00
parent b9a096c53a
commit 9ec0da1b2c
3 changed files with 81 additions and 9 deletions
+16 -2
View File
@@ -10,7 +10,7 @@
| Last reviewed | 2026-11-09 (re-review; baseline 2026-06-01 at `5f75cd4`) |
| Reviewer | Claude (2026-11-09 re-review, scadaproj#15; baseline: Claude automated) |
| Commit reviewed | `91fb2b4` |
| Open findings | 3 |
| Open findings | 2 |
## Summary
@@ -429,7 +429,7 @@ I verified this live against a SQL Server 2022 container:
| Severity | Low |
| Priority | P3 |
| Category | Correctness & logic bugs |
| Status | Open |
| Status | Resolved |
| Location | `ZB.MOM.WW.Auth/src/ZB.MOM.WW.Auth.Ldap/LdapAuthService.cs:437` |
**Description**
@@ -454,6 +454,20 @@ fake connection: typed `jsmith` returned `Username == "John Smith"`.
Prefer the attribute value that equals the typed (normalised) name under `OrdinalIgnoreCase`, which still
canonicalises case. Fall back to `values[0]` only when no value matches. Add a multi-valued-attribute test.
**Resolution**
Resolved 2026-11-09 (scadaproj#46, Auth `0.4.4`, unpublished). Re-verified through the fake connection: with
`cn: ["John Smith", "jsmith"]`, typed `jsmith` returned `Username == "John Smith"`. `ExtractUserName` now returns the
attribute value that equals the typed (normalised) name under `OrdinalIgnoreCase`. That keeps 0.4.1's case
canonicalisation, so typed `jsmith` against `JSmith` still returns `JSmith`. Only when no value matches does it fall
back to the first non-blank value, and then to the typed name, as 0.4.1 did. Both bind modes go through
`SucceedFromEntry`, so both are covered.
Tests (`LdapAuthServiceTests`):
- `Username_FromAMultiValuedAttribute_IsTheValueTheUserMatched` fails pre-fix.
- `..._StillTakesTheDirectorysCase` fails pre-fix.
- `Username_FallsBackToTheFirstValue_WhenNoValueMatchesTheTypedName` is a guard that passes both ways.
### Auth-010 — `ApiKeyVerifier` lets store exceptions escape despite its "only exception path is cancellation" contract
| | |