Calling the MFA SDK with PowerShell Invoke-Command
Had a fun question to answer the other day, “How many users have OAuth tokens?”.
At a glance it looked like an easy task because the MFA Server has a web service SDK
Being spoiled by modern REST web services and Invoke-RestMethod it seemed this would be simple, but then a couple challenges stood in the way:
- the endpoint used SOAP
- the endpoint required certificate authentication
The first challenge was nicely explained by fellow MIM’er Darren J Robinson in this post: Connecting to and Using the Azure MFA Web Service SDK Server SOAP API with Powershell
The New-WebServiceProxy command does an awesome job of bringing the web service functionality into PowerShell but unfortunately it does not work with client certificate authentication.
It was so tempting to just configure the endpoint to allow an authentication method supported by New-WebServiceProxy but tampering with the security of the endpoint made me feel dirty.
A cool workaround was to download the WSDL using Invoke-WebRequest (Invoke-WebReqeust does work with certificate authentication), so:
- Use Invoke-WebRequest to download the WSDL by appending ?WSDL to the URI
- Save the WSDL to a file
- Use New-WebServiceProxy with a URI of file://<path to the saved WSDL file>
This approach gets around the issue with certificate support in New-WebServiceProxy because client certificate authentication works after the proxy is created, it just does not work when connecting to the endpoint to get the WSDL.
Although I was able to generate the proxy, the resulting methods had some [ref] parameters that did not make sense and I wasn’t excited about figuring that out.
I was more interested to see if Invoke-WebRequest could be used to call the SOAP endpoint directly, without New-WebServiceProxy.
The help content in the Multi-Factor Authentication Server Help file seemed pretty straighforward, just get the 26 parameters correct and you’re good to go (just glad the parameters are documented!).
The content generated by the endpoint was also super helpful because it provided the actual body that the endpoint expected.
It took a few attempts to get the request body right, and I learned a few things along the way:
- the help content includes the xml tag at the top but needs to be removed
- the endpoint validates the parameters (xml nodes in the body)
- the parameter value types mostly explain themselves (string, boolean, etc)
- the help file was great for listing out the enums
- the web request might return OK but there could still be an error in the response content
- the web request will fail if the usual things are wrong (authorization, connectivity, SSL/TLS, etc.)
The final script to call the endpoint is actually fairly simple and I was happy to have it working with Invoke-WebRequest which is a pretty reliable command.
$cert = dir Cert:\LocalMachine\My | Where-Object Subject -EQ 'CN=MfaSdkCertificate' <# Thumbprint Subject ---------- ------- 80088008B8008C8008980083D37800858008 CN=MfaSdkCertificate #> <# The SOAP body for the FindUsers_4 API #> $findUsersBody = @' <soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope"> <soap12:Body> <FindUsers_4 xmlns="http://www.phonefactor.com/PfWsSdk"> <usernameFilter>*</usernameFilter> <usernameCaseSensitive>false</usernameCaseSensitive> <firstNameFilter>*</firstNameFilter> <firstNameCaseSensitive>false</firstNameCaseSensitive> <lastNameFilter>*</lastNameFilter> <lastNameCaseSensitive>false</lastNameCaseSensitive> <emailFilter>*</emailFilter> <emailCaseSensitive>false</emailCaseSensitive> <userGroupFilter>*</userGroupFilter> <userGroupCaseSensitive>false</userGroupCaseSensitive> <phoneFilter>*</phoneFilter> <modeFilter>oathToken</modeFilter> <modeSpecified>true</modeSpecified> <pinModeFilter>standard</pinModeFilter> <pinModeSpecified>false</pinModeSpecified> <smsDirectionFilter>unspecified</smsDirectionFilter> <smsDirectionSpecified>false</smsDirectionSpecified> <smsModeFilter>unspecified</smsModeFilter> <smsModeSpecified>false</smsModeSpecified> <phoneAppModeFilter>unspecified</phoneAppModeFilter> <phoneAppModeSpecified>false</phoneAppModeSpecified> <tagMatchType>allUsers</tagMatchType> <tagIds></tagIds> <enabledFilter>false</enabledFilter> <enabledSpecified>false</enabledSpecified> <resultLimit>1000</resultLimit> </FindUsers_4> </soap12:Body> </soap12:Envelope> '@ ### Make the SOAP Call with the client certificate $soapResponse = Invoke-WebRequest -Method Post -Uri https://myMfaServer/multifactorauthwebservicesdk/PfWsSdk.asmx -Body $findUsersBody -ContentType 'application/soap+xml' -Certificate $cert <# Output the results Note the results are an XML document #> ($soapResponse.Content -as [xml]).Envelope.Body.FindUsers_4Response.userRows.UserRow <# Username : firstname.lastname@example.org UserGroup : CountryCode : 1 Phone : 555-867-5309 Extension : #>