graphql-dotnet / authorization

A toolset for authorizing access to graph types for GraphQL .NET.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Authorizing Subscriptions With JWT

jonmill opened this issue · comments

Hi,

I have some authorizations setup to handle Queries and Mutations like so (simplified)

services.AddSingleton(x =>
                {
                    AuthorizationSettings settings = new AuthorizationSettings();
                    settings.AddPolicy(AuthConstants.USERS_POLICY, p => p.RequireClaim(ClaimTypes.Role));
                    settings.AddPolicy(AuthConstants.ADMIN_POLICY, p => p.RequireClaim(ClaimTypes.Role, ((int)UserRoles.Administrator).ToString()));
                    settings.AddPolicy(AuthConstants.SUPERVISOR_POLICY, p => p.RequireClaim(ClaimTypes.Role, ((int)UserRoles.Administrator).ToString(),
                                                                                                             ((int)UserRoles.Supervisor).ToString()));
                    return settings;
                })

Now I'm attempting to add Subscriptions, but it looks like the Authorizations are not working. There didn't seem to be any built-in support for authorizing Subscriptions with JWTs, so I used this class for guidance. I can successfully retrieve the token from the connection, validate it, and add it to the HTTP context in an IOperationMessageListener::BeforeHandleAsync

        public Task BeforeHandleAsync(MessageHandlingContext context)
        {
            if (context.Message.Type == MessageType.GQL_CONNECTION_INIT)
            {
                JObject payload = context.Message.Payload as JObject;
                if (payload.TryGetValue("Authorization", System.StringComparison.OrdinalIgnoreCase, out JToken authValue))
                {
                    string token = authValue.Value<string>();
                    if (string.IsNullOrWhiteSpace(token) == false)
                    {
                        int start = token.IndexOf(BEARER, System.StringComparison.OrdinalIgnoreCase);
                        if (start >= 0)
                        {
                            token = token.Substring(start + BEARER_LENGTH);
                            _httpContextAccessor.HttpContext.User = JwtHelper.CreatePrincipal(token);
                        }
                    }
                }
            }

            ClaimsPrincipal user = _httpContextAccessor.HttpContext.User;
            context.Properties["user"] = user;
            return Task.CompletedTask;
        }

But the subscription endpoint still says that I'm unauthorized when I use AuthorizeWith. Is this a bug or how can I authorize Subscriptions using JWTs? Any guidance would be much appreciated

@joemcbride any ideas here? :)

same issue

I ended up switching away from the built in provider and rolled my own in order to work with Subscriptions and Queries / Mutations

It's not directly supported from what I've seen - from previous dicussions on the topic, auth is expected to more/less be your own custom coding.

Ideally you're running this under a WebAPI controller - so JWT should already be negotiated to the context.

For usage you'd do like:

settings.AddPolicy(AuthConstants.ADMIN_POLICY, _ => { _.AddRequirement(new RoleAuthorizationRequirement(UserRoles.Administrator)); });
....
ExecutionResult result = await _executer.ExecuteAsync(_ =>
{
...
_.UserContext = HttpContext.Current
...
}

....
public class RoleAuthorizationRequirement : IAuthorizationRequirement
...
public RoleAuthorizationRequirement(params string[] roles)
....
public Task Authorize(AuthorizationContext ac)
....
HttpContext ctx = (HttpContext)ac.UserContext;

if (ctx == null || ctx.User == null || ctx.User.Identity == null ||String.IsNullOrEmpty(ctx.User.Identity.Name))
 throw;
...
Do lookups with - ctx.User.Identity.Name

Also keep in mind, "UserContext" can be anything you want.

@jonmill Is this issue still actual for you?

@sungam3r it would be nice if there was a way that this was built-in, instead of having to roll-your-own auth...but no, I have moved on from this issue and done auth myself

I was able to setup Subscription auth using built in elements alone. If you are interested @jonmill I can give you my method.

@mlynam May I ask if you would share the snipped to anyone? Thank you very much @mlynam

Sure.

The main issue I ran into with authorization and graphql-dotnet is the inconsistent handling of the user context. In graphql-dotnet proper, we are told to implement a Dictionary<string,object> and here in authorization we are told to implement an IProvideClaimsPrincipal. After digging through the source I discovered that really it should be the same implementation for both types as the validator here in authorization casts the user context to an IProvideClaimsPrincipal before authorizing. It does work when the GraphQLMiddleware sets up the execution context because that middleware invokes the BuildUserContext method. The GraphQLWebSocketMiddleware does not invoke this method and instead sends a MessageHandlingContext to the executor. Due to this, whatever authenticate pipeline we setup in native AspNetCore is ignored completely by the authorizing validator here in authorization.

I know this preamble was long but bear with me because it explains this next part: I'm not using the authorization repo for the authorization. I dug through the graphql-dotnet repositories and found a namespace tucked away in the server repository: Authorization.AspNetCore. This repository has very similar code to what exists here in the authorization repo except that its authorization validator uses the IHttpContextAccessor to find the actual AspNetCore user context (I believe this makes way more sense than having an arbitrary Dictionary<string,object> provider for user context. This validator works for whichever GraphQL middleware is currently executing the result. The error handling is a little dry in the server repo but it really doesn't matter to me because it properly leverages the well written Microsoft Authorization types.

The final step here is how to actually handle the token during a websocket request. Obviously we cannot pass it through the headers as browsers do not allow header values beyond web socket protocol. In my case, I'm sending the access token as a query param and then using a message event handler in my JWT config to set the token during a websocket request. Here's startup psuedocode that should work for you.

public void ConfigureServices(IServiceCollection services)
{
      services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
          options.Authority = "some authority";
          options.Audience = "some audience";

          options.TokenValidationParameters = new TokenValidationParameters
          {
            NameClaimType = ClaimTypes.NameIdentifier
          };

          options.Events = new JwtBearerEvents
          {
            OnMessageReceived = context =>
            {
              if (context.Request.Query.TryGetValue("access_token", out StringValues queryToken) &&
                  context.Request.Headers.ContainsKey("Sec-WebSocket-Protocol"))
              {
                context.Token = queryToken.Single();
              }

              return Task.CompletedTask;
            }
          };
        });

      // Add schema types...

      services.AddGraphQL()
        .AddGraphQLAuthorization()
        .AddWebSockets();
}

public void Configure(IApplicationBuilder builder)
{
      builder.UseAuthentication();
      builder.UseWebsockets();
      builder.UseGraphQLWebsockets<Schema>();
      builder.UseGraphQL<Schema>();
}

I can probably put together a sample repo a little later on.