fix: make Admin LDAP sign-in work against GLAuth

Three bugs blocked sign-in entirely:

- Login.razor is static-SSR but its form model lacked
  [SupplyParameterFromForm], so the posted username/password never
  bound — SignInAsync saw empty fields and bailed before LDAP was
  contacted. Annotate the model; seed it in OnInitialized since
  BL0008 forbids an initializer on a [SupplyParameterFromForm]
  property.
- appsettings.json ServiceAccountDn used ou=svcaccts, which GLAuth
  reads as a (non-existent) group — the service-account bind failed
  with "Group not found". Use cn=serviceaccount,dc=lmxopcua,dc=local.
- LdapAuthService resolved the user DN by searching (uid=...), but
  GLAuth keys users by cn. Add an LdapOptions.UserNameAttribute knob
  (default cn for GLAuth; set sAMAccountName for Active Directory)
  and use it for the search filter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-18 02:48:00 -04:00
parent 482d5f5637
commit 5f5bfe1ea5
4 changed files with 18 additions and 4 deletions

View File

@@ -47,10 +47,17 @@
public string Password { get; set; } = string.Empty; public string Password { get; set; } = string.Empty;
} }
private Input _input = new(); // Static-SSR form post: the model must be [SupplyParameterFromForm] or the
// submitted username/password never bind back onto _input. The property
// cannot carry an initializer (BL0008) — seed it in OnInitialized instead.
[SupplyParameterFromForm]
private Input _input { get; set; } = default!;
private string? _error; private string? _error;
private bool _busy; private bool _busy;
protected override void OnInitialized() => _input ??= new();
private async Task SignInAsync() private async Task SignInAsync()
{ {
_error = null; _error = null;

View File

@@ -108,7 +108,7 @@ public sealed class LdapAuthService(IOptions<LdapOptions> options, ILogger<LdapA
await Task.Run(() => await Task.Run(() =>
conn.Bind(_options.ServiceAccountDn, _options.ServiceAccountPassword), ct); conn.Bind(_options.ServiceAccountDn, _options.ServiceAccountPassword), ct);
var filter = $"(uid={EscapeLdapFilter(username)})"; var filter = $"({_options.UserNameAttribute}={EscapeLdapFilter(username)})";
var results = await Task.Run(() => var results = await Task.Run(() =>
conn.Search(_options.SearchBase, LdapConnection.ScopeSub, filter, ["dn"], false), ct); conn.Search(_options.SearchBase, LdapConnection.ScopeSub, filter, ["dn"], false), ct);
@@ -116,7 +116,7 @@ public sealed class LdapAuthService(IOptions<LdapOptions> options, ILogger<LdapA
return results.Next().Dn; return results.Next().Dn;
throw new LdapException("User not found", LdapException.NoSuchObject, throw new LdapException("User not found", LdapException.NoSuchObject,
$"No entry for uid={username}"); $"No entry for {filter}");
} }
return string.IsNullOrWhiteSpace(_options.SearchBase) return string.IsNullOrWhiteSpace(_options.SearchBase)

View File

@@ -29,6 +29,13 @@ public sealed class LdapOptions
public string DisplayNameAttribute { get; set; } = "cn"; public string DisplayNameAttribute { get; set; } = "cn";
public string GroupAttribute { get; set; } = "memberOf"; public string GroupAttribute { get; set; } = "memberOf";
/// <summary>
/// Attribute the service-account search matches the login name against to resolve the
/// user's DN. <c>cn</c> for GLAuth (the dev default); set <c>sAMAccountName</c> for
/// Active Directory.
/// </summary>
public string UserNameAttribute { get; set; } = "cn";
/// <summary> /// <summary>
/// Maps LDAP group name → Admin role. Group match is case-insensitive. A user gets every /// Maps LDAP group name → Admin role. Group match is case-insensitive. A user gets every
/// role whose source group is in their membership list. Example dev mapping: /// role whose source group is in their membership list. Example dev mapping:

View File

@@ -10,7 +10,7 @@
"UseTls": false, "UseTls": false,
"AllowInsecureLdap": true, "AllowInsecureLdap": true,
"SearchBase": "dc=lmxopcua,dc=local", "SearchBase": "dc=lmxopcua,dc=local",
"ServiceAccountDn": "cn=serviceaccount,ou=svcaccts,dc=lmxopcua,dc=local", "ServiceAccountDn": "cn=serviceaccount,dc=lmxopcua,dc=local",
"ServiceAccountPassword": "serviceaccount123", "ServiceAccountPassword": "serviceaccount123",
"DisplayNameAttribute": "cn", "DisplayNameAttribute": "cn",
"GroupAttribute": "memberOf", "GroupAttribute": "memberOf",