ADFS 3.0

Active Directory Federation Service (ADFS) is a federated identity service using Active Directory (AD) as the identity provider (IdP).

Microsoft initially created ADFS to enable single sign-on for windows based applications using Active Directory (AD) as the identity service. Users that have authenticated once via AFDS can share the authentication with other ADFS enabled applications (when configured).

Starting with Windows Server 2012 R2, ADFS was extended to support additional authentication protocols such as SAML2.0, OAuth, OAUTH2.0 and OpenIdConnect to promote ADFS’ compatibility with other applications.

ADFS 3.0 for OAuth JWT

OAUTH 2.0 defines various authorization grants, client and token types but ADFS 3.0 only supports a subset of these including Authentication Code flow. Microsoft increased the support for additional OAUTH2.0 protocols and frameworks such as OpenIDConnect (OIDC) with Windows Server 2016 ADFS 4.0. (source)

Grant typeADFS 3.0ADFS 4.0
Authorization code grantY(es)Y
Implicit grantN(o)Y
Resource owner credentialsNY
Client credentialsNY
Client typesADFS 3.0ADFS 4.0
Public Client N Y
Confidential Client NN

In addition, Microsoft’s client-side authentication library known as MSAL.js does not have out-of-box client libraries that support SPA applications for OAUTH2 Authentication Code flow. Though some client authentication frameworks such as adal.js will work with ADFS 3.0.

The following are steps needed to obtain a valid access_token from ADFS using the OAuth authentication Code flow (source):

  1. Resource owner (RO) requests authentication from the OAUTH client application.
  2. OAUTH client application redirects resource owner to ADFS credentials login page.
  3. Authenticated RO gets redirected with a token that can be used to retrieve a linked access_token.
  4. Client application requests access_token using the linked token.
Depiction of the authentication flow between the user, user-agent (SPA), web-api and the authorization server (ADFS) source

source – protecting .Net Core SPA application
source – setup ADFS to issue OAuth tokens
source – AFDS and OAuth2 possible ? Demo: setup ADFS 3.0 to use OAUTH 2.0
source – jwt support for adfs

Demo: using ADFS 3.0 to secure SPA web application

This demo will cover how to setup ADFS to use OAUTH2.0 for authentication using the Authorization Code flow. The application we are securing consists of a React front end and a .Net Core 2 API. The API will be using JwtBearerAuthentication to validate the JWT generated from ADFS federated identity.

Below is an overview of the key steps involved:

1) Authorization request
– React (font-end UI) redirects user to ADFS authorization end point

GET /adfs/oauth2/authorize?response_type=code&client_id=application_identifier&resource=relying_party_trust_identifier&redirect_uri=application_callback_url

2) User login challenge
– ADSF presents a login screen where the user can log in with credentials
– Once the user is authenticated, ADFS issues session cookie and redirects the user back to React application along with the token.

HTTP/1.1
Host: adfs_sever_hostname

3) Authorization Grant
– React extracts code token

HTTP/1.1 302 Found
Location: application_callback_url?code=autorization_code

4) Access token request (via .Net Core API)
– Using the extracted code token we request an access_token

POST /adfs/oauth2/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Host: adfs_sever_hostname
Content-Length: length_of_content

grant_type=authorization_code&client_id=application_identifier&redirect_uri=application_callback_url&code=autorization_code

Using React to request access_token will likely not work if the client is a SPA running on a different (sub)domain than ADFS due to CORS. (source)
Two options:
1. Enable CORS on ADFS (requires a web server that supports CORS)
2. Create a new endpoint proxy on .Net Core API to request access_token to bypass CORS issue.

5) ADFS returns the access_token to (.Net Core API)

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
{
    "access_token":access_token,
    "token_type":"bearer",
    "expires_in":6000
}

source – ADFS 3.0 and OAuth example

ADFS configuration

  1. Add Replying Party Trust (RPT)
    • Follow the instructions
    • Leave encryption details blank (ADFS 3.0 does not support)
    • Leave WS and SAML configuration blank (Only OAuth needed)
    • Add RPT Identifier(user application domain name)
  2. Add AdfsClient to RT
  3. Enable JWT for RT
  4. Add AD claims (Optional)
# Check if RPT name is unique
Get-AdfsRelyingPartyTrust -Name "rpt_application_name"

# Add Adfs client to RPT (ClientId=generated GUID)
Add-AdfsClient -RedirectUri "http://example.com/token" -ClientId "00000000-0000-0000-0000-000000000000" -Name "any_client_name"

# Check the created Adfs client
Get-AdfsClient -Name "any_client_name"

# Enable JWT for RPT
Set-AdfsRelyingPartyTrust -TargetName "rpt_application_name" -EnableJWT $true

ADFS 3.0 does not support secrets or token encryption/decryption for OAUTH2. Multiple AdfsClients can be associated with one relying party trust, each representing a different application. The identifier should be unique across all relying parties.

source – configuring ADFS for OAUTH
source – WPF example using OAUTH with ADFS 3.0
source – setup relying party trust
source – setup adfs 3.0 for OAuth2

Web application code

React SPA code

import React from 'react'
import queryString from 'query-string'

export default class TokenProvider extends React.Component {
    accesstkn = "accesstkn"
    expire = "expire"
    clientId = '00000000-0000-0000-0000-000000000000'
    clientEndPnt = window.location.origin + '/token'
    callBack = window.location.origin + '/callbackhandle'
    resource = 'https://example.com'
    adfsUrl = 'https://adfs.server.com/adfs'
    componentDidMount() {
        this.CodeTkn()
    }
    CodeTkn() {
        if (!this.IsAuthed()) {
            if (this.props != undefined && this.props.location != undefined) {
                fetch("/api/auth/token", {
                    headers: { 'Content-Type': 'application/json' },
                    method: "POST",
                    body: JSON.stringify({
                        clientId: this.clientId,
                        redirect_uri: this.clientEndPnt,
                        code: queryString.parse(this.props.location.search).code
                    })
                })
                    .then(resp => resp.json())
                    .then((data) => {
                        if (data != undefined) {
                            localStorage.setItem(this.accesstkn, data.access_token)
                            localStorage.setItem(this.expire, data.expires_in)
                            this.props.history.push("/callbackhandle");
                        }
                    })
                    .catch((err) => { localStorage.clear() })
            }
        }

        return localStorage.getItem(this.accesstkn)
    }
    SignIn() {
        let pClientId = "&client_id=" + this.clientId
        let pRedirectUri = "&redirect_uri=" + this.clientEndPnt
        let pResource = "&resource=" + this.resource
        window.location = this.adfsUrl + "/oauth2/authorize?response_type=code" + pClientId + pRedirectUri + pResource;
    }
    SignOut() {
        this.Cleanup()
        window.location = this.adfsUrl + "/ls/?wa=wsignout1.0&wreply=" + this.callBack
    }
    IsAuthed() {
        let tmp = localStorage.getItem(this.accesstkn)
        return tmp != undefined && tmp != null
    }
    getAuthHeader(method, data) {
        let codeTkn = this.CodeTkn()
        if (data) {
            return {
                method: method,
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json',
                    'Authorization': 'Bearer ' + codeTkn,
                },
                body: JSON.stringify(data)
            }
        }
        else {
            return {
                method: method,
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json',
                    'Authorization': 'Bearer ' + codeTkn,
                }
            }
        }
    }
    render() { return (
) } }

.Net Core CORS workaround

[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
        [HttpPost("token")]
        public ActionResult TokenHandler([FromBody] FromUIVM authVM)
        {
            var httpService = new HttpRequestService();
            var tkn = httpService.GetAccessToken(Configuration.TokenServer,
                authVM.clientId.ToString(),
                authVM.code,
                authVM.redirect_uri);

            return Ok(tkn);
        }
}
public class FromUIVM 
{
    public string grant_type { get { return "authorization_code"; } }
    public Guid clientId { get; set; }
    public string redirect_uri { get; set; }
    public string code { get; set; }
}
public class HttpRequestService
{
    public AccessCodeJson GetAccessToken(string url, string clientId, string code, string redirectUrl)
    {
        if (string.IsNullOrEmpty(code) || string.IsNullOrEmpty(redirectUrl))
        {
            return null;
        }

        var client = new HttpClient();
        var postData = new List<KeyValuePair<string, string>>();
        postData.Add(new KeyValuePair<string, string>("grant_type", "authorization_code"));
        postData.Add(new KeyValuePair<string, string>("client_id", clientId));
        postData.Add(new KeyValuePair<string, string>("redirect_uri", redirectUrl));
        postData.Add(new KeyValuePair<string, string>("code", code));

        AccessCodeJson accessToken = null;
        var response = client.PostAsync(new Uri(url), new FormUrlEncodedContent(postData)).Result;
        if (response.IsSuccessStatusCode && response.Content != null)
        {
            var jsonTkn = response.Content.ReadAsStringAsync().Result;
            if (jsonTkn != null)
            {
                accessToken = JsonConvert.DeserializeObject<AccessCodeJson>(jsonTkn);
            }
        }

        return accessToken;
    }
}
public class AccessCodeJson
{
    public string access_token { get; set; }
    public string token_type { get; set; }
    public int expires_in { get; set; }
    public DateTime Createdt { get; set; }
}

source – adfs CORS issue with grant=’authorization_code’

Securing .Net Core API resources

[Authorize(AuthenticationSchemes = Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerDefaults.AuthenticationScheme)]
[Route("api/[controller]")]
[ApiController]
public class ExampleSecureController : ControllerBase {
}

Configuring JwtBearerAuthentication

By default, JwtBearerAuthentication is optimized for OIDC and has built-in code to auto-configure itself by fetching FederationMetadata from the federated identity server. Any federation identity service that supports OIDC (like ADFS 4.0) will work with the build in auto configuring.

The HTTP query used to retrieve FederationMetadata :

 https://adfs_server/.well-known/openid-configuration (source)

The query for FederationMetadata will fail against any federated identity service that does not support OIDC such as ADFS 3.0. To disable this built-in behavior, pass an OpenIdConnectConfiguration object to the configuration property of the JwtBearerOptions. (source)

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(o =>
    {
        o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        var rawCertData = Convert.FromBase64String(Configuration["AdfsOAuth:SigningKey"]);
        var cert = new X509Certificate2(rawCertData);
        var signingKey = new X509SecurityKey(cert);
        options.Configuration = new OpenIdConnectConfiguration { Issuer = Configuration["AdfsOAuth:Issuer"] };
        options.Audience = Configuration["AdfsOAuth:Audience"];
        options.Authority = Configuration["AdfsOAuth:Authority"];
        options.RequireHttpsMetadata = false;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true, IssuerSigningKey = signingKey, ValidateIssuer = false, ValidateAudience = false,
        };
        options.Events = new JwtBearerEvents
        {
            OnAuthenticationFailed = context => { return System.Threading.Tasks.Task.CompletedTask; },
            OnTokenValidated = context => { return System.Threading.Tasks.Task.CompletedTask; }
        };
    });
}

Note: TokenValidationParameters shown above is only for example. You should validate as much of the token as possible.

Example configuration in appsettings.json

"AdfsOAuth": {
    "SigningKey": "x509 cert data from https://Your.ADFS.Site/FederationMetadata/2007-06/FederationMetadata.xml",
    "Issuer": "https://Your.ADFS.server/adfs/services/trust",
    "Audience": "https://Your.Site.com",
    "Authority": "https://Your.ADFS.server/adfs"
  }

source – JwtBearerAuthentication with ADFS 3.0
source – token authentication deep dive

X509 Cert data

To verify the JWT token issued from ADFS, X509 certificate data is needed. The public cert data can be obtained from:

https://Your.ADFS.Site/FederationMetadata/2007-06/FederationMetadata.xml

Example of X509Data:

<KeyDescriptor use="signing">
    <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
        <X509Data>
            <X509Certificate>SOMEUUENCDODEDSTRING=</X509Certificate>
        </X509Data>
    </KeyInfo>
</KeyDescriptor>

source – example using adfs windows server 2012
source – ADFS 3.0 OAuth2.0 against client applications

One Reply to “ADFS 3.0”

  1. Hi, nice work. I really appeaciate the information you are providing through your site, i have alwasy find it helpful. Keep up the amazing work.

Leave a Reply

Your email address will not be published. Required fields are marked *