Skip to content

Commit b5e3c75

Browse files
authoredJul 1, 2021
Added authorization docs (#3830)
1 parent ff81245 commit b5e3c75

File tree

4 files changed

+590
-0
lines changed

4 files changed

+590
-0
lines changed
 

‎website/src/docs/docs.json

+18
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,24 @@
190190
}
191191
]
192192
},
193+
{
194+
"path": "security",
195+
"title": "Security",
196+
"items": [
197+
{
198+
"path": "index",
199+
"title": "Overview"
200+
},
201+
{
202+
"path": "authentication",
203+
"title": "Authentication"
204+
},
205+
{
206+
"path": "authorization",
207+
"title": "Authorization"
208+
}
209+
]
210+
},
193211
{
194212
"path": "api-reference",
195213
"title": "API Reference",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
---
2+
title: Authentication
3+
---
4+
5+
import { ExampleTabs } from "../../../components/mdx/example-tabs"
6+
7+
Authentication allows us to determine a user's identity. This is of course a prerequisite for authorization, but it also allows us to access the authenticated user in our resolvers. This is useful, if we for example want to build a `me` field that fetches details about the authenticated user.
8+
9+
Hot Chocolate fully embraces the authentication capabilities of ASP.NET Core, making it easy to reuse existing authentication configuration and integrating a variety of authentication providers.
10+
11+
[Learn more about authentication in ASP.NET Core](https://docs.microsoft.com/aspnet/core/security/authentication)
12+
13+
# Setup
14+
15+
Setting up authentication is largely the same as in any other ASP.NET Core application.
16+
17+
**In the following example we are using JWTs, but we could use any other authentication scheme supported by ASP.NET Core.**
18+
19+
1. Install the `Microsoft.AspNetCore.Authentication.JwtBearer` package
20+
21+
```bash
22+
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
23+
```
24+
25+
2. Register the JWT authentication scheme
26+
27+
```csharp
28+
public class Startup
29+
{
30+
public void ConfigureServices(IServiceCollection services)
31+
{
32+
var signingKey = new SymmetricSecurityKey(
33+
Encoding.UTF8.GetBytes("MySuperSecretKey"));
34+
35+
services
36+
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
37+
.AddJwtBearer(options =>
38+
{
39+
options.TokenValidationParameters =
40+
new TokenValidationParameters
41+
{
42+
ValidIssuer = "https://auth.chillicream.com",
43+
ValidAudience = "https://graphql.chillicream.com",
44+
ValidateIssuerSigningKey = true,
45+
IssuerSigningKey = signingKey
46+
};
47+
});
48+
}
49+
}
50+
```
51+
52+
> ⚠️ Note: This is an example configuration that's not intended for use in a real world application.
53+
54+
3. Register the ASP.NET Core authentication middleware with the request pipeline by calling `UseAuthentication`
55+
56+
```csharp
57+
public class Startup
58+
{
59+
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
60+
{
61+
app.UseRouting();
62+
63+
app.UseAuthentication();
64+
65+
app.UseEndpoints(endpoints =>
66+
{
67+
endpoints.MapGraphQL();
68+
});
69+
}
70+
}
71+
```
72+
73+
The above takes care of parsing and validating an incoming HTTP request.
74+
75+
In order to make the authentication result available to our resolvers, we need to complete some additional, Hot Chocolate specific steps.
76+
77+
1. Install the `HotChocolate.AspNetCore.Authorization` package
78+
79+
```bash
80+
dotnet add package HotChocolate.AspNetCore.Authorization
81+
```
82+
83+
2. Call `AddAuthorization()` on the `IRequestExecutorBuilder`
84+
85+
```csharp
86+
services
87+
.AddGraphQLServer()
88+
.AddAuthorization()
89+
.AddQueryType<Query>();
90+
```
91+
92+
All of this does not yet lock out unauthenticated users. It only exposes the identity of the authenticated user to our application through a `ClaimsPrincipal`. If we want to prevent certain users from querying our graph, we need to utilize authorization.
93+
94+
[Learn more about authorization](/docs/hotchocolate/security/authorization)
95+
96+
# Accessing the ClaimsPrincipal
97+
98+
The [ClaimsPrincipal](https://docs.microsoft.com/dotnet/api/system.security.claims.claimsprincipal) of an authenticated user can be accessed in our resolvers like the following.
99+
100+
<ExampleTabs>
101+
<ExampleTabs.Annotation>
102+
103+
```csharp
104+
public class Query
105+
{
106+
public User GetMe(ClaimsPrincipal claimsPrincipal)
107+
{
108+
// Omitted code for brevity
109+
}
110+
111+
// before v11.3.1
112+
public User GetMeLegacy(
113+
[GlobalState(nameof(ClaimsPrincipal))] ClaimsPrincipal claimsPrincipal)
114+
{
115+
// Omitted code for brevity
116+
}
117+
}
118+
```
119+
120+
</ExampleTabs.Annotation>
121+
<ExampleTabs.Code>
122+
123+
```csharp
124+
public class QueryType : ObjectType
125+
{
126+
protected override void Configure(IObjectTypeDescriptor descriptor)
127+
{
128+
descriptor
129+
.Field("me")
130+
.Resolve(context =>
131+
{
132+
var claimsPrincipal = context.GetUser();
133+
// before v11.3.1
134+
var claimsPrincipal = context.GetGlobalValue<ClaimsPrincipal>(
135+
nameof(ClaimsPrincipal));
136+
137+
// Omitted code for brevity
138+
});
139+
}
140+
}
141+
```
142+
143+
</ExampleTabs.Code>
144+
<ExampleTabs.Schema>
145+
146+
```csharp
147+
services
148+
.AddGraphQLServer()
149+
.AddDocumentFromString(@"
150+
type Query {
151+
me: User
152+
meLegacy: User
153+
}
154+
")
155+
.AddResolver("Query", "me", (context) =>
156+
{
157+
var claimsPrincipal = context.GetUser();
158+
// before v11.3.1
159+
var claimsPrincipal = context.GetGlobalValue<ClaimsPrincipal>(
160+
nameof(ClaimsPrincipal));
161+
162+
// Omitted code for brevity
163+
})
164+
```
165+
166+
</ExampleTabs.Schema>
167+
</ExampleTabs>
168+
169+
With the authenticated user's `ClaimsPrincipal`, we can now access their claims.
170+
171+
```csharp
172+
var userId = claimsPrincipal.FindFirstValue(ClaimTypes.NameIdentifier);
173+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
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&#x3C;T&#x3E;](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`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
title: "Overview"
3+
---
4+
5+
In this section we will learn how to secure our GraphQL endpoint.
6+
7+
# Authentication
8+
9+
Authentication in Hot Chocolate is built around the official authentication mechanisms in ASP.NET Core, allowing us to fully embrace their customizability and variety of authentication providers.
10+
11+
[Learn more about authentication](/docs/hotchocolate/security/authentication)
12+
13+
# Authorization
14+
15+
Authorization is one of the most basic security concepts. It builds on top of authentication and allows us to restrict access to types and fields, based on whether a user is authenticated, assigned specific roles or satisfies one or more policies. Hot Chocolate closely matches and nicely integrates with the official ASP.NET Core authorization APIs.
16+
17+
[Learn more about authorization](/docs/hotchocolate/security/authorization)
18+
19+
<!-- # Persisted Queries
20+
21+
Depending on our setup and requirements, the simplest way to make our server secure and control the request impact is to use persisted queries. With this approach, we can export the request from our client applications at development time and only allow the set of known queries to be executed in our production environment.
22+
23+
[Learn more about persisted queries](/docs/hotchocolate/performance/persisted-queries) -->

0 commit comments

Comments
 (0)
Please sign in to comment.