Password Sync from AD to BPOS

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

Start by enabling Password Sync:Tools –> Options –> Enable Password Synchronization
Configure the AD MA which is the source for synchronization of changed passwords:

On the “Configure Directory Properties” tab, tick to enable the partition as a password synchronization source.

Click “Targets” and select your BPOS MA.

Configure the BPOS MA:

On the “Configure Extensions” tab, tick to enable password management and select the password extension dll file (code below).

You will also have to set the connection credentials. As I’m accessing the MSOnline cmdlets via remote powershell I actually need two sets of credentials – one for the remote server connection and one for BPOS. However the MA config for password extensions only gives you space for one username and password. I have gone with the completely inelegant workaround of including both usernames and both passwords, separated by a semi-colon, like so:

   mydomainUser;bposUser
   mydomainPassword;bposPassword

It’s not pretty and I don’t much like it, but it’s working for now.

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

6 Replies to “Password Sync from AD to BPOS”

  1. 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.

  2. 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.

  3. 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.

Comments are closed.