Windows Obtaining a SAML 2.0 assertion from AD FS by using WS-Trust and Integrated Windows Authentication

When an application needs to access APIs, it typically needs to authenticate itself. For this purpose, many environments provide applications access to ambient credentials. These are credentials that the application can obtain without having to perform any additional authentication. Examples include:

But ambient credentials not only exist in the cloud: Applications that run in an Active Directory environment often have access to ambient credentials, too. When the application is configured to run as a domain user, the process has ambient access to Kerberos and NTLM credentials.

Having access to Kerberos and NTLM credentials is useful to access resources within the same Active Directory environment – but what if we need to access resources outside the environment where OAuth and SAML are prevalent?

We can bridge the gap between the NTLM/Kerberos world and the OAuth/SAML world by using an identity provider that supports Integrated Windows Authentication (IWA). In the last post, we looked at how an application can use the client credentials grant and Integrated Windows Authentication to authenticate AD FS and obtain OAuth access tokens. But what if we need a SAML assertion instead of an OAuth access token?

AD FS’s heritage

Active Directory Federation Services (AD FS) had their debut in Windows Server 2003, long before even OAuth 1.0 was a thing. So AD FS was built around the concepts of WS-Trust and WS-Federation, with OAuth and OpenID Connect support bolted on later.

Today, both WS-Federation and WS-Trust are outdated. But WS-Trust can still be useful if an application has Kerberos credentials and needs to “exchange” them against a SAML assertion. There’s only one catch: WS-Trust uses SAML 1.1 by default. Today, SAML 1.1 assertions are rarely useful and if an application uses SAML, it almost always requires a SAML 2.0 assertion.

But it is possible to use AD FS’s WS-Trust support to obtain SAML 2.0 assertions. Let’s see how.

Preparing AD FS

To use WS-Trust, we need to create a relying party trust in AD FS. Alternatively, we can create an application of type Web API (as we would do for OAuth), which automatically creates a relying party trust behind the scenes.

To use WS-Trust in combination with IWA, we need to use the /adfs/services/trust/13/windowsmixed endpoint. This endpoint is disabled by default, so we must enable it.

Finally, we have to ensure AD FS is ready to use IWA by configuring the right SPN, extending the user agent whitelist, and disabling extended protection for authentication if necessary.

Obtaining a security token in C

With AD FS prepared, we can use the following code to authenticate to AD FS by using IWA, and get a SAML 2.0 assertion in return:

GenericXmlSecurityToken GetSamlSecurityToken(
  Uri adfsBaseUri,
  Uri relyingPartyId)
  // Create a binding for IWA.
  var binding = new WS2007HttpBinding(SecurityMode.TransportWithMessageCredential);
  binding.Security.Message.EstablishSecurityContext = false;
  binding.Security.Message.ClientCredentialType = MessageCredentialType.Windows;
  binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None;
  // Create a channel factory for AD FS WS-Trust endpoint.
  WSTrustChannelFactory factory = new WSTrustChannelFactory(
    new EndpointAddress(new Uri(adfsBaseUri, "services/trust/13/windowsmixed")));
  factory.TrustVersion = TrustVersion.WSTrust13;
  factory.Credentials.Windows.ClientCredential = CredentialCache.DefaultNetworkCredentials;

  // Request a SAML 2.0 assertion.
  var tokenRequest = new RequestSecurityToken
    RequestType = RequestTypes.Issue,
    AppliesTo = new EndpointReference(relyingPartyId.ToString()),
    KeyType = KeyTypes.Bearer,
    TokenType = "urn:oasis:names:tc:SAML:2.0:assertion"

  return (GenericXmlSecurityToken)factory.CreateChannel().Issue(tokenRequest);

The key to get a SAML 2.0 assertion is the line

TokenType = "urn:oasis:names:tc:SAML:2.0:assertion"

Without this extra parameter, we’d get a SAML 1.1 assertion.

Note that this code uses System.ServiceModel instead of the older Microsoft.IdentityModel classes.

Obtaining a security token in PowerShell

If we’re in a hurry and don’t want to write any C# code, we can do the same with a few lines of PowerShell:

Add-Type -AssemblyName 'System.ServiceModel'
Add-Type -AssemblyName 'System.IdentityModel'

# Create a binding for IWA.
$Binding = New-Object `
    -TypeName System.ServiceModel.WS2007HttpBinding `
    -ArgumentList ([System.ServiceModel.SecurityMode]::TransportWithMessageCredential)

$Binding.Security.Message.EstablishSecurityContext = $False
$Binding.Security.Message.ClientCredentialType = [System.ServiceModel.MessageCredentialType]::Windows
$Binding.Security.Transport.ClientCredentialType = [System.ServiceModel.HttpClientCredentialType]::None

# Create a channel factory for AD FS WS-Trust endpoint.
$Endpoint = New-Object `
    -TypeName System.ServiceModel.EndpointAddress `
    -ArgumentList ('')

$Factory = New-Object `
    -TypeName System.ServiceModel.Security.WSTrustChannelFactory `
    -ArgumentList ($Binding, $Endpoint)

$Factory.TrustVersion = [System.ServiceModel.Security.TrustVersion]::WSTrust13
$Factory.Credentials.Windows.ClientCredential = [System.Net.CredentialCache]::DefaultNetworkCredentials

# Request a SAML 2.0 assertion.
$Request = New-Object `
    -TypeName System.IdentityModel.Protocols.WSTrust.RequestSecurityToken `
    -Property @{
        RequestType   = [System.IdentityModel.Protocols.WSTrust.RequestTypes]::Issue
        AppliesTo     = ''
        KeyType       = [System.IdentityModel.Protocols.WSTrust.KeyTypes]::Bearer
        TokenType     = 'urn:oasis:names:tc:SAML:2.0:assertion'

$Token = $Factory.CreateChannel().Issue($Request)

Write-Host $Token.TokenXml.OuterXml

Adding a NameID

By default, the SAML assertion we get from AD FS doesn’t include a NameID:

    <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <SubjectConfirmationData NotOnOrAfter="2021-10-01T07:01:25.436Z"/>

That’s not too useful, because what we’d like to see is a NameID that identifies the application. But we can change that by editing the claims issuance policy and adding a rule that maps the claim (which contains the UPN used for IWA) to the NameID claim:

c:[Type == ""]

 => issue(Type = "", Issuer = c.Issuer, OriginalIssuer = c.OriginalIssuer, Value = c.Value, ValueType = c.ValueType, Properties[""] = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified");

With that extra rule in place, our assertions contain a proper NameID:

    <NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">[email protected]</NameID>
    <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <SubjectConfirmationData NotOnOrAfter="2021-10-01T08:48:58.137Z"/>
Any opinions expressed on this blog are Johannes' own. Refer to the respective vendor’s product documentation for authoritative information.
« Back to home