Windows Obtaining AD FS access tokens using the client credentials grant and Integrated Windows Authentication

When a web application needs to access an OAuth-secured API, it can use the OAuth authorization code flow (aka 3-legged OAuth or 3LO) to obtain access tokens and access the API on the user’s behalf. That’s great for scenarios where an end user is involved, but rarely applicable for unattended applications such as Windows services.

When an unattended application needs to obtain OAuth credentials for itself, we have to use the client credentials flow and let the application provide some sort of credential. AD FS supports multiple types of credentials for the client credentials flow, including

What both of these options have in common is that we have to store a secret. In the first case, it’s a shared secret key, in the second case it’s a private key.

But there is a third way, which Microsoft didn’t care to document properly and doesn’t require us to store any secrets: Using Integrated Windows Authentication to authenticate a client.

Using Integrated Windows Authentication (IWA) lets us create a bridge between Kerberos and OAuth: Any Windows process that runs as a domain user has “ambient” access to Kerberos credentials, and it can use these credentials to prove its identity to AD FS. No additional secrets required.

For Integrated Windows Authentication to work, there are a few prerequisites that the client must meet:

  • It must run on a domain-joined computer, and the computer and AD FS must be members of the same or trusting Active Directory domains.
  • It must run as a domain user (as opposed to a local Windows user account)

There are a few prerequisites for the AD FS too – let’s walk through those in more detail.

Enable IWA for intranet authentication

First, we need to ensure IWA is enabled. The easiest way to do this is to open the AD FS MMC snap-in, go to AD FS > Service > Authentication methods, and ensure that Windows Authentication is enabled for Intranet scenarios.

Ensure that AD FS has the right SPN

A Kerberos ticket is only valid for a specific service. If a client needs a Kerberos ticket, it has to know the service’s service principal name (SPN) so that it can then request the KDC to issue a ticket for it.

In the case of Active Directory, services are actually user accounts which have a servicePrincipalName attribute. If a client needs a ticket for AD FS, what really happens is:

  1. The client builds a SPN for AD FS. It does that by taking the DNS name of AD FS, say login.example.com and prepending it by HTTP/.
  2. It then goes to the domain controller to requests a Kerbeos ticket for HTTP/login.example.com
  3. The domain controller looks in the directory for a user object with a matching servicePrincipalName attribute. If that exists, and everything else goes well, it’ll issue a Kerberos ticket.

There are two important points here:

  • The SPN must point to the user account the AD FS service runs as, not to the computer account. Often, the service account used by AD FS is a gMSA.
  • The DNS name login.example.com might be different from the Windows host name of AD FS. That’s particularly likely if you’re using multiple servers, or have deployed AD FS behind a load balancer. Because of this discrepancy, the SPN might not exist by default.

To check is the SPN exists, we can run setspn -Q:

setspn  -Q HTTP/login.example.com

Checking domain DC=lab,DC=local
CN=adfs-gmsa,CN=Managed Service Accounts,DC=corp,DC=example,DC=com
        http/login.example.com
        host/login.example.com
Existing SPN found!

If the SPN can’t be found, we can add an add an SPN using setspn -a:

setspn -a http/FQDN USER 

Where FQDN is the fully qualified domain name of AD FS (like login.example.com), and USER is the AD FS service account .

Extend the user agent whitelist for Integrated Windows Authentication

Not all clients support IWA. Therefore, AD FS maintains a whitelist of user agents that it thinks can handle IWA, and disables IWA for all other clients.

We can view the user agent whitelist by using the Get-AdfsProperties cmdlet:

(Get-AdfsProperties).WIASupportedUserAgents

MSAuthHost/1.0/In-Domain
MSIE 6.0
MSIE 7.0
MSIE 8.0
MSIE 9.0
MSIE 10.0
Trident/7.0
MSIPC
Windows Rights Management Client
MS_WorkFoldersClient
=~Windowss*NT.*Edge

This default whitelist is obviously not exhaustive and excludes pretty much all API clients, including PowerShell. To add entries to the whitelist, we can use the following command:

Set-AdfsProperties `
    -WIASupportedUserAgents ((Get-AdfsProperties).WIASupportedUserAgents + "Mozilla/5.0")

Mozilla/5.0 captures pretty much every browser on this planet, including PowerShell, which uses a user agent like this:

User-Agent: Mozilla/5.0 (Windows NT; Windows NT 10.0; en-US) WindowsPowerShell/5.1.19041.1151

Disable extended protection for authentication if necessary

When a client tries to authenticate by using IWA, AD FS uses token binding by default. Token binding, or extended protection for authentication, improves security by:

help[ing] protect against man-in-the-middle (MITM) attacks, in which an attacker intercepts client credentials and forwards them to a server. Protection against such attacks is made possible through a Channel Binding Token (CBT) which can be either required, allowed, or not required by the server when it establishes communications with clients.

This is a useful feature, but it only works if there is a direct TLS connection between the client and the AD FS service. That’s not the case if you run AD FS behind a load balancer that terminates TLS, such as a Google Cloud HTTPS load balancer.

We can disable token binding by running:

Set-ADFSProperties –ExtendedProtectionTokenCheck None

Testing IWA using PowerShell

To test if IWA works, let’s see if we can obtain an access token by using PowerShell and authenticate using our current domain user credentials.

For that, we first need an OIDC client:

  1. On the AD FS server, open the AD FS MMC snap-in and go to Application Groups.
  2. Click Add application group.
  3. On the Welcome page, enter a name such as powershell-test and select Server application. Then click Next.
  4. On the Server application page, enter a client identifier such as powershell-test – this will be the client_id in the OAuth request.
  5. Add a redirect URI such as http://localhost/. The URI won’t be used, so it doesn’t matter what you use. Then click Next.
  6. On the Configure application credentials page, check Integrated Windows Authentication and select your own user.
  7. Complete the remaining steps of the dialog.

We also need an OIDC resource to test with:

  1. Go to Application Groups again and click Add application group.
  2. On the Welcome page, enter a name such as powershell-test-api and select Web API. Then click Next.
  3. On the Configure Web API page, enter an identifiers such as http://powershell-test-api.example.com/ and click Next.
  4. On the Apply access control policy page, select Permit everyone for now and click Next.
  5. On the Configure application permissions page, add the OIDC client we created previously (powershell-test) and ensure that openid is checked in the list of permitted scopes. Then click Next.
  6. Complete the remaining steps of the dialog.

Now let’s log in to a domain-joined machine and request an AD FS token by using the following PowerShell command:

Invoke-RestMethod `
  -Uri "https://login.example.com/adfs/oauth2/token/" `
  -Method POST `
  -Body @{
    client_id = 'powershell-test'
    resource = 'http://powershell-test-api.example.com/'
    grant_type = 'client_credentials'
    use_windows_client_authentication = 'true'
    scope = 'openid'
    } `
    -UseDefaultCredentials

There are a few things to unpack about this command:

  • client_id identifies the OIDC client (Server application) in AD FS
  • resource identifies the OIDC resource (Web API) in AD FS
  • grant_type = 'client_credentials', in combination with use_windows_client_authentication = 'true', tells AD FS that we want to authenticate the client (instead of a user) and that we want to use Integrated Windows Authentication instead of passing a client_secret or assertion.

    Interestingly, the use_windows_client_authentication is not mentioned at all in the AD FS documentation, but the parameter is covered in the [MS-OAPX]: OAuth 2.0 Protocol Extensions documentation, which defines the parameter as:

    The use_windows_client_authentication parameter is optional, and can be specified by the client role of the OAuth 2.0 Protocol Extensions in the POST body when making a request to the token endpoint
    (section 3.2.5.2). The client provides a value of "true" for the use_windows_client_authentication parameter to indicate that it will authenticate via the HTTP Negotiate Authentication Scheme described in RFC4559.
  • The -UseDefaultCredentials flag instructs Invoke-RestMethod to use our current user credentials to authenticate. Alternatively, we can use -Credential to authenticate as a different user.

What we expect to happen behind the scenes is this:

Process

And indeed, it works – the command returns an access token:

access_token
------------
eyJ0eXAiOiJKV1QiLCJhbGciOi...

If we run Fidder while running the PowerShell command, we can see 2 requests to AD FS. The first request looks like this:

POST https://login.example.com/adfs/oauth2/token/ HTTP/1.1
User-Agent: Mozilla/5.0 (Windows NT; Windows NT 10.0; en-US) WindowsPowerShell/5.1.19041.1151
Content-Type: application/x-www-form-urlencoded
Host: login.example.com
Content-Length: 151
Connection: Keep-Alive
scope=openid&resource=https%3A%2F%2FIWA.example.com%2F&grant_type=client_credentials&client_id=powershell-server&use_windows_client_authentication=true

To which the server replies with a challenge:

HTTP/1.1 401 Unauthorized
Content-Length: 0
Server: Microsoft-HTTPAPI/2.0
WWW-Authenticate: Negotiate
WWW-Authenticate: NTLM
Date: Wed, 22 Sep 2021 07:59:54 GMT
Via: 1.1 google
Alt-Svc: clear
Proxy-Support: Session-Based-Authentication

The client then obtains the right Kerberos ticket and repeats the request, this time with a proper Authorization header:

POST https://login.example.com/adfs/oauth2/token/ HTTP/1.1
User-Agent: Mozilla/5.0 (Windows NT; Windows NT 10.0; en-US) WindowsPowerShell/5.1.19041.1151
Content-Type: application/x-www-form-urlencoded
Authorization: Negotiate YIIG6QYGKwYBB...
Host: login.example.com
Content-Length: 151

scope=openid&resource=https%3A%2F%2FIWA.example.com%2F&grant_type=client_credentials&client_id=powershell-server&use_windows_client_authentication=true

This time, authentication succeeds and AD FS returns an access token.

If we additionally run Wireshark, we can also see that the client requests a Kerberos ticket for the SPN of AD FS…

Kerberos request

…and receives a ticket in return:

Kerberos response

Any opinions expressed on this blog are Johannes' own. Refer to the respective vendor’s product documentation for authoritative information.
« Back to home