Just show me the code
As always if you don’t care about the post I have upload the source code on my Github.

In today’s post I want to talk about how you can secure a .NET graphQL API using Azure Active Directory (AAD).

If you want to build a graphQL API in .NET right now, the most well-known options available are the graphQL.NET implementation (https://graphql-dotnet.github.io/) and the HotChocolate one (https://chillicream.com/).

After tinkering quite a bit with both of them I decided to use the HotChocolate implementation. HotChocolate has more features, some off them are really useful like schema stitching. It is also faster and has a smaller memory footprint. And to top it off it has an steady update frequency.

In this post I won’t be talking about how you can build a graphQL API with HotChocolate, mainly because there are a lot of great examples online and writing another one seems meaningless. Instead of that I’ll be focusing on how you can secure it with AAD.

The main talking points in this post are going to be the following ones:

  • How to secure a graphQL api using the Microsoft.Web.Identity nuget package.
  • What are the introspection queries and how to secure them.
  • How to call a protected graphQL api using the GraphQL.Client and the Microsoft.Web.Client nuget packages.

1. How to secure a graphQL api using the Microsoft.Web.Identity nuget package

First thing is to register a couple of apps on AAD. In this case I have registered an app called GraphQL.WebApi and another one called GraphQL.Client.
Also the GraphQL.WebApi app exposes an scope and the GraphQL.Client app is authorized to ask for this scope.

Now it’s time to install and set up the Microsoft.Identity.Web nuget package. To install it run the command: dotnet add package Microsoft.Identity.Web --version 1.16.0

After the library is installed there are a few thing you need to do:

  1. On the appsettings.json configure how the library is going to work with AAD:
"AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "8a0671e2-3a30-4d30-9cb9-ad709b9c744a",
    "Domain": "carlosponsnoutlook.onmicrosoft.com",
    "ClientId": "2afa13f1-6873-4629-a072-c6a5792e55c3",
    "TokenValidationParameters": {
      "ValidateIssuer": true,
      "ValidIssuer": "https://sts.windows.net/8a0671e2-3a30-4d30-9cb9-ad709b9c744a/",
      "ValidateAudience": true,
      "ValidAudiences": [ "api://2afa13f1-6873-4629-a072-c6a5792e55c3" ]
    }

The 2afa13f1-6873-4629-a072-c6a5792e55c3 GUID is the GraphQL.WebApi clientID.
Also it is a good practice to always validate the iss and the aud attributes from the JWT Token, that way you can ascertain that the token has been issued by your own identity provider and the target for the token is your API.

  1. On the Startup.cs register the library dependencies using the AddMicrosoftIdentityWebApi extension method.
public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddMicrosoftIdentityWebApi(Configuration);
    ...
}

With the Microsoft.Web.Identity package put in place the API is capable to authorize your calls using the AAD.

  1. Now you need to install the HotChocolate.AspNetCore.Authorization nuget package. To install it run the command: dotnet add package HotChocolate.AspNetCore.Authorization --version 11.3.5

This library will allow us to add authorization for our graphQL types, querys and mutations.

The nice thing about this library is that is built on top of the .NET Core authentication mechanisms, so it will work hand in hand with the Microsoft.Identity.Web library without any extra code.
Once the HotChocolate.AspNetCore.Authorization is installed you need to register the library dependencies using the AddAuthorization() extension method:

 services.AddGraphQLServer()
         .AddAuthorization();

The only thing left is to choose what part of your graphQL API you want to secure. There are 4 options available here:

Option 1. You can protect the entire set of queries or mutations. To do it you need to add the Authorize attribute at class level.

Be careful! The Authorize attribute that you need is the one from the HotChocolate.AspNetCore.Authorization assembly. Do not get confused with the other one that comes from the Microsoft.AspNetCore.Authorization assembly.

[GraphQLDescription("Represents the available Book queries.")]
[ExtendObjectType(typeof(Query))]
[Authorize]
public class BookQuery
{
    public  IEnumerable<Book> GetBooks(
        [Service] IBookRepository repository)
    {
        return repository.GetBooks();
    }

    [Authorize]
    public IEnumerable<Book> GetBooksByAuthor(
        [Service] IBookRepository repository,
        string author)
    {
        return repository.GetBookByAuthor(author);
    }
}

Option 2. You can protect concrete queries or mutations. To do it you need to add the Authorize attribute at method level.
In this example only the GetBooks query is protected.

[GraphQLDescription("Represents the available Book queries.")]
[ExtendObjectType(typeof(Query))]
public class BookQuery
{
    [Authorize]
    public  IEnumerable<Book> GetBooks(
        [Service] IBookRepository repository)
    {
        return repository.GetBooks();
    }

    public IEnumerable<Book> GetBooksByAuthor(
        [Service] IBookRepository repository,
        string author)
    {
        return repository.GetBookByAuthor(author);
    }
}

Option 3. You can protect an object type. In that case you’ll need to be authorized only if you want to execute a query that returns this particular object type.
To do it you need to add the Authorize attribute in the object type if you’re using an annotation based approach.

In the example below I’m protecting the Book object type. If I try to run a query that returns a list of books without the authorization header it will return an error.

[Authorize]
public class Book
{
    public string Author { get; set; }
    public string Title { get; set; }
    public double Price { get; set; }
    public int NumberOfPages { get; set; }
}

If you’re using a code-first based approach instead of the annotation based approach, you’ll need to create a new class inheriting from ObjectType<T> and define it there.

public class BookType : ObjectType<Book>
{
    protected override void Configure(IObjectTypeDescriptor<Book> descriptor)
    {
        descriptor.Authorize();

        descriptor.Description("Represents a book entity.");

        descriptor
            .Field(c => c.Author)
            .Description("The author of the book.");

        descriptor
            .Field(c => c.Title)
            .Description("The title of the book.");

        descriptor
            .Field(c => c.Price)
            .Description("Book price on Euros.");

        descriptor
            .Field(c => c.NumberOfPages)
            .Description("How many pages the book has.");

        base.Configure(descriptor);
    }
}

Option 4. You can protect a concrete object attribute. In that case you’ll need to be authorized only if you want to execute a query that returns this particular attribute.
To do it you need to add the Authorize attribute at attribute level if you’re using an annotation based approach.

In the example below I’m protecting the Title attribute of the Book object type. If I try to run a query that returns a list of books without the authorization header and I ask to get back the title field it will return an error.
If I run the same query but I don’t ask for the title field it would work.

public class Book
{
    public string Author { get; set; }
    [Authorize]
    public string Title { get; set; }
    public double Price { get; set; }
    public int NumberOfPages { get; set; }
}

If you’re using a code-first based approach instead of the annotation based approach, you’ll need to create a new class inheriting from ObjectType<T> and define it there.

public class BookType : ObjectType<Book>
{
    protected override void Configure(IObjectTypeDescriptor<Book> descriptor)
    {
        descriptor.Description("Represents a book entity.");

        descriptor
            .Field(c => c.Author)
            .Description("The author of the book.");

        descriptor
            .Field(c => c.Title)
            .Description("The title of the book.")
            .Authorize();

        descriptor
            .Field(c => c.Price)
            .Description("Book price on Euros.");

        descriptor
            .Field(c => c.NumberOfPages)
            .Description("How many pages the book has.");

        base.Configure(descriptor);
    }
}

Testing the graphQL API

If we try to invoke the graphQL API without the authorization header:

curl -g -k -X POST -H "Content-Type: application/json" \
-d '{"query":"query{ books { title author}}"}' \ 
https://localhost:5001/graphql

Then the API responds with an error:

{"errors":[{"message":"The current user is not authorized to access this resource.","locations":[{"line":1,"column":8}],"path":["books"],"extensions":{"code":"AUTH_NOT_AUTHENTICATED"}}],"data":{"books":null}}

Now let’s fetch a token from the AAD. One of fastest ways to do it is by starting an implicit flow by building the URI.

https://login.microsoftonline.com/8a0671e2-3a30-4d30-9cb9-ad709b9c744a/oauth2/v2.0/authorize
?client_id=a895c416-cfb9-4df0-9051-190febbbdf64
&redirect_uri=https%3A%2F%2Foidcdebugger.com%2Fdebug
&scope=api%3A%2F%2F2afa13f1-6873-4629-a072-c6a5792e55c3%2Fissuer.readwrite
&response_type=token
&response_mode=form_post&nonce=w6esugjg4wf

And when invoking the API with the Authorization header

curl -g -k -X POST -H "Content-Type: application/json" \
-H "Authorization: Bearer add-your-token-here" \
-d '{"query":"query{ books { title author}}"}' \ 
https://localhost:5001/graphql

It responds with the expected result:

{"data":{"books":[{"title":"Learning Go","author":"Jon Bodner"},{"title":"Python Crash Course","author":"Eric Matthes"},{"title":"Clean Architecture","author":"Robert C Martin"}]}}

2. What are the introspection queries and how to secure them.

The introspection query enables anyone to query a GraphQL server for information about the schema.
With this query you can gain information about the directives, available types, and available operation types.

The introspection query is used primary as a discovery mechanism.

Probably you’re not going to run the query by yourself, but nonetheless it’s important for tooling and GraphQL IDEs like GraphiQL or Banana Cake Pop. Behind the scenes, GraphQL IDEs uses the introspection queries to offer a rich user experience and for diagnosing your graph during development.

When you move your API to production it doesn’t seem a good idea to leave the introspection query unprotected, it might reveal some data or give extra information to a malicious user.

I’m going to show you a couple of options available if you want to protect the introspection query in your graphQL API.

Option 1. Protect the introspection query using a custom header.

With this option we’re allowing introspection queries only if the request contains a certain custom header.

You’ll need to build a custom interceptor that intercepts any Http request and inspects if the header is present or not. Here’s how it looks.

public class ConditionalIntrospectionHttpRequestInterceptor : DefaultHttpRequestInterceptor
{
    private readonly IConfiguration _configuration;

    public ConditionalIntrospectionHttpRequestInterceptor(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public override async ValueTask OnCreateAsync(HttpContext context,
        IRequestExecutor requestExecutor,
        IQueryRequestBuilder requestBuilder,
        CancellationToken cancellationToken)
    {
        string header = context.Request.Headers["X-INTROSPECTION-QUERY"];
        var key = _configuration["IntrospectionKey"];

        if (header != null &&
            key != null &&
            header.Equals(key, StringComparison.InvariantCultureIgnoreCase))
            requestBuilder.AllowIntrospection();
        else
            requestBuilder.SetIntrospectionNotAllowedMessage($"The introspection query has been disabled.");

        await base.OnCreateAsync(context, requestExecutor, requestBuilder, cancellationToken);
    }
}

Also you need the register the dependencies like these:

services.AddGraphQLServer()
            .AddIntrospectionAllowedRule()
            .AddHttpRequestInterceptor(_ => new ConditionalIntrospectionHttpRequestInterceptor(configuration));

The AddIntrospectionAllowedRule() extension method is blocking each and every one of the introspection queries that the server receives. And with the ConditionalIntrospectionHttpRequestInterceptor we are only letting through the introspection queries that contains our custom header.

Option 2. Protect the introspection query with AAD.

This is more of a side effect when trying to protect the entire query set. In the previous section I have showed you that you could protect the entire query or mutation set by placing the Authorize attribute at class level. Like this:

[GraphQLDescription("Represents the available Book queries.")]
[ExtendObjectType(typeof(Query))]
[Authorize]
public class BookQuery
{
    public  IEnumerable<Book> GetBooks(
        [Service] IBookRepository repository)
    {
        return repository.GetBooks();
    }

    [Authorize]
    public IEnumerable<Book> GetBooksByAuthor(
        [Service] IBookRepository repository,
        string author)
    {
        return repository.GetBookByAuthor(author);
    }
}

Placing the Authorize attribute at class level will protect the entire query set including the introspection query.

3. How to call a protected graphQL api using the Microsoft.Web.Client nuget package.

The HotChocolate suite has a graphQL client named Strawberry Shake, but it’s a very opinionated client.
To use it you’ll need to:

  • Create a graphqlrc.json file that specfies where the schema can be fetched.
  • Create a .graphql file that contains the query you want to execute.
  • Execute the dotnet build command. This will auto-generate a bunch of files and amongst them there will generate a csharp client that you can use to call the graphQL API.

I’m not a big fan of Strawberry.Shake, usually I prefer to use the GraphQL.Client nuget package. It’s built on top of the C# HttpClient implementation and works exactly like an HttpClient.

  • Create a new instance of the graphQL client.
  • Specify the URI and the query.
  • Ran it.

To show you how to call a protected graphQL API I have created a secondary web API and I’ll use it to call the protected graphQL server.

To call the protected graphQ server I will be using an OAuth2 Client Credentials flow and for that purpose I need to install the Microsoft.Identity.Client package.

After the library is installed there are a few thing you need to do:

  1. On the appsettings.json I need to configure how the library is going to work with AAD:
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/8a0671e2-3a30-4d30-9cb9-ad709b9c744a",
    "ClientId": "a895c416-cfb9-4df0-9051-190febbbdf64",
    "ClientSecret": "J.6l-VR2lefhipuJSB",
    "Scopes": [ "api://2afa13f1-6873-4629-a072-c6a5792e55c3/.default" ]
  } 

As I have stated in the previous section I have registered in my AAD an app called GraphQL.WebApi and another one called GraphQL.Client.
The a895c416-cfb9-4df0-9051-190febbbdf64 is the GraphQL.Client ClientId and the api://2afa13f1-6873-4629-a072-c6a5792e55c3/.default is the default scope from the GraphQL.WebApi.

  1. Register the Microsoft.Identity.Client configuration and the graphQL.Client implementation into the DI container:
services.AddScoped<IGraphQLClient>(sp =>
    new GraphQLHttpClient(new GraphQLHttpClientOptions
        {
            EndPoint = new Uri(Configuration["GraphQLURI"]),
            HttpMessageHandler = new AuthorizationHandler(       
                sp.GetRequiredService<IOptions<AzureAdConfig>>()),

        }, new SystemTextJsonSerializer())
);

services.AddOptions<AzureAdConfig>()
    .Bind(Configuration.GetSection("AzureAd"));

To fetch a JWT token from my AAD I’ll be using an HttpMessageHandler. The implementation of the DelegatingHandler looks like this:

public class AuthorizationHandler : DelegatingHandler
{
    private readonly IOptions<AzureAdConfig> _config;

    public AuthorizationHandler(IOptions<AzureAdConfig> config, HttpMessageHandler inner = null) 
        : base(inner ?? new HttpClientHandler())
    {
        _config = config;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, 
        CancellationToken cancellationToken)
    {
        var app = ConfidentialClientApplicationBuilder.Create(_config.Value.ClientId)
            .WithClientSecret(_config.Value.ClientSecret)
            .WithAuthority(new Uri(_config.Value.Authority))
            .Build();

        var token = await app.AcquireTokenForClient(_config.Value.Scopes)
            .ExecuteAsync(cancellationToken);

        request.Headers.Authorization = new AuthenticationHeaderValue("bearer", token.AccessToken);
        return await base.SendAsync(request, cancellationToken);
    }
}

With everything put in place it’s time to create a graphQL query, like this one:

public static class GetBooksByAuthor
{
    public const string Value =
        @"
        query GetByAuthor ($author: String!) {
            getBooksByAuthor: booksByAuthor(author: $author) {
                author
                title
                numberOfPages
                price
            }
        }";
}

And try to call the protected graphQL API, like this:

[ApiController]
[Route("[controller]")]
public class BooksController : ControllerBase
{
    private readonly ILogger<BooksController> _logger;
    private readonly IGraphQLClient _client;

    public BooksController(
        ILogger<BooksController> logger, 
        IGraphQLClient client)
    {
        _logger = logger;
        _client = client;
    }

    [HttpGet("{author}")]
    public async Task<ActionResult> GetBooksByAuthor(
        [FromRoute]string author)
    {
        try
        {
            var query = new GraphQLRequest
            {
                Query = Queries.GetBooksByAuthor.Value,
                Variables = new
                {
                    author = author
                }
            };

            var result = await _client.SendQueryAsync<GetBooksByAuthorData>(query);

            if (result.Errors != null && result.Errors.Any())
            {
                return StatusCode(StatusCodes.Status500InternalServerError);
            }

            return Ok(result.Data.Books);
        }
        catch (Exception e)
        {
            _logger.LogError("Something went wrong", e);
            return StatusCode(StatusCodes.Status500InternalServerError, "Something went wrong");
        }
    }
}