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
The Code
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



Nice work Carol!
Any chance that you’ll post the dll or send it via email?
Thanks in advance
Hi Nadav. I don’t ever post compiled code. This code worked in my environment but yours may not be the same, so it’s really better if you compile it yourself. It’s not hard to do – all you need is Visual Studio with the VB.NET libraries installed. Here’s a good ref: http://msdn.microsoft.com/en-us/library/ms695368.aspx
Thanks for your answer but I have a problem with the DLL it seams like the csentry.DN.ToString is a GUID and I think that Set-MSOnlineUserPassword needs an email as identity.
Thanks for your time.
Nadav.
What csentry are you talking about? In this example my csentry comes from my BPOS MA where the DN is in fact the BPOS identity (normally the same as the email address). A csentry.DN does not have to be a GUID.
I’ve just encountered the System.AppDomainUnloadedException issue on a PowerShell based MA and knew it would be worth checking in here!
Thanks for the post – that’s saved me a “bit” of investigation…
J.