ID Token validation in dotnet
Today I’ve got a challenge. On one of the projects I’m working on, by business reasons we don’t want users to login user 3rd IDP’s. However, we have a requirement to validate user’s identity with BankID via criipto.com which provides integration via OpenID.
What means that user should authenticate with our authority registered in criipto.com and pass issued ID-Token to the backend as a proof of identity. There are many tools that adds OpenID support to dotnet authentication pipeline, however it took me a while to find tools to specifically validate ID-Token.
If that’s what u happened to need, below will be an example, with some explanations after.
Code example
You’ll need IdentityModel.OidcClient.IdentityTokenValidator package.
using System.Net.Http;
using System.Threading.Tasks;
using IdentityModel.Client;
using IdentityModel.OidcClient;
using Xunit;
namespace IdTokenValidation;
public class ValidateTokenExample
{
// Obviously constant values must be replaced for test to pass
const string Authority = "AUTHORITY";
const string ClientId = "CLIENT-ID";
const string IdToken = "ID-TOKEN";
[Fact]
public async Task Validate()
{
// GetDiscoveryDocumentAsync extension comes from IdentityModel.OidcClient namespace
// from IdentityModel.OidcClient package.
DiscoveryDocumentResponse? doc = await new HttpClient().GetDiscoveryDocumentAsync(Authority);
Assert.False(doc!.IsError);
var validator = new JwtHandlerIdentityTokenValidator();
var options = new OidcClientOptions
{
ClientId = ClientId,
ProviderInformation = new ProviderInformation
{
IssuerName = doc.Issuer,
KeySet = doc.KeySet
}
};
IdentityTokenValidationResult? result = await validator.ValidateAsync(IdToken, options);
Assert.False(result.IsError);
}
}
Just a small note here: normally you would cache discovery documents, here it’s not done for the sake of simplicity.
Caveats
Even tho ValidateAsync
method has async signature,
I was assuming giving it an authority in OidcClientOptions
would be enough.
I assumed it will fetch discovery document and will take a key from there.
Unfortunately, despite async signature it doesn’t do so.
It performs synchronous validation based on data passed in,
therefore you need to fetch document yourself like and pass KeySet in to provider information,
like on the example above.
Endpoint is on a different host than authority
By default GetDiscoveryDocumentAsync
doesn’t allow discovery document segments from domains that are not matching authority.
For example in case of google (Authority: https://accounts.google.com/)
opening discovery document root https://accounts.google.com/.well-known/openid-configuration you may see that some endpoints
have different domains. Like token_endpoint
will be at https://oauth2.googleapis.com/token and jwks_uri
will be at https://www.googleapis.com/oauth2/v3/certs
Therefore, there is an overload for GetDiscoveryDocumentAsync
where you can specify validation policy,
and either whitelist allowed domains either disable endpoint validation at all, like on the examples below.
using System.Net.Http;
using System.Threading.Tasks;
using IdentityModel.Client;
using Xunit;
namespace IdTokenValidation;
public class FetchGoogleDocumentExample
{
[Fact]
public async Task UsingWhitelist()
{
var opt = new DiscoveryDocumentRequest
{
Address = "https://accounts.google.com/",
Policy =
{
AdditionalEndpointBaseAddresses = { "https://oauth2.googleapis.com/", "https://www.googleapis.com/", "https://openidconnect.googleapis.com/" } }
};
DiscoveryDocumentResponse? doc = await new HttpClient().GetDiscoveryDocumentAsync(opt);
Assert.False(doc!.IsError);
}
[Fact]
public async Task WithEndpointValidationDisabled()
{
var opt = new DiscoveryDocumentRequest
{
Address = "https://accounts.google.com/",
Policy = { ValidateEndpoints = false } };
var http = new HttpClient();
DiscoveryDocumentResponse? doc = await http.GetDiscoveryDocumentAsync(opt);
Assert.False(doc!.IsError);
}
}
In case of issues, you might want check example repository at https://github.com/tretyakov-d/playground-id-token-validation.