Working with signed JWTs (OAuth with certificates)
Published Feb 13 2019 09:38 AM 6,761 Views
Iron Contributor
First published on MSDN on Dec 10, 2018

Authored by Andreas Helland


How do I use certificates for authenticating against an ADFS server while using OAuth as a trusted client? Simple question right? Yes, but unfortunately it still took me a little work to land on the relevant pages in an Internet search, and subsequently getting it all to work. (Actually most things here apply to Azure Active Directory as well, but I worked it from the ADFS angle.)

There is actually a decent explanation here of how to set up the certificate-based authentication on the ADFS server:
https://blogs.technet.microsoft.com/cloudpfe/2017/10/16/oauth-2-0-confidential-clients-and-activ...

As explained in that article the certificates aren't used for establishing the SSL/TLS connection itself, but rather using it for the payload. Which coincidentially solves a lot of issues involved in getting a web server to "speak" the client certs language.

The client code shown can seem a bit daunting however, and can be made much easier if you don't want to go the low-level route. Actually you can make it work pretty much the same as if you are working with Azure AD on the client-side.

Digging further there is a fairly thorough explanation of certificates with Azure AD apps here:
https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-certificate-credential

Now, if we could only combine the two…

First things first, to authenticate with a certificate we need to have a certificate to present to the identity provider. Since I happen to have Visual Studio installed I open up the "Developer Command Prompt" to use the makecert utility. This is to generate certificates in files rather than using the certificate store in Windows. (Making it suitable both for running containerized on Linux, and for something like Azure Key Vault should you want to put your code in the cloud.)

[code language="csharp"]
makecert -r -pe -n "CN=adfs.contoso.com" -sky exchange -sv adfs.contoso.com.pvk adfs.contoso.com.cer

pvk2pfx -pvk adfs.contoso.com.pvk -spc adfs.contoso.com.cer -pfx adfs.contoso.com.pfx
[/code]


You will also be prompted to specify a password for the certificate. I skipped this to make the lab exercise simpler.


With a certificate on the client we should also be able to use it for acquiring a token. Before doing so make sure that the certificate you just generated is trusted on your ADFS Server. (Assumption being that you have created the basic app group setup on ADFS.)


Let's keep the token stuff as simple as possible. I generate a dotnet console app on the command line, and then fire up Visual Studio Code:


You need ADAL so throw that into SignedJWT.csproj:
[code language="csharp"]
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="4.4.0" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
</ItemGroup>

</Project>
[/code]

Why not MSAL which seems to be the general recommendation for authentication libraries from Microsoft? Well, MSAL doesn't support ADFS yet so for now we will use ADAL.

The main code can also be kept simple:
[code language="csharp"]
Program.cs

using System;
using System.Security.Cryptography.X509Certificates;
using Microsoft.IdentityModel.Clients.ActiveDirectory;

namespace SignedJwt
{
class Program
{
static void Main(string[] args)
{
string adfsInstance = "https://contoso.com/adfs/";
string ResourceId = "https://contoso.com/api";
string clientId = "copy-from-adfs-server";
string clientSecret = "copy-from-adfs-server";

ClientAssertionCertificate certCred = null;
AuthenticationContext authContext = null;
var authority = $"{adfsInstance}";

authContext = new AuthenticationContext(authority, false);

X509Certificate2 cert = new X509Certificate2("adfs.contoso.com.pfx");

var token = "";
AuthenticationResult result = null;
try
{
ClientCredential clientCred = new ClientCredential(clientId, clientSecret);
certCred = new ClientAssertionCertificate(clientId, cert);
result = authContext.AcquireTokenAsync(ResourceId, certCred).Result;
token = result.AccessToken;
}
catch (Exception x)
{
Console.WriteLine($"Error: {x.Message}");
}
Console.WriteLine($"Token: {token}");
}
}
}
[/code]

Hopping back to the command line you can execute the app with dotnet run :


That's all there is to it really when it comes to acquiring a token based on a certificate. And in case you're using the ADFS Web Application Proxy the code works with that too.

How do we know this actually worked, aside from the fact that we get a token in return? Can we validate the token somehow? So happy that you asked!

Let's create a simple API for this purpose:



Let's start with the task of just doing a raw dump of the certificate and return a parsed output of the claims:
[code language="csharp"]
[HttpGet]
[Route("Parse")]
public ActionResult<IEnumerable<string>> Parse()
{
var token = string.Empty;

//The token can be passed either via query string or headers
if (HttpContext.Request.QueryString.Value.Contains("token"))
{
token = HttpContext.Request.Query["token"].ToString();
}
if (HttpContext.Request.Headers["Authorization"].ToString() != null)
{
token = HttpContext.Request.Headers["Authorization"];

//Remove "Bearer " from string
token = token.Substring(7);
}
//No token equals bad request
else
{
return BadRequest("Missing something?");
}

//Let's try to treat it like a token
var jwtHandler = new JwtSecurityTokenHandler();
var jwtInput = token;

//Check if readable token (string is in a JWT format)
var readableToken = jwtHandler.CanReadToken(jwtInput);

if (readableToken == true)
{
var jwtoken = jwtHandler.ReadJwtToken(jwtInput);

var header = jwtoken.RawHeader;
byte[] hData = Convert.FromBase64String(header);
string hDecodedString = Encoding.UTF8.GetString(hData);

//.NET needs some padding to Base64 decode
var payload = jwtoken.RawPayload + "==";
byte[] pData = Convert.FromBase64String(payload);
string pDecodedString = Encoding.UTF8.GetString(pData);

return Content("[" + hDecodedString + "," +
pDecodedString + "]",
"application/json");
}
if (readableToken != true)
{
//The token doesn't seem to be in a proper JWT format.
//Assume it's a combo token and break it apart
string decodedString = string.Empty;
try
{
byte[] data = Convert.FromBase64String(token + "=");
decodedString = Encoding.UTF8.GetString(data);
}
catch (Exception)
{
//If this fails we'll just assume bogus input
return BadRequest("Not able to figure out this token");
}

//The tokens are separated with a comma
var tokens = decodedString.Split(',');

//Sort out the proxy token first
var proxyToken = tokens[0];
proxyToken = proxyToken.Substring(16);
proxyToken = proxyToken.Substring(0, proxyToken.Length - 1);

var pToken = jwtHandler.ReadJwtToken(proxyToken);

var ptHeader = pToken.RawHeader;
byte[] ptHeaderData = Convert.FromBase64String(ptHeader);
string ptHDecodedString = Encoding.UTF8.GetString(ptHeaderData);

var ptPayload = pToken.RawPayload;
//.NET needs extra padding to do Base64 decode
byte[] ptPayloadData = Convert.FromBase64String(ptPayload + "==");
string ptTDecodedString = Encoding.UTF8.GetString(ptPayloadData);

//Figure out the access token
var accessToken = tokens[1];
accessToken = accessToken.Substring(16);
accessToken = accessToken.Substring(0, accessToken.Length - 2);

var aToken = jwtHandler.ReadJwtToken(accessToken);

var atHeader = aToken.RawHeader;
byte[] atHeaderData = Convert.FromBase64String(atHeader);
string atHDecodedString = Encoding.UTF8.GetString(atHeaderData);

var atPayload = aToken.RawPayload;
//.NET needs extra padding to do Base64 decode
byte[] atPayloadData = Convert.FromBase64String(atPayload + "==");
string atTDecodedString = Encoding.UTF8.GetString(atPayloadData);

return Content("[" + ptHDecodedString + "," +
ptTDecodedString + "," +
atHDecodedString + "," +
atTDecodedString + "]",
"application/json");
}

return new string[] { "How did you end up here?" };
}
[/code]

Since the console app we created to acquire the token didn't actually do anything but print out to the command line I chose to call this "API" through Fiddler, but you can of course extend the token acquisition app to do a subsequent HTTP call instead. A slightly modified output (readability and removal of ids) looks like this:
[code language="csharp"]
[{
"typ": "JWT",
"alg": "RS256",
"x5t": "..."
},{
"aud": "urn:AppProxy:com",
"iss": "http://adfs.contoso.com/adfs/services/trust",
"iat": 1543769125,
"exp": 1543772725,
"relyingpartytrustid": "...",
"clientreqid": "...",
"authmethod":
["http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/tlsclient",
"http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/x509"],
"auth_time": "2018-12-02T16:45:25.055Z",
"ver": "1.0"
},{
"typ": "JWT",
"alg": "RS256",
"x5t": "..."
},{
"aud": "https://contoso.com/api",
"iss": "http://adfs.contoso.com/adfs/services/trust",
"iat": 1543769125,
"exp": 1543772725,
"authmethod":
["http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/tlsclient",
"http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/x509"],
"apptype": "Confidential",
"appid": "...",
"auth_time": "2018-12-02T16:45:25.055Z",
"ver": "1.0"
}]
[/code]

Sure, it could have been formatted in a prettier way, but that's not the main point here. The takeaways here are that it says that the authentication was done with a certificate (x509), and that in this case we have a combo token. There's one cert for the ADFS proxy, and one regular access token.

Ok, perhaps this requires a little bit explanation in case you're not familiar with the proxy. The purpose of the proxy is (usually) to add a pre-authentication step so that you don't get to talk to the app unless you have been pre-approved. The way ADFS implementes this is basically having the proxy generate one token as stamp of approval, and letting the traffic through to the backend ADFS server letting it add another token so you have a net of two tokens that are bundled together.

As evidenced by the above code handling the combo token is sort of messy. From the client developer's perspective this should not be noticeable. You request a token, you get something back, you present that to the API you are calling. You should not take a dependency on parsing access tokens. Parsing identity tokens is a different matter, but the access token is something you should perceive as an opaque base64 string.

The API accepting the token might have a different perspective, which brings us to the next step.

This wasn't exactly validation of the token though - we just accepted an input and returned some text. What if we want to actually validate that the token is ok, and do an actual authorization?

Let's do that by modifying Startup.cs which contains the auth middlewares.
[code language="csharp"]
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.MetadataAddress = "https://adfs.contoso.com/adfs/.well-known/openid-configuration";
options.Validate();

options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidIssuer = "http://adfs.contoso.com/adfs/services/trust",
ValidateAudience = true,
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ValidAudience = "https://contoso.com/api",
RequireSignedTokens = true,
ValidateActor = true,
};
});

services.AddAuthorization(options =>
{
options.AddPolicy("Certificate", policy =>
policy.RequireAssertion(context =>
context.User.HasClaim(c =>
(c.Type == System.Security.Claims.ClaimTypes.AuthenticationMethod &&
c.Value == "http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/tlsclient" ||
c.Value == "http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/x509" ))));
});

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseMvc();
}
[/code]

There are a couple of things to note here.
We're using the JwtBearer middleware since this is an API and not an interactive flow which mean we're not redirecting to a web page for signin.

Even though we pull metadata from the openid-configuration endpoint we still need to set the valid issuer in the validation parameters. This is a quirk of ADFS where the metadata contains issuer=https://adfs.contoso.com/adfs and access_token_issuer=http://adfs.contoso.com/adfs/services/trust so we need to accept specifically for access tokens here.

You will also notice that we create a policy for authentication method. The token is validated and can be found ok regardless of how you acquired the token. However since part of the point of our exercise is to use certificates we use that as an authorization parameter enabling us to reject password-based authentication attempts. (You can choose per API endpoint or controller if you want to use the policy so it doesn't mean you're blocked from also accepting other authentication mechanisms.)

To use the policy we attach it to an API controller:

[code language="csharp"]
[HttpGet]
[Authorize(Policy = "Certificate")]
[Route("Validate")]
public ActionResult<IEnumerable<string>> Validate()
{
var token = "{";
foreach (var claim in User.Claims)
{
//Datetimes are already escaped
if (claim.Type.ToString().Contains("time"))
{
token += $"\"{claim.Type}\":{claim.Value},";
}
else
{
//Let's not care about the authentication method here
//since that requires an array for building valid json
if (claim.Type == ClaimTypes.AuthenticationMethod)
{ }
else
{
token += $"\"{claim.Type}\":\"{claim.Value}\",";
}
}
}
//Remove the surplus comma
token = token.Substring(0, token.Length - 1);
token += "}";

return Content(token, "application/json");
}
[/code]

Yes, this is really pretty code for parsing the token, I know :) (You're not likely to actually work with the token like this in real life though.)

The output is still the contents of the token, but this time you're not allowed to see the claims until you have authenticated yourself properly. (There's nothing inherently secret about tokens, but it looks better than returning "Hello World".)

If you try to pass the same combo token to this API endpoint you will get an error in return - "invalid token". So, the middleware doesn't seem to be created for dealing with this scenario. If you set up your API behind the ADFS proxy, (and publish through the corresponding wizard), it isn't going to be a problem either because the proxy takes care of it for you. Whether this is the right choice for you depends on other factors as well so it's hard to say which path you should take. You could of course take care of it yourself, but apart from the initial auth that means the subsequent requests are not pre-authenticated. Or maybe you have a different proxy product you pipe your traffic to before hitting ADFS. Either way; this means that this part of the code only works with "pure" tokens from ADFS, but it should be possible to build the rest as an exercise if you like.

The complete code sample can be found here:
https://github.com/ahelland/AADGuide-CodeSamples/tree/master/CoreWebAPISignedJWTs

4 Comments
Copper Contributor

Hi Andreas,

 

Thanks for the useful post. Need your help on this. I have a scenerio where I should use jwt certificate to authenticate. Here I am using source as Salesforce rest API and Target as gen2 storage. Now i have to use azure datafactory to pull and load data but the authentication should happen through the certificate. 

 

Salesforce architect expecting public key and certificate csr file to upload this is Salesforce rest API.

I've tried lot of things like azure key vault ...etc but nothing worked out.

 

Could you please guide me how to generate a JWT public key and CA signed certificate in azure.

Iron Contributor

Hmm, notification support must not be working as I didn't get pinged about your comment.

 

Azure Key Vault should be happy generating certs for you, but you can also create them locally and upload the Key Vault. For instance in Windows:

$cert = New-SelfSignedCertificate -Type Custom -Subject "CN=MySelfSignedCertificate" -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3") -KeyUsage DigitalSignature -KeyAlgorithm RSA -KeyLength 2048 -NotAfter (Get-Date).AddYears(2) -CertStoreLocation "Cert:\CurrentUser\My" 

 

Followed by exporting (with private key) through MMC, and uploading to portal. You could separately export the public key. It's self-signed, so if it needs to be trusted by a public CA that's not going to work. 

 

(Details here: https://techcommunity.microsoft.com/t5/azure-developer-community-blog/generating-azure-ad-quot-look-...)

Copper Contributor

Hi @Andreas Helland 

 

I am reading the guide - a lot to take for me as I am new to all of this so apologies in advanced if I ask silly questions. I am wondering if you can help me out please.

 

Do you know if it would be possible user a certificate to sign a JWT and have ADFS verify it comes from one of the many users in the AD? I was also reading the guide https://learn.microsoft.com/en-gb/archive/blogs/cloudpfe/oauth-2-0-confidential-clients-and-active-d... you mentioned and this doesn't meet my needs unfortunately as it involves manually uploading a certificate (and I would need to manually upload many for different users and maintain this)

 

Iron Contributor

@broberts1 

This is an article I haven't touched in a long time :)

First things first; the code is using ADAL so I do not recommend using that - MSAL would be the way to go now. (And of course the C# code is not of the latest version either.)

I don't remember which version of ADFS I was testing against, but logic has it that it would be Windows Server 2016 looking at the time stamp. So, there would be new versions of ADFS as well since this article.

I have new samples on both JWT generation and validation though.

Now, your question doesn't really touch this directly so that's more about painting the back drop.

The approach in the article quite correctly does not scale to a large number of certificates. For a use case where you have a few server side apps this might not be a problem, but if the use case is that you have a large number of users you probably don't want to generate or upload them like this.

To be honest I have not researched what non-interactive ways to upload might exist in ADFS 2022, and I don't know if there is a maximum of how many certs can be uploaded.

Your question isn't silly - I see how client certs is an upgrade over passwords in many ways. It's just so painful to work with client certs :)

I don't have the perfect solution right off the bat. Initially guessing I would like into things like smartcards (that have a plug-in for ADFS), or creating your own plug-in for that matter. The recommendation on a higher level would be to look into solving things with Azure AD instead of ADFS regardless of certificates, but that's potentially a bigger task to solve.
Version history
Last update:
‎Feb 13 2019 09:38 AM
Updated by: