D
Dennis Tretyakov

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.

TLDR;

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.

© 2020 - 2024, Dennis Tretyakov