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.