Hotchocolate GraphQL is perfectly integrated with ASP.NET Core Authentication/Authorization pipeline
In this intro I'm using JWTs in a very semplyfied way, but Hotchocolate is built around the ASP.NET Core authentication procesess and so it supports any other authentication scheme.
To setup the authentication we need to install these packages:
dotnet add ./graphqlServer package Microsoft.AspNetCore.Authentication.JwtBearer --version 6.0.1
dotnet add ./graphqlServer package HotChocolate.AspNetCore.Authorization --version 12.1.0
and add some configuration settings for JWT authentication:
"Jwt": {
"Key": "ThisIsMySuperSecretKey",
"Issuer": "https://williamverdolini.github.io/",
"Audience": "http://localhost:4200/"
},
After that we have to setup the application pipelines:
builder.Services
.AddJwtAuthentication(builder.Configuration);
...
var app = builder.Build();
...
app.UseAuthentication();
app.UseAuthorization();
public static AuthenticationBuilder AddJwtAuthentication(this IServiceCollection services, IConfiguration config)
{
services.AddControllers();
return services
.AddTransient<IUserRepository, UserRepository>()
.AddTransient<ITokenService, TokenService>()
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters =
new TokenValidationParameters
{
ValidIssuer = config["Jwt:Issuer"],
ValidAudience = config["Jwt:Issuer"],
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["Jwt:Key"]))
};
});
}
As said, here I'm using a custom TokenService to generate token and validate it, but in a real scenario you should use some external OAuth 2.0 provider like Azure AD.
Last step is to enable Hotchocolate to use Authentication/Authorization pipeline:
services
.AddGraphQLServer()
.AddAuthorization()
With that, we can acces authenticated user information in our resolvers.
Authorization is a more interesting part: because it's here that we can choose and control in a very targeted way which permission the user has to read the GraphQL schema.
We can control the user's permission at resolver level and that allow us to choose if the user can get some particular data.
In particular we can exploit both the possibility of using claims or policies.
Here we want to allow only the users who have "admin" claim to see book's authors. To do that the only thing to do is the following:
descriptor
.Field(f => f.Authors)
.ResolveWith<BookResolvers>(t => t.GetAuthorsAsync(default!, default!, default!, default))
.Authorize(roles: new [] {"admin"});
Now let's try to launch this query for book's authors:
query {
books(first: 1, where: { title: { startsWith: "Reconstituirea" } }) {
nodes {
id
title
authors {
firstName
surnName
}
}
}
}
without having the required claim, we'get this error:
{
"errors": [
{
"message": "The current user is not authorized to access this resource.",
"locations": [
{
"line": 6,
"column": 7
}
],
"path": [
"books",
"nodes",
0,
"authors"
],
"extensions": {
"code": "AUTH_NOT_AUTHENTICATED"
}
}
],
"data": {
"books": null
}
}
what you should note here is that we CANNOT get "readable" fields like book's id
or title
either, while with the right authenticated user we get expected data.
{
"data": {
"books": {
"nodes": [
{
"id": "Qm9vawpkNzVmMzE2NjgtZjM0OC00YWEwLTg4YzktZDE4NmUwYTZmZTRl",
"title": "Reconstituirea (Reconstruction)",
"authors": [
{
"firstName": "Emmeline",
"surnName": "Giannassi"
},
{
"firstName": "Bridgette",
"surnName": "Chace"
}
]
}
]
}
}
}
With policies we can do more.
Here I want to allow only the users who have "publishers.read"
policy to read the book's publisher info, without loosing the capability to allow to read other book data if the user does NOT have the publisher policy.
To do that we have to create the policy as an AuthorizationHandler
:
public class CanReadPublishersRequirement : IAuthorizationRequirement { }
public class CanReadPublishersAuthorizationHandler
: AuthorizationHandler<CanReadPublishersRequirement, IResolverContext>
{
private readonly IUserRepository users;
public CanReadPublishersAuthorizationHandler(IUserRepository users)
{
this.users = users;
}
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
CanReadPublishersRequirement requirement,
IResolverContext resource)
{
if (context.User?.Identity?.Name != null)
{
var user = users.GetUserByName(context.User.Identity.Name);
if (user?.Policies.Contains("publishers.read") == true)
{
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}
and register it:
public static IServiceCollection AddAuthorizationPolicies(this IServiceCollection services)
{
return services
.AddAuthorization(options =>
{
options.AddPolicy("publishers.read", policy =>
policy.Requirements.Add(new CanReadPublishersRequirement()));
})
.AddSingleton<IAuthorizationHandler, CanReadPublishersAuthorizationHandler>();
}
Finally, as before, we can configure our resolver:
descriptor
.Field(f => f.Publisher)
.ResolveWith<BookResolvers>(t => t.GetPublisherAsync(default!, default!, default!, default))
.Authorize(policy: "publishers.read");
Now let's try to launch this query for book's publisher:
query {
books(first: 1, where: { title: { startsWith: "Reconstituirea" } }) {
nodes {
id
title
publisher {
name
address
}
}
}
}
This time, if the user has not the right policy will get
{
"errors": [
{
"message": "The current user is not authorized to access this resource.",
"locations": [
{
"line": 6,
"column": 7
}
],
"path": [
"books",
"nodes",
0,
"publisher"
],
"extensions": {
"code": "AUTH_NOT_AUTHENTICATED"
}
}
],
"data": {
"books": {
"nodes": [
{
"id": "Qm9vawpkNzVmMzE2NjgtZjM0OC00YWEwLTg4YzktZDE4NmUwYTZmZTRl",
"title": "Reconstituirea (Reconstruction)",
"publisher": null
}
]
}
}
}
As you can see, here we have error information about not allowed requested data and, in the result json portion,the publisher data is null, but the other visible data are returned.
With these permission settings the result schema has changed introducing the @authorize
directive:
type Book implements Node {
id: ID!
authors: [Author!]! @cost(complexity: 5)
publisher: Publisher @cost(complexity: 5)
authors: [Author!]! @authorize(roles: [ "admin" ], apply: BEFORE_RESOLVER) @cost(complexity: 5)
publisher: Publisher @authorize(policy: "publishers.read", apply: BEFORE_RESOLVER) @cost(complexity: 5)
relatedBooks: [Book!]! @cost(complexity: 5)
title: String
abstract: String
editionVersion: Int
publicationDate: DateTime
categories: [String!]