The FIM Sync Service allows passwords to be synchronised from a source AD account to the user’s accounts in other systems. The sync is done at the point of password change and relies on the Password Change Notification Service, which you must install on your domain controllers.
Many target systems are supported OOB, but for BPOS you have to get a little creative and write your own Password Extension. This post shows you how I did that.
Before I continue however, I do feel obliged to make a comment about the security implications of doing this. There is an argument that passwords used internally should never be sent outside the firewall, and this should be considered in your environment. In my case the source AD was created just for DirSync so the accounts there are not actually being used for anything else, and the password remains different to the user’s normal desktop logon. Password Sync provides an alternative to the BPOS admin console, and opens the way for reset through the FIM Portal.
First, you need a connector space which represents your BPOS user objects
This should be pretty obvious – for password sync to work it needs a direct join, through the Sync Service, from the source AD to the target account. So you need a connector space with objects that represent your BPOS users and contain, at an absolute minimum, the BPOS Identity which you will use in the powershell password change process.
For ideas about creating a BPOS MA see Three Different Ways to Create a BPOS Management Agent.
The MA has to run in process
There is apparently a bug with password extensions in FIM Sync – if you run the MA in a seperate process the sync service can’t find the extension and you see these errors in the event log:
An unexpected error has occurred during a password set operation.
BAIL: MMS(4948): ma.cpp(373): 0x80040154 (Class not registered)
Running the MA in process fixes this problem.
The Powershell Runspace doesn’t dispose quickly enough
I ran into a problem with a System.AppDomainUnloadedException. The password sync worked fine, but then five minutes later the entire miiserver.exe process crashed with this exception.
The Sync service loads an extension dll when it is needed, and keeps it open while it’s being used. Five minutes after the last use of the dll it runs any termination code and unloads the dll. Clearly something was going wrong here.
At this point I’m going to shout out a big thanks to Brian Desmond and Craig Martin who helped me with this problem. While I still don’t completely understand what’s going on, I gather it has something to do with the sync service unloading the password extension while the runspace is still being disposed. The solution I found is to add a System.Threading.Thread.Sleep(0) straight after the Dispose instruction, which is an instruction to wait for other threads to finish.
Install and configure PCNS
I’m not going to go into this. There is perfectly good documentation (see Peter Geelen’s overview and troubleshooting tips here) and actually the instructions haven’t changed since MIIS 2003.
Though I will just add, in case anyone’s interested, yes you can install PCNS on a server core domain controller (though not an RODC for the obvious reason that it can’t process a password change).
Configuring the Sync Service
And here is the password extension code.
Imports Microsoft.MetadirectoryServices Imports System.Management.Automation Imports System.Management.Automation.Host Imports System.Management.Automation.Runspaces Imports System.Diagnostics Public Class BPOSPasswordExtension Implements IMAPasswordManagement Dim myRunSpace As Runspace Dim bposCred As PSCredential Public Sub BeginConnectionToServer(ByVal connectTo As String, _ ByVal user As String, _ ByVal password As String) _ Implements Microsoft.MetadirectoryServices.IMAPasswordManagement.BeginConnectionToServer Dim PSRemoteUser As String = "" Dim PSRemotePassword As String = "" Dim BPOSUser As String Dim BPOSPassword As String If user.Contains(";") Then PSRemoteUser = user.Split(";")(0) BPOSUser = user.Split(";")(1) Else BPOSUser = user End If If password.Contains(";") Then PSRemotePassword = password.Split(";")(0) BPOSPassword = password.Split(";")(1) Else BPOSPassword = password End If If Not connectTo = "" Then If PSRemoteUser = "" Or PSRemotePassword = "" Then Throw New BadServerCredentialsException("If Connect To is configured in the Password Settings then " _ & "there must be two semicolon separated Users (psremoteuser;bposuser) and two semicolon separated " _ & "passwords (psremotepassword;bpospassword)") Exit Sub End If End If ' Open powershell runspace If connectTo = "" Then myRunSpace = OpenLocalRunspace() Else myRunSpace = OpenRemoteRunspace(connectTo, PSRemoteUser, PSRemotePassword) End If If myRunSpace Is Nothing Then Throw New PasswordExtensionException("Failed to open powershell runspace to server " & connectTo) Exit Sub End If WriteToEventLog("Successfully opened powershell runspace to " & connectTo, EventLogEntryType.Information) ' Create credential for attaching to BPOS Dim bpospass As New Security.SecureString For Each c In BPOSPassword bpospass.AppendChar(c) Next bposCred = New PSCredential(BPOSUser, bpospass) ' Add plugin required for BPOS cmdlets Dim psh As PowerShell = PowerShell.Create() psh.Runspace = myRunSpace psh.AddCommand("Add-PSSnapin") psh.AddParameter("Name", "Microsoft.Exchange.Transporter") Try psh.Invoke() Catch ex As Exception Throw New PasswordExtensionException("Failed to add PS snapin. " & ex.Message) End Try End Sub Public Sub ChangePassword(ByVal csentry As Microsoft.MetadirectoryServices.CSEntry, _ ByVal OldPassword As String, _ ByVal NewPassword As String) _ Implements Microsoft.MetadirectoryServices.IMAPasswordManagement.ChangePassword End Sub Public Sub EndConnectionToServer() _ Implements Microsoft.MetadirectoryServices.IMAPasswordManagement.EndConnectionToServer If Not myRunSpace Is Nothing Then myRunSpace.Dispose() System.Threading.Thread.Sleep(0) WriteToEventLog("Runspace closed.", EventLogEntryType.Information) End If End Sub Public Function GetConnectionSecurityLevel() As Microsoft.MetadirectoryServices.ConnectionSecurityLevel _ Implements Microsoft.MetadirectoryServices.IMAPasswordManagement.GetConnectionSecurityLevel End Function Public Sub RequireChangePasswordOnNextLogin(ByVal csentry As Microsoft.MetadirectoryServices.CSEntry, _ ByVal fRequireChangePasswordOnNextLogin As Boolean) _ Implements Microsoft.MetadirectoryServices.IMAPasswordManagement.RequireChangePasswordOnNextLogin ' This method is not used Throw New EntryPointNotImplementedException End Sub Public Sub SetPassword(ByVal csentry As Microsoft.MetadirectoryServices.CSEntry, _ ByVal NewPassword As String) _ Implements Microsoft.MetadirectoryServices.IMAPasswordManagement.SetPassword Dim psh As PowerShell = PowerShell.Create() Dim psresult As New System.Collections.ObjectModel.Collection(Of PSObject) psh.Runspace = myRunSpace psh.AddCommand("Set-MSOnlineUserPassword") psh.AddParameter("Identity", csentry.DN.ToString) psh.AddParameter("Password", NewPassword) psh.AddParameter("ChangePasswordOnNextLogon", False) psh.AddParameter("Credential", bposCred) Try psresult = psh.Invoke() Catch ex As Exception Throw New PasswordExtensionException(ex.Message) End Try If psh.Streams.Warning.Count > 0 Then Throw New PasswordExtensionException(psh.Streams.Warning.Item(0).Message) ElseIf psh.Streams.Error.Count > 0 Then Throw New PasswordExtensionException(psh.Streams.Error.Item(0).ErrorDetails.Message) End If psh.Dispose() End Sub #Region "Powershell Functions" Private Function PSCredObject(ByVal username As String, ByVal password As String) As PSCredential Dim PWSecureString As New Security.SecureString Dim c As Char For Each c In password PWSecureString.AppendChar(c) Next Dim PSCred As New PSCredential(username, PWSecureString) Return PSCred End Function Private Function OpenRemoteRunspace(ByVal RemoteServer As String, ByVal RemoteUser As String, ByVal RemotePassword As String) As Runspace Const SHELL_URI As String = "http://schemas.microsoft.com/powershell/Microsoft.PowerShell" ' Open remote powershell session Dim serverUri As New Uri("http://" & RemoteServer.ToUpper & ":5985/wsman") Dim connectionInfo As New WSManConnectionInfo(serverUri, SHELL_URI, PSCredObject(RemoteUser, RemotePassword)) Dim myRunSpace As Runspace Try myRunSpace = RunspaceFactory.CreateRunspace(connectionInfo) myRunSpace.Open() Catch ex As Exception Throw New PasswordExtensionException(ex.Message) Return Nothing Exit Function End Try Return myRunSpace End Function Private Function OpenLocalRunspace() As Runspace Dim config As RunspaceConfiguration = RunspaceConfiguration.Create() Dim myRunSpace As Runspace Try myRunSpace = RunspaceFactory.CreateRunspace(config) myRunSpace.Open() Catch ex As Exception Throw New PasswordExtensionException(ex.Message) Return Nothing Exit Function End Try Return myRunSpace End Function #End Region Public Function WriteToEventLog(ByVal Entry As String, ByVal eventType As EventLogEntryType) Dim appName As String = "BPOS Password Sync" Dim logName = "Application" Dim objEventLog As New EventLog() Try 'Register the App as an Event Source 'Note: this only works if the service account has rights to the reg key http://support.microsoft.com/kb/842795 If Not EventLog.SourceExists(appName) Then EventLog.CreateEventSource(appName, logName) End If objEventLog.Source = appName objEventLog.WriteEntry(Entry, eventType) Return True Catch Ex As Exception Return False End Try End Function End Class