|
| 1 | +--- |
| 2 | +title: Authorization |
| 3 | +--- |
| 4 | + |
| 5 | +import { ExampleTabs } from "../../../components/mdx/example-tabs" |
| 6 | + |
| 7 | +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. |
| 8 | + |
| 9 | +Authentication is a prerequisite of Authorization, as we first need to validate a user's "authenticity" before we can evaluate his authorization claims. |
| 10 | + |
| 11 | +[Learn how to setup authentication](/docs/hotchocolate/security/authentication) |
| 12 | + |
| 13 | +# Setup |
| 14 | + |
| 15 | +After we have successfully setup authentication, there are only a few things left to do. |
| 16 | + |
| 17 | +1. Register the necessary ASP.NET Core services |
| 18 | + |
| 19 | +```csharp |
| 20 | +public class Startup |
| 21 | +{ |
| 22 | + public void ConfigureServices(IServiceCollection services) |
| 23 | + { |
| 24 | + services.AddAuthorization(); |
| 25 | + |
| 26 | + // Omitted code for brevity |
| 27 | +
|
| 28 | + services |
| 29 | + .AddGraphQLServer() |
| 30 | + .AddAuthorization() |
| 31 | + .AddQueryType<Query>(); |
| 32 | + } |
| 33 | +} |
| 34 | +``` |
| 35 | + |
| 36 | +> ⚠️ Note: We need to call `AddAuthorization()` on the `IServiceCollection`, to register the services needed by ASP.NET Core, and on the `IRequestExecutorBuilder` to register the `@authorize` directive and middleware. |
| 37 | +
|
| 38 | +2. Register the ASP.NET Core authorization middleware with the request pipeline by calling `UseAuthorization` |
| 39 | + |
| 40 | +```csharp |
| 41 | +public class Startup |
| 42 | +{ |
| 43 | + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) |
| 44 | + { |
| 45 | + app.UseRouting(); |
| 46 | + |
| 47 | + app.UseAuthentication(); |
| 48 | + app.UseAuthorization(); |
| 49 | + |
| 50 | + app.UseEndpoints(endpoints => |
| 51 | + { |
| 52 | + endpoints.MapGraphQL(); |
| 53 | + }); |
| 54 | + } |
| 55 | +} |
| 56 | +``` |
| 57 | + |
| 58 | +# Usage |
| 59 | + |
| 60 | +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. |
| 61 | + |
| 62 | +<ExampleTabs> |
| 63 | +<ExampleTabs.Annotation> |
| 64 | + |
| 65 | +In the Annotation-based approach we can use the `[Authorize]` attribute to add the `@authorize` directive. |
| 66 | + |
| 67 | +```csharp |
| 68 | +[Authorize] |
| 69 | +public class User |
| 70 | +{ |
| 71 | + public string Name { get; set; } |
| 72 | + |
| 73 | + [Authorize] |
| 74 | + public Address Address { get; set; } |
| 75 | +} |
| 76 | +``` |
| 77 | + |
| 78 | +> ⚠️ Note: We need to use the `HotChocolate.AspNetCore.AuthorizationAttribute` instead of the `Microsoft.AspNetCore.AuthorizationAttribute`. |
| 79 | +
|
| 80 | +</ExampleTabs.Annotation> |
| 81 | +<ExampleTabs.Code> |
| 82 | + |
| 83 | +```csharp |
| 84 | +public class UserType : ObjectType<User> |
| 85 | +{ |
| 86 | + protected override void Configure(IObjectTypeDescriptor<User> descriptor) |
| 87 | + { |
| 88 | + descriptor.Authorize(); |
| 89 | + |
| 90 | + descriptor.Field(f => f.Address).Authorize(); |
| 91 | + } |
| 92 | +} |
| 93 | +``` |
| 94 | + |
| 95 | +</ExampleTabs.Code> |
| 96 | +<ExampleTabs.Schema> |
| 97 | + |
| 98 | +```sdl |
| 99 | +type User @authorize { |
| 100 | + name: String! |
| 101 | + address: Address! @authorize |
| 102 | +} |
| 103 | +``` |
| 104 | + |
| 105 | +</ExampleTabs.Schema> |
| 106 | +</ExampleTabs> |
| 107 | + |
| 108 | +Specified on a type the `@authorize` directive will be applied to each field of that type. Its authorization logic is executed once for each individual field, depending on whether it was selected by the requestor or not. If the directive is placed on an individual field, it overrules the one on the type. |
| 109 | + |
| 110 | +If we do not specify any arguments to the `@authorize` directive, it will only enforce that the requestor is authenticated, nothing more. If he is not and tries to access an authorized field, a GraphQL error will be raised and the field result set to `null`. |
| 111 | + |
| 112 | +## Roles |
| 113 | + |
| 114 | +Roles provide a very intuitive way of dividing our users into groups with different access rights. |
| 115 | + |
| 116 | +When building our `ClaimsPrincipal`, we just have to add one or more role claims. |
| 117 | + |
| 118 | +```csharp |
| 119 | +claims.Add(new Claim(ClaimTypes.Role, "Administrator")); |
| 120 | +``` |
| 121 | + |
| 122 | +We can then check whether an authenticated user has these role claims. |
| 123 | + |
| 124 | +<ExampleTabs> |
| 125 | +<ExampleTabs.Annotation> |
| 126 | + |
| 127 | +```csharp |
| 128 | +[Authorize(Roles = new [] { "Guest", "Administrator" })] |
| 129 | +public class User |
| 130 | +{ |
| 131 | + public string Name { get; set; } |
| 132 | + |
| 133 | + [Authorize(Roles = new[] { "Administrator" })] |
| 134 | + public Address Address { get; set; } |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | +</ExampleTabs.Annotation> |
| 139 | +<ExampleTabs.Code> |
| 140 | + |
| 141 | +```csharp |
| 142 | +public class UserType : ObjectType<User> |
| 143 | +{ |
| 144 | + protected override Configure(IObjectTypeDescriptor<User> descriptor) |
| 145 | + { |
| 146 | + descriptor.Authorize(new[] { "Guest", "Administrator" }); |
| 147 | + |
| 148 | + descriptor.Field(t => t.Address).Authorize(new[] { "Administrator" }); |
| 149 | + } |
| 150 | +} |
| 151 | +``` |
| 152 | + |
| 153 | +</ExampleTabs.Code> |
| 154 | +<ExampleTabs.Schema> |
| 155 | + |
| 156 | +```sdl |
| 157 | +type User @authorize(roles: [ "Guest", "Administrator" ]) { |
| 158 | + name: String! |
| 159 | + address: Address! @authorize(roles: "Administrator") |
| 160 | +} |
| 161 | +``` |
| 162 | + |
| 163 | +</ExampleTabs.Schema> |
| 164 | +</ExampleTabs> |
| 165 | + |
| 166 | +> ⚠️ 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. |
| 167 | +
|
| 168 | +[Learn more about role-based authorization in ASP.NET Core](https://docs.microsoft.com/aspnet/core/security/authorization/roles) |
| 169 | + |
| 170 | +## Policies |
| 171 | + |
| 172 | +Policies allow us to create richer validation logic and decouple the authorization rules from our GraphQL resolvers. |
| 173 | + |
| 174 | +A policy consists of an [IAuthorizationRequirement](https://docs.microsoft.com/aspnet/core/security/authorization/policies#requirements) and an [AuthorizationHandler<T>](https://docs.microsoft.com/aspnet/core/security/authorization/policies#authorization-handlers). |
| 175 | + |
| 176 | +Once defined, we can register our policies like the following. |
| 177 | + |
| 178 | +```csharp |
| 179 | +public class Startup |
| 180 | +{ |
| 181 | + public void ConfigureServices(IServiceCollection services) |
| 182 | + { |
| 183 | + services.AddAuthorization(options => |
| 184 | + { |
| 185 | + options.AddPolicy("AtLeast21", policy => |
| 186 | + policy.Requirements.Add(new MinimumAgeRequirement(21))); |
| 187 | + |
| 188 | + options.AddPolicy("HasCountry", policy => |
| 189 | + policy.RequireAssertion(context => |
| 190 | + context.User.HasClaim(c => c.Type == ClaimTypes.Country))); |
| 191 | + }); |
| 192 | + |
| 193 | + services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>(); |
| 194 | + |
| 195 | + // Omitted code for brevity |
| 196 | +
|
| 197 | + services |
| 198 | + .AddGraphQLServer() |
| 199 | + .AddAuthorization() |
| 200 | + .AddQueryType<Query>(); |
| 201 | + } |
| 202 | +} |
| 203 | +``` |
| 204 | + |
| 205 | +We can then use these policies to restrict access to our fields. |
| 206 | + |
| 207 | +<ExampleTabs> |
| 208 | +<ExampleTabs.Annotation> |
| 209 | + |
| 210 | +```csharp |
| 211 | +[Authorize(Policy = "AllEmployees")] |
| 212 | +public class User |
| 213 | +{ |
| 214 | + public string Name { get; } |
| 215 | + |
| 216 | + [Authorize(Policy = "SalesDepartment")] |
| 217 | + public Address Address { get; } |
| 218 | +} |
| 219 | +``` |
| 220 | + |
| 221 | +</ExampleTabs.Annotation> |
| 222 | +<ExampleTabs.Code> |
| 223 | + |
| 224 | +```csharp |
| 225 | +public class UserType : ObjectType<User> |
| 226 | +{ |
| 227 | + protected override Configure(IObjectTypeDescriptor<User> descriptor) |
| 228 | + { |
| 229 | + descriptor.Authorize("AllEmployees"); |
| 230 | + |
| 231 | + descriptor.Field(t => t.Address).Authorize("SalesDepartment"); |
| 232 | + } |
| 233 | +} |
| 234 | +``` |
| 235 | + |
| 236 | +</ExampleTabs.Code> |
| 237 | +<ExampleTabs.Schema> |
| 238 | + |
| 239 | +```sdl |
| 240 | +type User @authorize(policy: "AllEmployees") { |
| 241 | + name: String! |
| 242 | + address: Address! @authorize(policy: "SalesDepartment") |
| 243 | +} |
| 244 | +``` |
| 245 | + |
| 246 | +</ExampleTabs.Schema> |
| 247 | +</ExampleTabs> |
| 248 | + |
| 249 | +This essentially uses the provided policy and runs it against the `ClaimsPrinciple` that is associated with the current request. |
| 250 | + |
| 251 | +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 they meet all of the specified conditions. |
| 252 | + |
| 253 | +<ExampleTabs> |
| 254 | +<ExampleTabs.Annotation> |
| 255 | + |
| 256 | +```csharp |
| 257 | +[Authorize(Policy = "AtLeast21")] |
| 258 | +[Authorize(Policy = "HasCountry")] |
| 259 | +public class User |
| 260 | +{ |
| 261 | + public string Name { get; set; } |
| 262 | +} |
| 263 | +``` |
| 264 | + |
| 265 | +</ExampleTabs.Annotation> |
| 266 | +<ExampleTabs.Code> |
| 267 | + |
| 268 | +```csharp |
| 269 | +public class UserType : ObjectType<User> |
| 270 | +{ |
| 271 | + protected override Configure(IObjectTypeDescriptor<User> descriptor) |
| 272 | + { |
| 273 | + descriptor |
| 274 | + .Authorize("AtLeast21") |
| 275 | + .Authorize("HasCountry"); |
| 276 | + } |
| 277 | +} |
| 278 | +``` |
| 279 | + |
| 280 | +</ExampleTabs.Code> |
| 281 | +<ExampleTabs.Schema> |
| 282 | + |
| 283 | +```sdl |
| 284 | +type User |
| 285 | + @authorize(policy: "AtLeast21") |
| 286 | + @authorize(policy: "HasCountry") { |
| 287 | + name: String! |
| 288 | +} |
| 289 | +``` |
| 290 | + |
| 291 | +</ExampleTabs.Schema> |
| 292 | +</ExampleTabs> |
| 293 | + |
| 294 | +[Learn more about policy-based authorization in ASP.NET Core](https://docs.microsoft.com/aspnet/core/security/authorization/policies) |
| 295 | + |
| 296 | +### IResolverContext within an AuthorizationHandler |
| 297 | + |
| 298 | +If we need to, we can also access the `IResolverContext` in our `AuthorizationHandler`. |
| 299 | + |
| 300 | +```csharp |
| 301 | +public class MinimumAgeHandler |
| 302 | + : AuthorizationHandler<MinimumAgeRequirement, IResolverContext> |
| 303 | +{ |
| 304 | + protected override Task HandleRequirementAsync( |
| 305 | + AuthorizationHandlerContext context, |
| 306 | + MinimumAgeRequirement requirement, |
| 307 | + IResolverContext resolverContext) |
| 308 | + { |
| 309 | + // Omitted code for brevity |
| 310 | + } |
| 311 | +} |
| 312 | +``` |
| 313 | + |
| 314 | +# Global authorization |
| 315 | + |
| 316 | +We can also apply authorization to our entire GraphQL endpoint. To do this, simply call `RequireAuthorization()` on the `GraphQLEndpointConventionBuilder`. |
| 317 | + |
| 318 | +```csharp |
| 319 | +public class Startup |
| 320 | +{ |
| 321 | + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) |
| 322 | + { |
| 323 | + app.UseRouting(); |
| 324 | + |
| 325 | + app.UseAuthentication(); |
| 326 | + app.UseAuthorization(); |
| 327 | + |
| 328 | + app.UseEndpoints(endpoints => |
| 329 | + { |
| 330 | + endpoints.MapGraphQL().RequireAuthorization(); |
| 331 | + }); |
| 332 | + } |
| 333 | +} |
| 334 | +``` |
| 335 | + |
| 336 | +> ⚠️ Note: This will also block unauthenticated access to GraphQL IDEs hosted on that endpoint, like Banana Cake Pop. |
| 337 | +
|
| 338 | +This method also accepts [roles](#roles) and [policies](#policies) as arguments, similiar to the `Authorize` attribute / methods. |
| 339 | + |
| 340 | +# Modifying the ClaimsPrincipal |
| 341 | + |
| 342 | +Sometimes we might want to add additional [ClaimsIdentity](https://docs.microsoft.com/dotnet/api/system.security.claims.claimsidentity) to our `ClaimsPrincipal` or modify the default identity. |
| 343 | + |
| 344 | +Hot Chocolate provides the ability to register an `IHttpRequestInterceptor`, allowing us to modify the incoming HTTP request, before it is passed along to the execution engine. |
| 345 | + |
| 346 | +```csharp |
| 347 | +public class HttpRequestInterceptor : DefaultHttpRequestInterceptor |
| 348 | +{ |
| 349 | + public override ValueTask OnCreateAsync(HttpContext context, |
| 350 | + IRequestExecutor requestExecutor, IQueryRequestBuilder requestBuilder, |
| 351 | + CancellationToken cancellationToken) |
| 352 | + { |
| 353 | + var identity = new ClaimsIdentity(); |
| 354 | + identity.AddClaim(new Claim(ClaimTypes.Country, "us")); |
| 355 | + |
| 356 | + context.User.AddIdentity(identity); |
| 357 | + |
| 358 | + return base.OnCreateAsync(context, requestExecutor, requestBuilder, |
| 359 | + cancellationToken); |
| 360 | + } |
| 361 | +} |
| 362 | + |
| 363 | +public class Startup |
| 364 | +{ |
| 365 | + public void ConfigureServices(IServiceCollection services) |
| 366 | + { |
| 367 | + services |
| 368 | + .AddGraphQLServer() |
| 369 | + .AddHttpRequestInterceptor<HttpRequestInterceptor>(); |
| 370 | + |
| 371 | + // Omitted code for brevity |
| 372 | + } |
| 373 | +} |
| 374 | +``` |
| 375 | + |
| 376 | +The `IHttpRequestInterceptor` can be used for many other things as well, not just for modifying the `ClaimsPrincipal`. |
0 commit comments