Code Monkey home page Code Monkey logo

Comments (16)

Shane32 avatar Shane32 commented on July 20, 2024 1

Works for my server. Look for an updated version of GraphQL.NET Server 7 shortly with the new property. It seems that my old notes shown here were actually mostly correct, but it didn't follow through setting the context.User property to the result.

//-- and you'd think this would work: --
//var auth = context.RequestServices.GetRequiredService<IAuthenticationService>();
//await auth.AuthenticateAsync(context, JwtBearerDefaults.AuthenticationScheme);

See the PR for the proposed implementation. My server code now looks like this, which is a substantial improvement:

                endpoints.MapGraphQL(
                    "/graphql",
                    opts => {
                        opts.HandleGet = false;
                        // GraphQL requests will ignore cookie authentication and use JWT authentication only
                        opts.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
                        opts.AuthorizationRequired = true;
                        opts.AuthorizedRoles.Add("myRole");
                    })
                    .RequireCors("GraphQL");

from server.

Shane32 avatar Shane32 commented on July 20, 2024

Which GraphQL nuget packages do you have installed and what versions?

from server.

Shane32 avatar Shane32 commented on July 20, 2024

Is the default authentication scheme the JWT bearer scheme?

from server.

StefanKoenigMUC avatar StefanKoenigMUC commented on July 20, 2024
    protected virtual void AddAuthentication(IServiceCollection services)
    {
        var tokenValidationParameters = new TokenValidationParameters()
        {
            ValidIssuer = Configuration["IdentityServer:TokenValidationParameters:ValidIssuer"],
            ValidAudiences = Configuration.GetSection("IdentityServer:TokenValidationParameters:ValidAudiences").Get<List<string>>(),
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["IdentityServer:TokenValidationParameters:Secret"])),
            NameClaimType = Configuration["IdentityServer:TokenValidationParameters:NameClaimType"],
            RoleClaimType = Configuration["IdentityServer:TokenValidationParameters:RoleClaimType"],
        };

        var jwtSecurityTokenHandler = new JwtSecurityTokenHandler
        {
            InboundClaimTypeMap = new Dictionary<string, string>()
        };

        IdentityModelEventSource.ShowPII = true;

        services.AddAuthentication().AddJwtBearer(options =>
        {
            if (JwtBackChannelHandler != null)
            {
                options.BackchannelHttpHandler = JwtBackChannelHandler;
            }

            options.Authority = Configuration["IdentityServer:BearerOptions:Authority"];
            options.Audience = Configuration["IdentityServer:BearerOptions:Audience"];
            options.IncludeErrorDetails = Configuration.GetValue<bool>("IdentityServer:BearerOptions:IncludeErrorDetails");
            options.SaveToken = Configuration.GetValue<bool>("IdentityServer:BearerOptions:SaveToken");
            options.RequireHttpsMetadata = Configuration.GetValue<bool>("IdentityServer:BearerOptions:RequireHttpsMetadata");
            options.SecurityTokenValidators.Clear();
            options.SecurityTokenValidators.Add(jwtSecurityTokenHandler);
            options.TokenValidationParameters = tokenValidationParameters;
            options.Events = new JwtBearerEvents
            {
                OnMessageReceived = context =>
                {
                    if (context.Request.Path.Value.StartsWith("/hubs/", StringComparison.InvariantCulture) && context.Request.Query.TryGetValue("access_token", out StringValues token))
                    {
                        context.Token = token;
                    }

                    return Task.CompletedTask;
                },
                OnAuthenticationFailed = context =>
                {
                    var te = context.Exception;

                    logger.Error(te, $"Error during jwt authentication for prinicipal {context.Principal}");

                    return Task.CompletedTask;
                }
            };
        });
    }

that's how jwt authentication is configured (according to https://learn.microsoft.com/en-us/dotnet/core/compatibility/aspnet-core/7.0/default-authentication-scheme I should not provide an default value anymore?)

installed packages:

		<PackageReference Include="Duende.IdentityServer" Version="6.3.2" />
		<PackageReference Include="Duende.IdentityServer.AspNetIdentity" Version="6.3.2" />
		<PackageReference Include="GraphQL.Client" Version="6.0.0" />
		<PackageReference Include="GraphQL.Client.Abstractions" Version="6.0.0" />
		<PackageReference Include="GraphQL.Client.LocalExecution" Version="6.0.0" />
		<PackageReference Include="GraphQL.Client.Serializer.Newtonsoft" Version="6.0.0" />
		<PackageReference Include="GraphQL.Client.Serializer.SystemTextJson" Version="6.0.0" />
		<PackageReference Include="GraphQL.NewtonsoftJson" Version="7.0.0" />
		<PackageReference Include="GraphQL.Server.All" Version="7.0.0" />
		<PackageReference Include="GraphQL.SystemTextJson" Version="7.0.0" />

yesterday I tried with 7.5 (but same result with 7.0.0 and 7.5.0).

Thanks for your reply!

from server.

StefanKoenigMUC avatar StefanKoenigMUC commented on July 20, 2024

played further around, but its not working at all. At the current state I'm always getting successfully authenticated requests - even when no jwt token is provided.

Is there maybe a working example with IdentityServer4 available? I'm confident there is an error on my side, but I'm not able to figure it out.

from server.

StefanKoenigMUC avatar StefanKoenigMUC commented on July 20, 2024

What also boggles my mind a bit - in

            options.Events = new JwtBearerEvents
            {
                OnMessageReceived = context =>
                {
                    if (context.Request.Path.Value.StartsWith("/hubs/", StringComparison.InvariantCulture) && context.Request.Query.TryGetValue("access_token", out StringValues token))
                    {
                        context.Token = token;
                    }

                    return Task.CompletedTask;
                },
                OnAuthenticationFailed = context =>
                {
                    var te = context.Exception;

                    logger.Error(te, $"Error during jwt authentication for prinicipal {context.Principal}");

                    return Task.CompletedTask;
                }
            };

you may see how I integrated SignalR into Identity Server. But already that function is not called when a GraphQl query is submitted. So it seems, the whole identity server part is skipped?

from server.

Shane32 avatar Shane32 commented on July 20, 2024

Since I see you're using the GraphQL Server authentication library, and specifically the newer implementation, I'm going to move this to the applicable repo.

from server.

Shane32 avatar Shane32 commented on July 20, 2024

I wrote the authorization implementation you've been trying to use. Basically, the GraphQL middleware uses the identity of the HttpContext.User property to obtain a ClaimsPrincipal instance to check authentication and role membership against. This is set by the call to ASP.NET Core's app.UseAuthentication() which must appear prior to the app.UseGraphQL() call.

The implementation is very simple (and can easily be overridden with a custom implementation) - see:

With JWT bearer authentication, there's two specific gotchyas that you have to look out for:

First, there is no setting to control the authentication scheme used. I believe that in theory you can have two different authentication schemes, say JWT bearer and cookie, and only one will be default. The default will be used to set HttpContext.User under normal circumstances. I'm not sure how to tell ASP.NET Core to use a different authentication scheme when using the middleware, which is why I don't have a property to set the auth scheme. It may be that if you use endpoint mapping, you can set the auth scheme of the endpoint within the routing config, and ASP.NET Core will properly set the HttpContext.User property before the middleware executes. This is similar to how you can program specific CORS rules to take effect.

Second, the JWT bearer claim names differ from those used within C# (assuming of course that you are passing the roles within the token itself). Here's ChatGPT's explanation:

In a JWT (JSON Web Token), the claim name commonly used to represent roles is "roles". The "roles" claim is used to specify the roles or permissions associated with a user.

However, in ASP.NET Core, the claim name for roles is "http://schemas.microsoft.com/ws/2008/06/identity/claims/role". This is the default claim name used by ASP.NET Core's authentication and authorization mechanisms to represent user roles.

It's worth noting that while "roles" is a widely used claim name in JWT, ASP.NET Core follows a more specific claim name to ensure compatibility with other systems and standards.

Normally this remapping is handled automatically by the JWT handler. But I did notice that it appears you've removed the default mappings, and so it's possible that this is an issue.

        var jwtSecurityTokenHandler = new JwtSecurityTokenHandler
        {
            InboundClaimTypeMap = new Dictionary<string, string>()
        };

Another issue I've seen is that the [Authorize] attribute exists within the ASP.NET Core and GraphQL namespaces. When tagging your type-first schema definitions, you need to use the GraphQL.NET AuthorizeAttribute or else it won't work as expected.

I do use JWT bearer extensively for my own servers, but I don't use IdentityServer. Hopefully I can still be of help. I suggest reviewing the JWT sample we have here:

And here is the sample for endpoint middleware:

This is what I would suggest as debugging steps:

  1. Remove [Authorize] attribute from the Query type
  2. Remove role check from the UseGraphQL call
  3. Try to connect and perform a simple query such as { __typename } with only the RequireAuthentication property set
  4. If it works, add test resolver and inspect the IResolveFieldContext.User property, checking if the roles were set properly, etc, otherwise:
  5. Ensure that UseAuthorization occurs before UseGraphQL
  6. Try setting the default authorization scheme; if that works, revert and see if you can configure the authorization scheme when using endpoint routing; if so, set up GraphQL through endpoint routing with a configured authorization scheme
  7. If all else fails, perhaps you can do something like this (which is what I do in one of my apps):
            app.MapWhen(
                context => context.Request.Path.Equals(path) &&
                    (context.WebSockets.IsWebSocketRequest || HttpMethods.IsPost(context.Request.Method) || HttpMethods.IsOptions(context.Request.Method)),
                b => {
                    // use JWT authentication, and ignore other authentication
                    b.Use(next => (RequestDelegate)(async context => {
                        //-- you'd think this would work: --
                        //var user = context.User.Identities.FirstOrDefault(x => x.AuthenticationType == JwtBearerDefaults.AuthenticationScheme);
                        //context.User = new ClaimsPrincipal(user ?? new ClaimsIdentity());

                        //-- and you'd think this would work: --
                        //var auth = context.RequestServices.GetRequiredService<IAuthenticationService>();
                        //await auth.AuthenticateAsync(context, JwtBearerDefaults.AuthenticationScheme);

                        //-- but they don't, so we do this --
                        context.User = new ClaimsPrincipal(new ClaimsIdentity());
                        var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
                        var authToken = authHeader?.StartsWith("Bearer ") ?? false ? authHeader[7..] : null;
                        if (authToken != null) {
                            var validationParameters = context.RequestServices.GetRequiredService<Microsoft.Extensions.Options.IOptionsMonitor<JwtBearerOptions>>()
                                .Get(JwtBearerDefaults.AuthenticationScheme)
                                .TokenValidationParameters;
                            var handler = new JwtSecurityTokenHandler();
                            try {
                                context.User = handler.ValidateToken(authToken, validationParameters, out _);
                            } catch { }
                        }

                        await next(context).ConfigureAwait(false);
                    }));
                    // apply GraphQL CORS policy
                    b.UseCors("GraphQL");
                    // call graphql
                    b.UseGraphQL(path, opts => {
                        opts.AuthorizationRequired = true;
                        opts.AuthorizedRoles.Add("myRole");
                    });
                });

from server.

Shane32 avatar Shane32 commented on July 20, 2024

If you have any ideas how we can implement an AuthenticationScheme property within the GraphQL middleware, so that ASP.NET Core automatically sets the HttpContext.User property properly for the JWT Bearer authentication scheme, please let me know. Obviously the above patch is quite specific to my own use case.

from server.

Shane32 avatar Shane32 commented on July 20, 2024

Perhaps this method would work (but I have not tried it):

https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhttpcontextextensions.authenticateasync?view=aspnetcore-7.0#microsoft-aspnetcore-authentication-authenticationhttpcontextextensions-authenticateasync(microsoft-aspnetcore-http-httpcontext-system-string)

from server.

StefanKoenigMUC avatar StefanKoenigMUC commented on July 20, 2024

image

just had a quick test right now, looks good! :-) will have a further look for implementation tomorrow. Big big thanks so far!

from server.

StefanKoenigMUC avatar StefanKoenigMUC commented on July 20, 2024

Short update, have now my own middleware in place:

    protected override ValueTask<bool> HandleAuthorizeAsync(HttpContext context, RequestDelegate next)
    {
        var resultTask = context.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme);

        resultTask.Wait();

        var result = resultTask.Result;

        if(result.Succeeded)
        {
            context.User = result.Principal;
        }

        return base.HandleAuthorizeAsync(context, next);
    }

it seems to be working now, not the best of all solutions. What confuses me at the moment

        var jwtSecurityTokenHandler = new JwtSecurityTokenHandler
        {
            InboundClaimTypeMap = new Dictionary<string, string>()
        };

is required for me, otherwise the claim cannot be found due to:

if (user.IsInRole(role))

is always looking for the "role" claim, the default InBoundClaimTypeMap is mapping the jwt "role" property to ClaimTypes.Role, which does not match for the "longer" role. But that's probably dug somewhere else within the application (need to check that).

So far its now working (maybe not in the best way - but takes the pressure out for now; will have a look at your described steps later today)

from server.

Shane32 avatar Shane32 commented on July 20, 2024

Excellent! Two comments: HandleAuthorizeAsync is any async method and so you can call AuthenticateAsync with await instead of .Wait and Result for true async flow.

Secondly, if you utilize subscriptions, then you might need to override HandleAuthorizeWebSocketConnectionAsync with basically the same code. I would as a matter of principle even I was not yet using subscriptions.

Perhaps I can add this functionality into GraphQL.NET Server 7.6

from server.

StefanKoenigMUC avatar StefanKoenigMUC commented on July 20, 2024

thanks - changed it accordingly. Subscriptions are not yet in use (but will add it there as well)

thanks a lot for your help. As said, will try your steps described above as well - maybe I'll get it to work as well.

from server.

Shane32 avatar Shane32 commented on July 20, 2024

Here is my prototype change (going to test it in my own 'fork' first before making the same PR here):

Shane32/GraphQL.AspNetCore3#59

from server.

StefanKoenigMUC avatar StefanKoenigMUC commented on July 20, 2024

Thanks so much for your work and help! Would have taken me ages to figure it out by myself.

from server.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.