Cloudey.Reflex.Authorization.HotChocolate
1.1.1
Prefix Reserved
See the version list below for details.
dotnet add package Cloudey.Reflex.Authorization.HotChocolate --version 1.1.1
NuGet\Install-Package Cloudey.Reflex.Authorization.HotChocolate -Version 1.1.1
<PackageReference Include="Cloudey.Reflex.Authorization.HotChocolate" Version="1.1.1" />
paket add Cloudey.Reflex.Authorization.HotChocolate --version 1.1.1
#r "nuget: Cloudey.Reflex.Authorization.HotChocolate, 1.1.1"
// Install Cloudey.Reflex.Authorization.HotChocolate as a Cake Addin #addin nuget:?package=Cloudey.Reflex.Authorization.HotChocolate&version=1.1.1 // Install Cloudey.Reflex.Authorization.HotChocolate as a Cake Tool #tool nuget:?package=Cloudey.Reflex.Authorization.HotChocolate&version=1.1.1
Reflex
Authorization for HotChocolate
Utilities for easy-to-use authorization with HotChocolate GraphQL server.
Features
- Assertions for types, fields, parent types, and resolver arguments/input
- Easy policy-based authorization
- Works with Cloudey.Reflex.Authorization
Installation
Install with NuGet
Make sure authorization is enabled for your HotChocolate server:
services.AddGraphQLServer()
// ...
.AddAuthorization() // <-- Enable authorization
// ...
Using the GraphQL assertions
Parent assertion
RequireParentAssertion
enables you to assert that the parent of a field fulfils a condition.
NOTE
When using projection, non-projected fields will not be resolved in the delegate. Therefore, it is important that you mark any other fields that you need in your delegate with the [IsProjected]
attribute to make sure they are always loaded, otherwise they will be null
in the assertion delegate.
The delegate receives the following arguments:
- The parent of type
T
- The authorization context
AuthorizationHandlerContext
- The GraphQL context
IMiddlewareContext
Example:
// UserPolicy.cs
// IMPORTANT: Defining policies in this way requires setting up Cloudey.Reflex.Authorization
public class SecretKeyPolicy : IPolicy
{
public static AuthorizationPolicy Policy { get; } = new AuthorizationPolicyBuilder()
.RequireParentAssertion<User>(
// The secret key can only be accessed by the user itself or an admin
(user, context, directiveContext) => user.Id == context.User.GetId() || context.User.IsInRole(Role.Admin)
)
.Build();
}
// User.cs
public class User : Entity {
// ...
[Guard<SecretKeyPolicy>]
public string SecretKey { get; set; }
}
Field assertion
RequireResultAssertion
enables you to assert that the resolved entity or field fulfills a condition.
Important
If the policy is applied to a field, the target is the value of the field.
If the policy is applied to a class, the target is the class instance.
If the policy is applied to a resolver, the target is the result of the resolver.
If the result is an IEnumerable, then the assertion is applied to all elements.
The delegate receives the following arguments:
- The field value
T
- The authorization context
AuthorizationHandlerContext
- The GraphQL context
IMiddlewareContext
Example:
// UserPolicy.cs
// IMPORTANT: Defining policies in this way requires setting up Cloudey.Reflex.Authorization
public class AvatarPolicy : IPolicy
{
public static AuthorizationPolicy Policy { get; } = new AuthorizationPolicyBuilder()
.RequireTargetAssertion<Avatar>(
// The avatar can only be accessed if it is set as public
(avatar, context, directiveContext) => avatar.IsPublic
)
.Build();
}
// User.cs
public class User : Entity {
// ...
[Guard<AvatarPolicy>] // Can be applied here to only have an effect when accessed through User
public Avatar? Avatar { get; set; }
}
// Avatar.cs
[Guard<AvatarPolicy>] // Can also be applied here to always have an effect whenever Avatar is resolved, incl. through other types
public class Avatar : Entity {
// ...
public bool IsPublic { get; set; }
}
Related assertion
RequireRelatedAssertion
enables you to assert that the field or the entity containing the field fulfills a condition. This is useful for defining policies used on both fields, classes, and resolvers interchangeably.
Important
If the policy is applied to a field with type T, the target is the value of the field.
If the policy is applied to a member of T which is not of type T, the target is an instance of the parent T.
If the policy is applied to a class of type T, the target is the instance of the class.
If the policy is applied to a resolver of return type T, the result is the result of the resolver.
If the result is an IEnumerable, then the assertion is applied to all elements.
NOTE
When using projection, non-projected fields will not be resolved in the delegate. Therefore, it is important that you mark any other fields that you need in your delegate with the [IsProjected]
attribute to make sure they are always loaded, otherwise they will be null
in the resolver if the field is not requested.
The delegate receives the following arguments:
- The related type
T
- The authorization context
AuthorizationHandlerContext
- The GraphQL context
IMiddlewareContext
Example:
// UserPolicy.cs
// IMPORTANT: Defining policies in this way requires setting up Cloudey.Reflex.Authorization
public class AvatarPolicy : IPolicy
{
public static AuthorizationPolicy Policy { get; } = new AuthorizationPolicyBuilder()
.RequireRelatedAssertion<Avatar>(
// The avatar can only be accessed if it is set as public
(avatar, context, directiveContext) => avatar.IsPublic
)
.Build();
}
// User.cs
public class User : Entity {
// ...
[Guard<AvatarPolicy>] // Here, the assertion is applied to Avatar
public Avatar? Avatar { get; set; }
}
// Avatar.cs
[Guard<AvatarPolicy>] // Here, the assertion is applied to Avatar
public class Avatar : Entity {
// ...
[Guard<AvatarPolicy>] // Here, the assertion is applied to Avatar (the parent)
public bool IsPublic { get; set; }
[Guard<AvatarPolicy>] // Here, the assertion is applied to the FIELD of type Avatar (the field not the parent!)
public Avatar AlternativeAvatar { get; set; } // Just an example
}
Argument assertion
RequireArgumentAssertion enables you to assert that an argument to the given resolver (query or mutation method) fulfills a condition. This is useful for authorising based on input objects.
Important
Policies with argument assertions should be applied with BEFORE_RESOLVER ApplyPolicy to avoid unintended side effects!
By default, the Guard
attribute applies an AFTER_RESOLVER policy, which means that the authorisation only runs after the resolver has returned a result. In the case of mutations, this could lead to the action being taken without authorisation.
Note that this also means the policy cannot contain assertions that work with the resolver result (i.e. RequireParentAssertion, RequireResultAssertion, RequireRelatedAssertion).
Example:
[QueryType]
public class HelloWorldQuery
{
// If not using Reflex authorisation: [Authorize("NameOfYourPolicy", ApplyPolicy.BeforeResolver]
[Guard<HelloWorldQueryPolicy>(ApplyPolicy.BeforeResolver)]
public HelloWorldPayload HelloWorld (HelloWorldInput input)
{
return new HelloWorldPayload
{ Message = $"Hello {input.Name}!" };
}
public record HelloWorldInput(string Name);
public record HelloWorldPayload
{
public string Message { get; init; } = string.Empty;
}
// Defining policies in this way requires using the Cloudey.Reflex.Authorization package!
public class HelloWorldQueryPolicy : IPolicy
{
public static AuthorizationPolicy Policy { get; } = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
// Only John is authorised to receive greetings in this application
.RequireArgumentAssertion<HelloWorldInput>(
(input, context, middlewareContext) => input?.Name == "John"
)
.Build();
}
Authorization in general
To authenticate a given request, entity, or property, use the [Guard]
attribute. The Guard attribute can be used to authenticate based on roles or a policy, and replaces the Authorize
attribute from HotChocolate.
Authorizing a query or mutation
Apply the [Guard]
attribute to the query or mutation method, eg:
[QueryType]
public class MyQuery {
...
[Guard(new[] { "Admin" })] // Only users with the "Admin" role can access this query
public async string GetHello () {
return "Hello";
}
}
Authorizing an entity
Apply the [Guard]
attribute to the entity class, eg:
[Guard(new[] { "Admin" })]
public class SecretInformation : Entity {
...
}
This prevents anyone without the Admin
role from accessing the entity in any query.
Authorizing a property
You can also restrict access on a field-level. Apply the [Guard]
attribute to the field, eg:
public class User : Entity {
public Guid Id {get; set;}
[Guard(new[] { "Admin" })]
public string PasswordHash {get; set;}
}
This disallows access to the PasswordHash fields for everyone except Admins.
Combining the authorization attributes
You can apply authorization attributes on multiple levels
, and they will all be executed in order. E.g. you can allow access to the Role entity to all authenticated users with a [Guard]
attribute on the Role class, but only allow access for Admins to a specific field in that entity by adding [Guard(new[] { "Admin" })]
to that field.
Using policies
When simple role-based authentication is not enough, you can also use policies to create more complex authorization logic. For an easy way to implement policy-based authorization, see Cloudey.Reflex.Authorization.
License
Licensed under Apache 2.0.
Copyright © 2023 Cloudey IT Ltd
Cloudey® is a registered trademark of Cloudey IT Ltd. Use of the trademark is NOT GRANTED under the license of this repository or software package.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
-
net7.0
- Cloudey.Reflex.Authorization (>= 1.0.0)
- HotChocolate.Authorization (>= 13.4.0)
- HotChocolate.Types (>= 13.4.0)
- Microsoft.AspNetCore.Authorization (>= 7.0.9)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.