Request ADFS Security Token with PowerShell

In the course of my work I often have need to investigate end-to-end protocol flows. For example, I want to see an RST being generated, I want to see HTTP 302 or 200 responses coming back from a server, and I want to see what my SOAP messages actually look like on the wire. Unfortunately for me and fortunately for end users, most applications strive to hide this detail as much as possible. To help myself and others in our work, I’ve written some nifty PowerShell functions to reveal the details that others have hidden.

Here I’ll share a function designed to request a security token from an ADFS server. It relies on .NET 4.5 and can be run from any system with Web access to the ADFS endpoints. It runs against the following two ADFS endpoints, so you’ll need to make sure they’re enabled on your ADFS server:

  • /adfs/services/trust/windowsmixed - WS-Trust 1.3, Windows, TransportWithMessageCredential (Mixed)
  • /adfs/services/trust/usernamemixed - WS-Trust 1.3, Password, TransportWithMessageCredential (Mixed)

You can download the function from the TechNet Script Gallery here. In this post, we’ll step through how it works.

The parameters for the function are:

  • ClientCredentialType: Will authentication be done via Windows authentication (SSPI negotiation) or a UserName/Password token. This determines which address in ADFS will be used and which binding will be configured.
  • ADFSBaseUri: The root URI of your ADFS application. For me, this is https://fs.joshgav.com, and yours is probably similar.
  • AppliesTo: The Relying Party realm the token is to be issued for. You’ll need to have previously configured this RP in ADFS.
  • Username, Password, Domain: Self-explanatory I think. I’m not sure if you need the domain or not, and will update this when I confirm that.
  • SAMLVersion (1 or 2): ADFS will give you SAML 1.1 assertions by default, but you can request SAML 2. This parameter helps you specify which you want.
  • OutputType (Token or RSTR): Choose if you want back a .NET SecurityToken object or a RequestSecurityTokenResponse object. Both have their uses.
  • IgnoreCertificateErrors: Do you know how expensive SSL certficates are? Save yourself some cash and set up your own Certificate Authority for testing. Specify this switch to ignore certificate problems during the request.

The first lines in the function are:

$ADFSTrustPath = 'adfs/services/trust/13'

$SecurityMode = 'TransportWithMessageCredential'

$ADFSBaseUri = $ADFSBaseUri.TrimEnd('/')

switch ($ClientCredentialType) {

    'Windows' {

        $MessageCredential = 'Windows'

        $ADFSTrustEndpoint = 'windowsmixed'

    }

    'UserName' {

        $MessageCredential = 'UserName'

        $ADFSTrustEndpoint = 'usernamemixed'

    }

}

 $Credential = New-Object System.Net.NetworkCredential `

    -ArgumentList $Username,$Password,$Domain

Here we set some initial variables. I’ve hardcoded in a reference to the WS-Trust 1.3 endpoints and we’ll be using the corresponding WS2007HttpBinding. We’ll also be using the TransportWithMessageCredential (aka “mixed”) SecurityMode. We make sure to remove trailing slashes on the ADFS URL so that formatting works smoothly later. Finally we set variables to be used for the ADFS endpoints and to configure our own client bindings either for Windows or UserName authentication. To make things easier, we create a credential object based on the values passed in by the user.

We’ll be using all these variables later in the function. Now it's time to get to work.

Add-Type -AssemblyName 'System.ServiceModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'

Add-Type -AssemblyName 'System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'

Here we make sure these assemblies are loaded. They might be already, but it doesn’t hurt to load them again. Assemblies can only exist once per AppDomain; each subsequent load does nothing, but also doesn’t cause an error.

Time to make the Binding! Bindings in WCF define the channels over which protocol messages will flow, such as whether HTTP, HTTPS, or TCP will be used, whether authentication is required, and whether messages will be sent as plain text or somehow compressed or encoded. Here we start with a prefab binding - the WS2007HttpBinding. This includes most of the elements needed for communicating with a WS-* web service; we just need to set some authentication and security-related parameters.

$Binding = New-Object -TypeName System.ServiceModel.WS2007HttpBinding `

-ArgumentList ([System.ServiceModel.SecurityMode] $SecurityMode)

$Binding.Security.Message.EstablishSecurityContext = $false

$Binding.Security.Message.ClientCredentialType = $MessageCredential

$Binding.Security.Transport.ClientCredentialType = 'None'

$EP = New-Object -TypeName System.ServiceModel.EndpointAddress `

-ArgumentList ('{0}/{1}/{2}' -f $ADFSBaseUri,$ADFSTrustPath,$ADFSTrustEndpoint)

 

We set the security mode to be TransportWithMessageCredential (using the variable set earlier). We specify not to set a persistent security context, which would use WS-SecureConversation and set up a Session token, a complication we don’t want right now. The message client credential type will be either Windows or UserName, depending on what we set earlier, and we don’t add any credentials at the transport (i.e. HTTP) layer.

We also create an Endpoint Address, which represents the address which the service is listening on, based on the parameters passed in by the caller.

With our Binding and Endpoint Address prepared, we’re nearly ready to combine them and build a WS-Trust protocol client. To do so, we’ll start by building a client factory. As its name implies, the factory will be able to churn out the actual client (also known as a ‘channel’) to do our work.

$WSTrustChannelFactory = New-Object -TypeName System.ServiceModel.Security.WSTrustChannelFactory `

    -ArgumentList $Binding, $EP

$WSTrustChannelFactory.TrustVersion = System.ServiceModel.Security.TrustVersion]::WSTrust13

$WSTrustChannelFactory.Credentials.Windows.ClientCredential = $Credential

$WSTrustChannelFactory.Credentials.UserName.UserName = $Credential.UserName

$WSTrustChannelFactory.Credentials.UserName.Password = $Credential.Password

$Channel = $WSTrustChannelFactory.CreateChannel()

The first line builds our factory, which combines the WS-Trust protocol with the binding and endpoint address we already created. Next we specify a trust version and credentials to use for channels created by this factory. The astute amongst you will notice we set both a Windows and UserName credential here, even though we’ll actually only use on or the other. It doesn’t hurt to add them both. Once we've built and configured our factory, we tell it to create us a channel!

Channel in hand, we now need to construct a RST (RequestSecurityToken) to send to ADFS.

$TokenType = @{

    SAML11 = 'urn:oasis:names:tc:SAML:1.0:assertion'

    SAML2 = 'urn:oasis:names:tc:SAML:2.0:assertion'

}

 

$RST = New-Object -TypeName System.IdentityModel.Protocols.WSTrust.RequestSecurityToken -Property @{

   RequestType = [System.IdentityModel.Protocols.WSTrust.RequestTypes]::Issue

   AppliesTo = $AppliesTo

   KeyType = [System.IdentityModel.Protocols.WSTrust.KeyTypes]::Bearer

   TokenType = if ($SAMLVersion -eq '2') {$TokenType.SAML2} else {$TokenType.SAML11}

}

$RSTR = New-Object -TypeName System.IdentityModel.Protocols.WSTrust.RequestSecurityTokenResponse

In the first command, we create a Hashtable to represent the two token types we’re prepared to retrieve. This is just to keep things clearer later on as you'll see. Next, we create the RST object and set its properties via a Hashtable passed for the -Property parameter. If this looks too convoluted in your browser, perhaps copy it out into the ISE. The RequestType is the WS-Trust 'Issue' protocol and we are requesting a Bearer key. The TokenType is set to SAML2 or SAML11 depending on the user’s selection; and the AppliesTo property is set to the specified Relying Party. In the last command, we create an empty RSTR object to hold the RSTR which ADFS will return.

Now we’re ready to use our RST in a request.

try {

    $OriginalCallback = [System.Net.ServicePointManager]::ServerCertificateValidationCallback

    if ($IgnoreCertificateErrors.IsPresent) {

[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {return $true}

    }

    $Token = $Channel.Issue($RST, [ref] $RSTR)

}

finally {

    [System.Net.ServicePointManager]::ServerCertificateValidationCallback = $OriginalCallback

}

As mentioned in the list of parameters, I wanted this function to work okay even with certificates not signed by a globally-trusted CA, so I’ve added a way to bypass certificate checking. Because this modification effects the entire PowerShell process, even after our function has completed, I’ve added some elements to make sure the change is only in effect during our service call and is then reverted. Wrapping this all in a try/finally block ensures that even if something goes wrong with the request, the certificate validation bypass is still reverted.

The main work here is done when we call Issue on the channel we created previously. We pass in the RST we created and receive back an RSTR and a processed SecurityToken from ADFS. The RSTR is stored in the out parameter $RSTR, and the SecurityToken is stored in $Token.

Now all that’s left is to return what the user asked for.

if ($OutputType -eq 'RSTR') {

    Write-Output -InputObject $RSTR

} else {

    Write-Output -InputObject $Token

}

These lines are pretty self-explanatory. If the user asked for an RSTR that’s what we return; if he/she wants the processed token, that’s what they get.

With our function declared and explained, we’re ready to test it out against our server. Don't forget, you can download the complete script from the TechNet Script Gallery. Here’s what calling it could look like:

Invoke-ADFSSecurityTokenRequest `

    -ClientCredentialType UserName `

    -ADFSBaseUri https://corp.sts.microsoft.com `

    -AppliesTo https://activedirectory.windowsazure.com `

    -UserName 'joshgav' `

    -Password 'MyPassword' `

    -Domain 'CORP' `

    -OutputType Token `

    -SAMLVersion 2 `

    -IgnoreCertificateErrors

Here I’m requesting a token from Microsoft’s corporate ADFS STS to apply to Azure Active Directory. I supply my username and password (names changed to protect the innocent) and receive a SAML 2 token in return.

Your next step should be to assign the SecurityToken or RSTR output from the function to a variable and explore its properties. I also recommend turning on Fiddler in the background while you execute the function and checking out the request and response.

Invoke-SecurityTokenRequest.zip