Authorization allows us to determine a user's permissions within our system. We can for example limit access to resources or only allow certain users to execute specific mutations.
Authentication is a prerequisite of Authorization, as we first need to validate a user's "authenticity" before we can evaluate his authorization claims.
Learn how to setup authentication
Setup
After we have successfully setup authentication, there are only a few things left to do.
- Register the necessary ASP.NET Core services
public class Startup{ public void ConfigureServices(IServiceCollection services) { services.AddAuthorization();
// Omitted code for brevity
services .AddGraphQLServer() .AddAuthorization() .AddQueryType<Query>(); }}
⚠️ Note: We need to call
AddAuthorization()on theIServiceCollection, to register the services needed by ASP.NET Core, and on theIRequestExecutorBuilderto register the@authorizedirective and middleware.
- Register the
UseAuthorizationmiddleware with the request pipeline
public class Startup{ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseRouting();
app.UseAuthentication(); app.UseAuthorization();
app.UseEndpoints(endpoints => { endpoints.MapGraphQL(); }); }}
Usage
At the core of authorization with Hot Chocolate is the @authorize directive. It can be applied to fields and types to denote that they require authorization.
If the @authorize directive is specified on a type, it is applied to all fields of that type. Specified on an individual field the directive precedences the one on the type.
In the Annotation-based approach we can use the [Authorize] attribute to add the @authorize directive.
[Authorize]public class User{ public string Name { get; set; }
[Authorize] public Address Address { get; set; }}
⚠️ Note: We need to use the
HotChocolate.AspNetCore.AuthorizationAttributeinstead of theMicrosoft.AspNetCore.AuthorizationAttribute.
If we do not specify any arguments to the @authorize directive, it will only enforce that the requestor is authenticated, nothing more.
A GraphQL error will be raised and the field result set to null, if the requestor is unauthenticated and tries to access an authorized field.
Roles
Roles provide a very intuitive way of dividing our users into groups with different access rights.
When building our ClaimsPrincipal, we just have to add one or more role claims.
claims.Add(new Claim(ClaimTypes.Role, "Administrator"));
We can then check, whether an authenticated user has these role claims.
[Authorize(Roles = new [] { "Guest", "Administrator" })]public class User{ public string Name { get; set; }
[Authorize(Roles = new[] { "Administrator" })] public Address Address { get; set; }}
⚠️ Note: If multiple roles are specified, a user only has to match one of the specified roles, in order to be able to execute the resolver.
Learn more about role-based authorization in ASP.NET Core
Policies
Policies allow us to create richer validation logic and decouple the authorization rules from our GraphQL resolvers.
A policy consists of an IAuthorizationRequirement and an AuthorizationHandler<T>.
Once defined, we can register our policies like the following.
public class Startup{ public void ConfigureServices(IServiceCollection services) { services.AddAuthorization(options => { options.AddPolicy("AtLeast21", policy => policy.Requirements.Add(new MinimumAgeRequirement(21)));
options.AddPolicy("HasCountry", policy => policy.RequireAssertion(context => context.User.HasClaim(c => c.Type == ClaimTypes.Country))); });
services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
// Omitted code for brevity
services .AddGraphQLServer() .AddAuthorization() .AddQueryType<Query>(); }}
When can then use these policies to restrict access to our fields.
[Authorize(Policy = "AllEmployees")]public class User{ public string Name { get; }
[Authorize(Policy = "SalesDepartment")] public Address Address { get; }}
This essentially uses the provided policy and runs it against the ClaimsPrinciple that is associated with the current request.
The @authorize directive is also repeatable, which means that we are able to chain the directive and a user is only allowed to access the field, if he meets all of the specified conditions.
[Authorize(Policy = "AtLeast21")][Authorize(Policy = "HasCountry")]public class User{ public string Name { get; set; }}
IResolverContext within an AuthorizationHandler
If we need to, we can also access the IResolverContext in our AuthorizationHandler.
public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement, IResolverContext>{ protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, MinimumAgeRequirement requirement, IResolverContext resolverContext) { // Omitted code for brevity }}
Global authorization
We can also apply authorization to our entire GraphQL endpoint, by calling RequireAuthorization() on the GraphQLEndpointConventionBuilder.
public class Startup{ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseRouting();
app.UseAuthentication(); app.UseAuthorization();
app.UseEndpoints(endpoints => { endpoints.MapGraphQL().RequireAuthorization(); }); }}
⚠️ Note: This will also block unauthenticated access to GraphQL IDEs hosted on that endpoint, like Banana Cake Pop.
This method also accepts roles and policies as arguments, similiar to the Authorize attribute / methods.