0

I am trying to create asp.net core application with graphql (not using hot chocolate) and jwt tokens authentication. I have created some graphql queries and mutations and I want to be able to run them only when I am already authorized, except the login mutation, which I want to run at any time. I am using altair to send request to my backend server and for each request I set "Authorization: Bearer {{token}}" header. If I set active jwt token, everything works properly, but if this token is expired, I can't run the login mutation, because it returns an error: "You are not authorized to run this query.\r\nThe current user must be authenticated."

I haven't found any solution of my problem in the last 3 days, so I decided to ask it here.

I have set up jwt tokens authentication in my Program.cs file. Also I have added AddGraphQLAuthorization() and policy "Authenticated", which requires authenticated users.

Program.cs:

using GraphQL.Server;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using TimeTrackerApp.Business.Repositories;
using TimeTrackerApp.MsSql.Repositories;
using TimeTrackerApp.GraphQL.GraphQLSchema;
using TimeTrackerApp.Helpers;
using Microsoft.AspNetCore.Http;
using GraphQL;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using System;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using GraphQL.Types;
using GraphQL.MicrosoftDI;
using GraphQL.SystemTextJson;

var builder = WebApplication.CreateBuilder(args);

string connectionString = builder.Configuration.GetConnectionString(Constants.DatabaseConnectionString);

builder.Services.AddSingleton<IAuthenticationTokenRepository>(provider => new AuthenticationTokenRepository(connectionString));
builder.Services.AddSingleton<IUserRepository>(provider => new UserRepository(connectionString));


builder.Services.AddControllersWithViews();

builder.Services.AddSpaStaticFiles(configuration =>
{
    configuration.RootPath = "ClientApp/build";
});

builder.Services.AddAuthentication(options => {
    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
    options.RequireHttpsMetadata = false;
    options.SaveToken = true;
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidateAudience = false,
        ValidateIssuer = false,
        ValidIssuer = Environment.GetEnvironmentVariable(Constants.JwtTokenIssuer),
        ValidAudience = Environment.GetEnvironmentVariable(Constants.JwtTokenAudience),
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable(Constants.JwtSecretKey))),
        ClockSkew = TimeSpan.Zero
    };
});

builder.Services.AddCors(
    builder => {
        builder.AddDefaultPolicy(option =>
        {
            option.AllowAnyOrigin();
            option.AllowAnyMethod();
            option.AllowAnyHeader();
        });
    }
);

builder.Services.AddScoped<AppSchema>();
builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
builder.Services.AddSingleton<IDocumentExecuter, DocumentExecuter>();

builder.Services.AddGraphQL(options =>
    {
        options.EnableMetrics = true;
    })
    .AddSystemTextJson()
    .AddGraphTypes(typeof(AppSchema), ServiceLifetime.Scoped)
    .AddGraphQLAuthorization(options =>
    {
        options.AddPolicy("Authenticated", policy => policy.RequireAuthenticatedUser());
    });
Newtonsoft.Json.ReferenceLoopHandling.Ignore);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();



var app = builder.Build();

if (app.Environment.IsDevelopment())
{ }
app.UseDeveloperExceptionPage();
app.UseHsts();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSpaStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();
app.UseCors(x => x
         .AllowAnyOrigin()
         .AllowAnyMethod()
         .AllowAnyHeader());

app.MapControllers();


app.UseGraphQL<AppSchema>();

app.UseGraphQLAltair();

app.UseSpa(spa =>
{
    spa.Options.SourcePath = "ClientApp";
    if (app.Environment.IsDevelopment())
    {
        spa.UseReactDevelopmentServer(npmScript: "start");
    }
});

app.Run();

In graphql queries and mutations I use method AuthorizeWith("Authenticated"), but I don't use it in login mutation.

Login and logout mutations:

Field<AuthResponseType, AuthResponse>()
                .Name("auth_login")
                .Argument<NonNullGraphType<StringGraphType>, string>("Email", "User email")
                .Argument<NonNullGraphType<StringGraphType>, string>("Password", "User password")
                .ResolveAsync(async context =>
                {
                    string email = context.GetArgument<string>("Email");
                    string password = context.GetArgument<string>("Password");
                    var authenticationServiceResponse = await authenticationService.Login(email, password);
                    var authenticationServiceApiResponse = new AuthResponse()
                    {
                        AccessToken = authenticationServiceResponse.AccessToken,
                        RefreshToken = authenticationServiceResponse.RefreshToken,
                        Message = authenticationServiceResponse.Message,
                    };
                    return authenticationServiceApiResponse;
                });

            Field<AuthResponseType, AuthResponse>()
                .Name("auth_logout")
                .Argument<NonNullGraphType<IdGraphType>, int>("UserId", "User id")
                .ResolveAsync(async context =>
                {
                    var userId = context.GetArgument<int>("UserId");
                    var authenticationServiceResponse = await authenticationService.Logout(userId);
                    var authenticationServiceApiResponse = new AuthResponse()
                    {
                        AccessToken = authenticationServiceResponse.AccessToken,
                        RefreshToken = authenticationServiceResponse.RefreshToken,
                        Message = authenticationServiceResponse.Message,
                    };
                    return authenticationServiceApiResponse;
                }).AuthorizeWith("Authenticated");

Also I have created some additional classes for convenience.

JwtTokensService.cs:

using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography;
using System.Security.Claims;
using System.Text;
using TimeTrackerApp.Business.Models;

namespace TimeTrackerApp.Business.Services
{
    public static class JwtTokenService
    {
        private static readonly string jwtSecretKey = Environment.GetEnvironmentVariable("JWT_SECRET_KEY")!;
        
        public static string GenerateJwtToken(IEnumerable<Claim> claims, int tokenDurationInSeconds)
        {
            var symmetricSecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecretKey));
            var credentials = new SigningCredentials(symmetricSecurityKey, SecurityAlgorithms.HmacSha256);
            var jwtSecurityToken = new JwtSecurityToken(
                issuer: Environment.GetEnvironmentVariable("JWT_TOKEN_ISSUER"),
                audience: Environment.GetEnvironmentVariable("JWT_TOKEN_AUDIENCE"),
                claims: claims,
                expires: DateTime.Now.AddSeconds(tokenDurationInSeconds),
                signingCredentials: credentials
            );
            return new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
        }

        public static IEnumerable<Claim> GetJwtTokenClaims(User user)
        {
            return new List<Claim>
            {
                new Claim("UserId", user.Id.ToString()),
                new Claim("UserEmail", user.Email),
                new Claim("UserPrivilegesValue", user.PrivilegesValue.ToString())
            };
        }

        public static string GenerateAccessToken(User user) => GenerateJwtToken(GetJwtTokenClaims(user), 120);

        public static string GenerateRefreshToken(User user) => GenerateJwtToken(GetJwtTokenClaims(user), 2592000);
    }
}

AuthenticationService.cs:

using TimeTrackerApp.Business.Models;
using TimeTrackerApp.Business.Repositories;

namespace TimeTrackerApp.Business.Services
{
    public class AuthenticationService
    {
        private IUserRepository userRepository { get; set; }
        private IAuthenticationTokenRepository authenticationTokenRepository { get; set; }

        public class AuthenticationResponse
        {
            public string? AccessToken { get; set; }
            public string? RefreshToken { get; set; }
            public string? Message { get; set; }

            public AuthenticationResponse(string accessToken, string refreshToken)
            {
                AccessToken = accessToken;
                RefreshToken = refreshToken;
            }

            public AuthenticationResponse(string accessToken, string refreshToken, string message)
            {
                AccessToken = accessToken;
                RefreshToken = refreshToken;
                Message = message;  
            }

            public AuthenticationResponse(string message)
            {
                Message = message;
            }
        }

        public AuthenticationService(IUserRepository userRepository, IAuthenticationTokenRepository authenticationTokenRepository)
        {
            this.userRepository = userRepository;
            this.authenticationTokenRepository = authenticationTokenRepository;
        }

        public async Task<AuthenticationResponse> Login(string email, string password)
        {
            try
            {
                var user = await userRepository.GetByEmailAsync(email);
                if (!PasswordService.CompareWithHash(user.Password, password))
                {
                    return new AuthenticationResponse("Wrong password!");
                }
                var accessToken = JwtTokenService.GenerateAccessToken(user);
                var refreshToken = JwtTokenService.GenerateRefreshToken(user);
                var refreshTokenDb = new AuthenticationToken()
                {
                    UserId = user.Id,
                    Token = refreshToken
                };
                await authenticationTokenRepository.CreateAsync(refreshTokenDb);
                return new AuthenticationResponse(accessToken, refreshToken, "Jwt tokens have been successfully received!");
            }
            catch (Exception exception)
            {
                return new AuthenticationResponse(exception.Message);
            }
        }

        public async Task<AuthenticationResponse> Logout(int userId)
        {
            try
            {
                await authenticationTokenRepository.RemoveByUserIdAsync(userId);
                return new AuthenticationResponse("User has successfully been logged out!");
            }
            catch (Exception exception)
            {
                return new AuthenticationResponse(exception.Message);
            }
        }

        public async Task<AuthenticationResponse> Refresh(int userId, string refreshToken)
        {
            try
            {
                var user = await userRepository.GetByIdAsync(userId);
                var userRefreshTokenDb = await authenticationTokenRepository.GetByUserIdAsync(userId);
                if (userRefreshTokenDb.Token == refreshToken)
                {
                    var newAccessToken = JwtTokenService.GenerateAccessToken(user);
                    var newRefreshToken = JwtTokenService.GenerateRefreshToken(user);
                    userRefreshTokenDb = new AuthenticationToken()
                    {
                        UserId = user.Id,
                        Token = newRefreshToken,
                    };
                    await authenticationTokenRepository.RemoveByUserIdAsync(userId);
                    await authenticationTokenRepository.CreateAsync(userRefreshTokenDb);
                    return new AuthenticationResponse(newAccessToken, newRefreshToken, "Jwt tokens have been successfully refreshed!");
                }
                return new AuthenticationResponse("Refresh tokens are different!");
            }
            catch (Exception exception)
            {
                return new AuthenticationResponse(exception.Message);
            }
        }
    }
}

I just want to be able to run login mutation without authentication and any other queries or mutations only when I am authenticated. However, I don't know what is wrong in my code. I would be grateful for your help.

  • Maybe [this case](https://stackoverflow.com/questions/50628455/graphql-authentication-with-asp-net-core-using-jwt) can help you understand it better. – Chen Aug 01 '22 at 09:19

0 Answers0