ID Token validation in dotnet
Tags: dotnet, openid
Published: 2022-08-24 11:17:23


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](https://www.bankid.com/en) via [criipto.com](https://www.criipto.com/)
which provides integration via OpenID.

What means that user should authenticate with our authority registered in [criipto.com](https://www.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.

## TLDR

> You'll need [IdentityModel.OidcClient.IdentityTokenValidator](https://www.nuget.org/packages/IdentityModel.OidcClient.IdentityTokenValidator) package.

```csharp
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.

```csharp
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.<br />

I hope it helped