Make Auth Broker authoritative for managed passwords

This commit is contained in:
2026-09-18 16:46:56 -06:00
parent 8290e347f5
commit 7d78a1f515
8 changed files with 154 additions and 1 deletions
+1
View File
@@ -22,4 +22,5 @@ internal static class BrokerEventIds
internal static readonly EventId DirectoryOptionalMetadataFailure = new(1301, nameof(DirectoryOptionalMetadataFailure));
internal static readonly EventId DirectoryGroupMembershipFailure = new(1302, nameof(DirectoryGroupMembershipFailure));
internal static readonly EventId DirectoryRoleGroupMembershipAdded = new(1303, nameof(DirectoryRoleGroupMembershipAdded));
internal static readonly EventId DirectoryPasswordChangeDenied = new(1304, nameof(DirectoryPasswordChangeDenied));
}
@@ -1,5 +1,7 @@
using System.Collections.Concurrent;
using System.DirectoryServices;
using System.Security.AccessControl;
using System.Security.Principal;
using SGU.AuthBroker.Core.Directory;
using SGU.AuthBroker.Core.Identity;
using SGU.AuthBroker.Core.Profiles;
@@ -15,6 +17,12 @@ public sealed class ActiveDirectorySynchronizer(
private const int InfoAttributeMaximumLength = 1024;
private const int AccountDisabled = 0x0002;
private const int NormalAccount = 0x0200;
private static readonly Guid ChangePasswordExtendedRight =
new("AB721A53-1E2F-11D0-9819-00AA0040529B");
private static readonly SecurityIdentifier EveryoneSid =
new(WellKnownSidType.WorldSid, null);
private static readonly SecurityIdentifier SelfSid =
new(WellKnownSidType.SelfSid, null);
private static readonly AuthenticationTypes BindFlags =
AuthenticationTypes.Secure | AuthenticationTypes.Signing | AuthenticationTypes.Sealing;
@@ -117,6 +125,12 @@ public sealed class ActiveDirectorySynchronizer(
// account without its required classification.
EnsureRoleGroupMembership(user, identity);
// SGU remains the password authority. Deny the user's Change
// Password extended right before making the account usable. This
// does not deny the broker's administrative Reset Password right,
// which ADSI SetPassword uses for each successful authentication.
EnsureCannotChangePassword(user, identity.UserName);
// The exact institutional password received by the broker is passed to AD.
// It is not derived, transformed, written to disk, or included in logs.
user.Invoke("SetPassword", [password]);
@@ -143,6 +157,52 @@ public sealed class ActiveDirectorySynchronizer(
}
}
private void EnsureCannotChangePassword(DirectoryEntry user, string institutionalUser)
{
user.Options!.SecurityMasks = SecurityMasks.Dacl;
ActiveDirectorySecurity security = user.ObjectSecurity;
if (!EnsureCannotChangePassword(security))
{
return;
}
user.ObjectSecurity = security;
user.CommitChanges();
logger.LogInformation(
BrokerEventIds.DirectoryPasswordChangeDenied,
"Denied direct password changes for managed Active Directory user {InstitutionalUser}; SGU Auth Broker remains the password authority.",
institutionalUser);
}
internal static bool EnsureCannotChangePassword(ActiveDirectorySecurity security)
{
bool changed = false;
foreach (SecurityIdentifier identity in new[] { EveryoneSid, SelfSid })
{
bool exists = security
.GetAccessRules(includeExplicit: true, includeInherited: false, typeof(SecurityIdentifier))
.OfType<ActiveDirectoryAccessRule>()
.Any(rule =>
rule.AccessControlType == AccessControlType.Deny &&
rule.IdentityReference.Equals(identity) &&
rule.ObjectType == ChangePasswordExtendedRight &&
(rule.ActiveDirectoryRights & ActiveDirectoryRights.ExtendedRight) != 0);
if (exists)
{
continue;
}
security.AddAccessRule(new ActiveDirectoryAccessRule(
identity,
ActiveDirectoryRights.ExtendedRight,
AccessControlType.Deny,
ChangePasswordExtendedRight));
changed = true;
}
return changed;
}
private static void TryApplyProfile(
DirectoryEntry user,
UserIdentity identity,