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
- Passing a (shared) client secret
in the
client_secret
parameter – probably the most commonly used option. - Using a certificate to create and sign a JWT-formatted assertion, and
passing that in the
client_assertion
parameter.
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:
- 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 byHTTP/
. - It then goes to the domain controller to requests a Kerbeos ticket for
HTTP/login.example.com
- 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:
- On the AD FS server, open the AD FS MMC snap-in and go to Application Groups.
- Click Add application group.
- On the Welcome page, enter a name such as
powershell-test
and select Server application. Then click Next. - On the Server application page, enter a client identifier such as
powershell-test
– this will be theclient_id
in the OAuth request. - 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. - On the Configure application credentials page, check Integrated Windows Authentication and select your own user.
- Complete the remaining steps of the dialog.
We also need an OIDC resource to test with:
- Go to Application Groups again and click Add application group.
- On the Welcome page, enter a name such as
powershell-test-api
and select Web API. Then click Next. - On the Configure Web API page, enter an identifiers such as
http://powershell-test-api.example.com/
and click Next. - On the Apply access control policy page, select Permit everyone for now and click Next.
- On the Configure application permissions page, add the OIDC client we created previously
(
powershell-test
) and ensure thatopenid
is checked in the list of permitted scopes. Then click Next. - 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 FSresource
identifies the OIDC resource (Web API) in AD FSgrant_type = 'client_credentials'
, in combination withuse_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 aclient_secret
orassertion
.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:
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…
…and receives a ticket in return: