authorization icon indicating copy to clipboard operation
authorization copied to clipboard

Checking for a valid JWT and integrating with a Refresh-Token-Workflow

Open tobias-tengler opened this issue 3 years ago • 10 comments

I want to check if a user is authenticated (NOT authorized) for a specific query, resolver, etc. via a JWT Token in the Authorization Header. I also need to integrate this with a Refresh-Token-Workflow on the client. I planned on using the authMiddleware of react-relay-network-modern.

This is the code I'm using at the moment:

// GraphQLAuthExtensions.cs
public static class GraphQLAuthExtensions
{
    public static IServiceCollection AddGraphQLAuth(this IServiceCollection services, Action<AuthorizationSettings> configure)
    {
        services.TryAddSingleton<IAuthorizationEvaluator, AuthorizationEvaluator>();
        services.TryAddTransient<IValidationRule, AuthorizationValidationRule>();

        services.TryAddTransient(s =>
        {
            var authSettings = new AuthorizationSettings();
            configure(authSettings);
            return authSettings;
        });

        return services;
    }
}

// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    // ...

    services.AddHttpContextAccessor();

    services.AddGraphQL()
        .AddUserContextBuilder(context => new GraphQLUserContext { User = context.User })
        .AddSystemTextJson();

    services
        .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(_jwtKey),
                ValidateIssuer = false,
                ValidateAudience = false,
                ValidateLifetime = true,
                ClockSkew = TimeSpan.Zero
            };
        });

    services.AddGraphQLAuth(settings =>
    {
        settings.AddPolicy("Authenticated", p => p.RequireAuthenticatedUser());
    });
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // ...

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseGraphQL<ISchema>("/");
}

// ExampleQuery.cs
public class ExampleQuery : ObjectGraphType
{
    public ExampleQuery()
    {
        this.AuthorizeWith("Authenticated");

        // ...
    }
}

I have a few questions regarding this:

  1. Is this the correct way to check if a user has a valid JWT? It seems bloated for this rather simple use case, compared to just slapping an [Authorize] attribute on a method or controller with ASP.NET Core REST. In the documentation I haven't found anything useful other than the RequireAuthenticatedUser() policy.
  2. How do I return a 401 HTTP Error code to the client? At the moment I can't integrate this with a token refresh workflow, since the unauthorized response has a 200 HTTP code and the response has no real identifier that it is in fact an unauthorized response:
{
  "errors": [
    {
      "message": "You are not authorized to run this mutation.\nAn authenticated user is required.",
      "locations": [{ "line": 5, "column": 3 }],
      "extensions": { "code": "VALIDATION_ERROR", "codes": ["VALIDATION_ERROR"], "number": "authorization" }
    }
  ]
}

The returned json only tells me that an authorization error happened:

  1. The client can't differentiate if this is an Authorization error or an Authentication error and it should refresh the token.
  2. It is also unnecessarily complex to query for the error code in the json, since it is an array of errors and in one query there could be errors relating to something other than authentication.

I also know that returning different HTTP error codes is not part of the GraphQL spec, since HTTP is just an agnostic transport protocol. But a Refresh-Token-Workflow is still something almost every modern SPA will have to implement. So having a solution for this would still be nice! :)

TL;DR:

  1. How do I correctly check if the user has a valid JWT?
  2. How do I handle refreshing authentication tokens for GraphQL in .NET?

tobias-tengler avatar Jan 04 '21 11:01 tobias-tengler

Relates to https://github.com/graphql-dotnet/server/pull/480

sungam3r avatar Jan 04 '21 11:01 sungam3r

Relates to graphql-dotnet/server#480

So if I got this right, once this is merged and released I would create a custom ErrorInfoProvider, which would generate a defined error code (in the JSON result) or something similar with which the client could tell whether, if it was an authentication issue or something else?

I would have some questions relating to this:

  1. How would I register a custom ErrorInfoProvider? I looked around a bit and found that I can inject one into the DocumentWriter, so am I correct in thinking that I would register a DocumentWriter in DI like this?
services.AddScoped<IDocumentWriter>(provider => new DocumentWriter(new CustomErrorInfoProvider()));
  1. How can I detect the Authentication error specifically in the ErrorInfoProvider? I want to have different error details depending on whether it is an Authentication or Authorization error:
public class CustomErrorInfoProvider : ErrorInfoProvider
{
    public override ErrorInfo GetInfo(ExecutionError executionError)
    {
        var info = base.GetInfo(executionError);

        // would be nice if I could modify the info.Code here...
        info.Message = executionError switch
        {
            // this is for the Authorization case with the planned AuthorizationError right?
            // what would I do to handle an AuthenticationError, i.e. an invalid JWT token?
            AuthorizationError authorizationError => "Custom message",
            _ => info.Message,
        };

        return info;
    }
}
  1. I can only specify the error message in the custom ErrorInfoProvider at the moment - a code would be better. Is there any way I can configure a custom code in my ErrorInfoProvider?

Samples for this would probably be nice, as you mentioned in the linked PR. Also is there any way I can help out and get this merged faster?

tobias-tengler avatar Jan 04 '21 12:01 tobias-tengler

which would generate a defined status code

Not http status code, just error code.

sungam3r avatar Jan 04 '21 12:01 sungam3r

How would I register a custom ErrorInfoProvider?

services.AddErrorInfoProvider

How can I detect the Authentication error specifically in the ErrorInfoProvider?

Look into AuthorizationError.AuthorizationResult

I can only specify the error message in the custom ErrorInfoProvider at the moment - a code would be better.

Code is stored into Extensions property.

sungam3r avatar Jan 04 '21 12:01 sungam3r

Also is there any way I can help out and get this merged faster?

Note that PR is targeted agains other project - server, not this repo.

sungam3r avatar Jan 04 '21 12:01 sungam3r

How would I register a custom ErrorInfoProvider?

services.AddErrorInfoProvider

I'm on the latest versions and I don't have an AddErrorInfoProvider extension method for IServiceColletion yet.

I only have an extension method for the IGraphQLBuilder with the following signature:

IGraphQLBuilder AddErrorInfoProvider(Action<ErrorInfoProviderOptions> configureOptions);

This is only used to configure an ErrorInfoProvider, not inject one yourself.

tobias-tengler avatar Jan 04 '21 12:01 tobias-tengler

https://github.com/graphql-dotnet/server/blob/master/src/Core/GraphQLBuilderExtensions.cs#L33

sungam3r avatar Jan 04 '21 13:01 sungam3r

I think graphql-dotnet/server#480 is not enough to solve this one... GraphQLHttpMiddleware will still respond with a 200 status code...

You can only change the message text which is reported inside the GraphQL error this way...

@sungam3r Maybe this method should return an int which is then used as HTTP status code....

rose-a avatar Jan 04 '21 13:01 rose-a

https://github.com/graphql-dotnet/server/blob/master/src/Core/GraphQLBuilderExtensions.cs#L33

Still, this only lets me configure options for an internally registered ErrorInfoProvider, not adding my own ErrorInfoProvider implementation.

I think graphql-dotnet/server#480 is not enough to solve this one... GraphQLHttpMiddleware will still respond with a 200 status code...

You can only change the message text which is reported inside the GraphQL error this way...

@sungam3r Maybe this method should return an int which is then used as HTTP status code....

Being able to change the HTTP Status code would probably be the easiest option for my case, since it's the easiest for clients to implement. But it's also something clients of an open API might not expect, so being able to communicate an Authentication error clearly in a regular GraphQL response (i.e. HTTP 200) does have its value. I still think graphql-dotnet/server#480 is the correct approach here, even though I would probably have to write my own middleware that parses each response, checks for an authentication error and initiates a token refresh.

tobias-tengler avatar Jan 04 '21 13:01 tobias-tengler

Being able to change the HTTP Status code would probably be the easiest option for my case, since it's the easiest for clients to implement.

It is not supported now. PRs are welcome.

sungam3r avatar Jan 04 '21 14:01 sungam3r