Search code examples
c#.netgraphqlauthorizationhotchocolate

NET 6 WebAPI with Hotchocolate GraphQL not authorizing


I have a NET 6 api that I am using HotChocolate 12.7 in. My queries work, but when I am trying to add the [Authorize] decorator to a query, and send the request with the Bearer token, I am getting the unauthorized response. I can not get it to recognize a properly authenticated user's JWT.

Here is the Program.cs

using altitude_api.Entities;
using altitude_api.Models;
using altitude_api.Queries;
using altitude_api.Services;
using altitude_api.Services.Interfaces;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Serilog;


namespace altitude_api
{
    public class Program
    {
        public static void Main(string[] args)
        {

            var configuration = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json")
                .Build();
            var key = configuration.GetSection("AppSettings").GetSection("SecretKey");
            var builder = WebApplication.CreateBuilder(args);
            builder.Services.AddAuthorization();
            builder.Services.AddAuthentication(options =>
                {
                    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                })
                .AddJwtBearer(opt =>
                {
                    opt.SaveToken = true;
                    opt.RequireHttpsMetadata = false;
                    opt.TokenValidationParameters = new TokenValidationParameters()
                    {
                        ValidateIssuer = true,
                        ValidateAudience = true,
                        ValidateLifetime = true,
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes((key.Value))),
                    };
                });

            builder.Services
                .AddGraphQLServer()
                .AddAuthorization()
                .AddQueryType<Query>()
                .AddTypeExtension<HealthQuery>();
            var services = builder.Services;
            
            
            // Add Services
            var appSettingsSection = builder.Configuration.GetSection("AppSettings").Get<AppSettings>();
            services.Configure<AppSettings>(configuration.GetSection("AppSettings"));
            services.AddScoped<IAuthService, AuthService>();
            
            var signinKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(key.Value));
            
            Log.Logger = new LoggerConfiguration()
                .ReadFrom.Configuration(configuration)
                .CreateLogger();
            
            
            Log.Information("App starting up");
            
            services.AddAuthorization(options =>
            {
                options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme).RequireAuthenticatedUser().Build();
            });
            var allowedOrigins = appSettingsSection.AllowedOriginList;
            
            services.AddDbContext<AltitudeContext>(
                options => options.UseSqlServer(configuration.GetConnectionString("AltitudeContext")));
            
            services.AddCors(options => options.AddPolicy("EnableCORS", build =>
            {
                build
                    .AllowAnyMethod()
                    .AllowAnyHeader()
                    .WithOrigins(allowedOrigins.ToString())
                    .AllowCredentials();
            }));
            
            var app = builder.Build();
            // app.MapControllers();
            app.UseRouting();
            app.UseCors("EnableCORS");
            app.UseAuthentication();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGraphQL();
            });
            app.Run();
        }
    }
}

And here is the authentication code.

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using altitude_api.Entities;
using altitude_api.Models;
using altitude_api.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Novell.Directory.Ldap;
using Serilog;


namespace altitude_api.Services;

public class AuthService: IAuthService
{

    private readonly AltitudeContext _altitudeContext;
    private readonly AppSettings appSettings;
    public AuthService(AltitudeContext altitudeContext, IOptions<AppSettings> _appSettings)
    {
        _altitudeContext = altitudeContext;
        appSettings = _appSettings.Value;
    }

    public UserWithToken AuthenticateUser(string userName, string password)
    {
        var isValid = false;

        var port = appSettings.LdapPort;

        Log.Information($"Beginning to authenticate user {userName}");

        using (var cn = new LdapConnection())
        {
            // connect

            try
            {
                cn.Connect(appSettings.LdapServer, Convert.ToInt32(port));
                cn.Bind(appSettings.ldapDomain + userName, password);
                if (cn.Bound)
                {
                    isValid = true;
                    Log.Information($"Successfully found user in LDAP {userName}");
                }
            }
            catch (Exception ex)
            {
                Log.Error( ex,$"Error looking up {userName} in LDAP.");
                throw ex;
            }
        }

        return isValid ? GetToken(userName) : throw new Exception("Unable to authenticate user at this time");
    }


    public Users GetUser()
    {
        return _altitudeContext.Users.First(p => p.Username == "maxwell.sands");
    }

    public UserWithToken GetToken(string userName)
    {

        try
        {
            var roles = _altitudeContext.Roles;
            
            var dbUser = _altitudeContext.Users.FirstOrDefault(p => p != null && p.Username == userName);
            
            if (dbUser == null)
            {
                var ex = new Exception("User not found");
                Log.Error(ex, "User is not found could not authenticate");
                throw ex;
            }
            if(dbUser.ExpiryDttm < DateTime.Now)
            {
                var ex = new Exception("ERROR: User access expired.");
                Log.Error(ex, "User is expired could not authenticate");
                throw ex;
            }
             var role = (from rle in _altitudeContext.Roles
                 join m in _altitudeContext.UserRoles on rle.RoleId equals m.RoleId
                 join usr in _altitudeContext.Users on m.UserId equals usr.UserId
                 where usr.Username.ToLower() == dbUser.Username.ToLower()
                 select rle.RoleName).FirstOrDefault();
             
             if (role == null)
             {
                 var ex = new Exception("Role not found");
                 Log.Error(ex, "User is expired could not authenticate");
                 throw ex;
             }
             

             var secret = appSettings.SecretKey;
             
             // authentication successful so generate jwt token
             var tokenHandler = new JwtSecurityTokenHandler();
             var key = Encoding.ASCII.GetBytes(secret);
             var tokenDescriptor = new SecurityTokenDescriptor
             {
                 Subject = new ClaimsIdentity(new Claim[]
                 {
                     new Claim(ClaimTypes.Name, userName),
                     new Claim(ClaimTypes.Role, role),
                 }),
                 Expires = DateTime.UtcNow.AddDays(1),
                 SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256)
             };
             var token = tokenHandler.CreateToken(tokenDescriptor);
     
             UserWithToken webuser = new UserWithToken();
     
             webuser.UserName = dbUser.Username;
             webuser.FirstName = dbUser.FirstName;
             webuser.LastName = dbUser.LastName;
             webuser.Id = dbUser.UserId;
             webuser.Role = role;
             webuser.Token = tokenHandler.WriteToken(token);
             Log.Information($"{webuser.FirstName} {webuser.LastName} was successfully logged in.");
             return webuser;   
                    
        }
        catch(Exception e)
        {
            Log.Information(e, "There was an error loading the user");
            throw new Exception("There was an issue loading the user", e);
        }
    }
}

And finally here is the endpoint I am require authorization on.

using HotChocolate.AspNetCore.Authorization;

namespace altitude_api.Queries;

[ExtendObjectType(typeof(Query))]
public class HealthQuery
{
    [Authorize]
    public bool HeartBeat()
    {
        return true;
    }
}

Solution

  • In response to @Arjav Dave, yes I did find the answer to this, almost by dumb luck. The order of things in the Program.cs is very important. My code looks like the following...

        builder.Services
     .AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
        {
            // omitted
        }).Services
        .AddAuthorization();
    
    builder.Services
        .AddGraphQLServer()
        .AddAuthorization()
        .AddFiltering()
        .AddSorting()
        .AddQueryType<Query>()
    

    and when I am setting up the app it looks like...

    var app = builder.Build();
    app.UseRouting();
    app.UseCors("EnableCORS");
    app.UseAuthentication();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGraphQL();
    });
    app.Run();
    

    Hope that helps.