Cloudey.Reflex.Authorization.HotChocolate 3.1.0

Prefix Reserved
dotnet add package Cloudey.Reflex.Authorization.HotChocolate --version 3.1.0                
NuGet\Install-Package Cloudey.Reflex.Authorization.HotChocolate -Version 3.1.0                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Cloudey.Reflex.Authorization.HotChocolate" Version="3.1.0" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Cloudey.Reflex.Authorization.HotChocolate --version 3.1.0                
#r "nuget: Cloudey.Reflex.Authorization.HotChocolate, 3.1.0"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Cloudey.Reflex.Authorization.HotChocolate as a Cake Addin
#addin nuget:?package=Cloudey.Reflex.Authorization.HotChocolate&version=3.1.0

// Install Cloudey.Reflex.Authorization.HotChocolate as a Cake Tool
#tool nuget:?package=Cloudey.Reflex.Authorization.HotChocolate&version=3.1.0                

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 => 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; } 
}

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 => 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 Authorize<T> attribute from this library 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: [Guard("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 => 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<T>] 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("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("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("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 © 2024 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 Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
3.1.0 80 6/15/2024
3.0.2 70 6/15/2024
3.0.1 74 6/15/2024
3.0.0 68 6/15/2024
2.0.0 94 4/19/2024
1.1.2 222 7/29/2023
1.1.1 147 7/29/2023
1.1.0 251 4/24/2023
1.0.3 206 4/21/2023
1.0.2 176 4/21/2023
1.0.1 208 4/21/2023
1.0.0 202 4/21/2023