Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Authorizing Subscriptions With JWT #49

Open
jonmill opened this issue May 15, 2019 · 10 comments
Open

Authorizing Subscriptions With JWT #49

jonmill opened this issue May 15, 2019 · 10 comments
Labels
question Further information is requested

Comments

@jonmill
Copy link

jonmill commented May 15, 2019

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

@jonmill
Copy link
Author

jonmill commented Jun 4, 2019

@joemcbride any ideas here? :)

@bioharz
Copy link

bioharz commented Jul 8, 2019

same issue

@jonmill
Copy link
Author

jonmill commented Jul 8, 2019

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

@OpenSpacesAndPlaces
Copy link

OpenSpacesAndPlaces commented Jul 16, 2019

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

@OpenSpacesAndPlaces
Copy link

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

@sungam3r
Copy link
Member

@jonmill Is this issue still actual for you?

@sungam3r sungam3r added the needs confirmation The problem is most likely resolved and requires verification by the author label May 19, 2020
@jonmill
Copy link
Author

jonmill commented May 19, 2020

@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

@sungam3r sungam3r removed the needs confirmation The problem is most likely resolved and requires verification by the author label May 20, 2020
@mlynam
Copy link

mlynam commented Oct 3, 2020

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

@bioharz
Copy link

bioharz commented Oct 3, 2020

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

@mlynam
Copy link

mlynam commented Oct 3, 2020

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

5 participants