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:
- On AWS, applications can use instance profiles to obtain temporary credentials.
- On Azure, applications can use managed identities to obtain access tokens.
- In Kubernetes, pods can get obtain ID tokens a Kubernetes service account
- In GitHub Actions, workflows can obtain ID tokens that reflect the workflow’s identity.
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(
binding,
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 ('https://login.example.com/adfs/services/trust/13/windowsmixed')
$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 = 'https://relyingparty.example.com/'
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
:
<Subject>
<SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<SubjectConfirmationData NotOnOrAfter="2021-10-01T07:01:25.436Z"/>
</SubjectConfirmation>
</Subject>
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 http://schemas.xmlsoap.org/ws/2005/05/identity/claims/implicitupn
claim (which contains the UPN used for IWA) to the NameID
claim:
c:[Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/implicitupn"]
=> issue(Type = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", Issuer = c.Issuer, OriginalIssuer = c.OriginalIssuer, Value = c.Value, ValueType = c.ValueType, Properties["http://schemas.xmlsoap.org/ws/2005/05/identity/claimproperties/format"] = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified");
With that extra rule in place, our assertions contain a proper NameID:
<Subject>
<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"/>
</SubjectConfirmation>
</Subject>