Search
K
  1. Security

Security

Introduction

The security aspects in FastEndpoints is built around the same authentication & authorization middleware that you're used to in ASP.NET such as JWT Bearer, Cookie, Identity, etc. Convenience wrappers are provided for JWT and Cookie schemes as described below.

Once auth middleware is configured, authorization requirements for endpoints can be specified in a convenient manner inside endpoints themselves. Roles/Claims/Permissions/Policies are all supported.

Endpoints are secure by default. You'd have to explicitly call AllowAnonymous() in the configuration if unauthenticated access is to be allowed to a particular endpoint.


JWT Bearer Authentication

Support for easy JWT Bearer Authentication is provided. Install the FastEndpoints.Security package and register it in the middleware pipeline like so:

terminal
  dotnet add package FastEndpoints.Security
Program.cs
using FastEndpoints;
using FastEndpoints.Security; //add this

var bld = WebApplication.CreateBuilder();
bld.Services
   .AddAuthenticationJwtBearer(s => s.SigningKey = "The secret used to sign tokens") //add this
   .AddAuthorization() //add this
   .AddFastEndpoints();

var app = bld.Build();
app.UseAuthentication() //add this
   .UseAuthorization() //add this
   .UseFastEndpoints();
app.Run();

The action supplied to the AddAuthenticationJwtBearer() method allows you to configure other advanced bearer token consumption options as needed.

Generating JWT Tokens

JWTs can be easily generated with an endpoint that signs in users such as the following:

public class UserLoginEndpoint : Endpoint<LoginRequest>
{
    public override void Configure()
    {
        Post("/api/login");
        AllowAnonymous();
    }

    public override async Task HandleAsync(LoginRequest req, CancellationToken ct)
    {
        if (await authService.CredentialsAreValid(req.Username, req.Password, ct))
        {
            var jwtToken = JwtBearer.CreateToken(
                o =>
                {
                    o.SigningKey = "A secret token signing key";
                    o.ExpireAt = DateTime.UtcNow.AddDays(1);
                    o.User.Roles.Add("Manager", "Auditor");
                    o.User.Claims.Add(("UserName", req.Username));
                    o.User["UserId"] = "001"; //indexer based claim setting
                });

            await SendAsync(
                new
                {
                    req.Username,
                    Token = jwtToken
                });
        }
        else
            ThrowError("The supplied credentials are invalid!");
    }
}

JwtBearer.CreateToken() static method can be used to supply the necessary arguments such as any user roles, claims, permissions, etc. for generating a token.


If your client applications have support for cookies, you can use cookies for auth instead of JWTs. By default, the following enables cookies (http-only & samesite-lax), so you can store user claims in the encrypted ASP.NET cookie without having to worry about the safety of front-end application storage choice.

Program.cs
using FastEndpoints;
using FastEndpoints.Security; //add this

var bld = WebApplication.CreateBuilder();
bld.Services
   .AddAuthenticationCookie(validFor: TimeSpan.FromMinutes(10)) //configure cookie auth
   .AddAuthorization(); //add this
   .AddFastEndpoints()

var app = bld.Build();
app.UseAuthentication() //add this
   .UseAuthorization() //add this
   .UseFastEndpoints();
app.Run();

Once the cookie auth middleware is configured, you can sign users in from within an endpoint handler by calling the following static method:

CookieAuth.SignInAsync(u =>
{
    u.Roles.Add("Admin");
    u.Permissions.AddRange(new[] { "Create_Item", "Delete_Item" });
    u.Claims.Add(new("Address", "123 Street"));

    //indexer based claim setting
    u["Email"] = "[email protected]";
    u["Department"] = "Administration";
});

The above method will embed a ClaimsPrincipal with the supplied roles/permissions/claims in an encrypted cookie and add it to the response. CookieAuth.SignOutAsync () can be used to sign users out.


Endpoint Authorization

Once an authentication middleware is registered such as JWT Bearer, or Cookie as shown above, access can be restricted to users based on the following:

  • Policies
  • Claims
  • Roles
  • Permissions

Pre-Built Security Policies

Security policies can be pre-built and registered during app startup and endpoints can choose to allow access to users based on the registered policy names like so:

Program.cs

bld.Services.AddAuthorization(options =>
{
    options.AddPolicy("ManagersOnly", x => x.RequireRole("Manager").RequireClaim("ManagerID"));
})
UpdateUserEndpoint.cs
public class UpdateUserEndpoint : Endpoint<UpdateUserRequest>
{
    public override void Configure()
    {
        Put("/api/users/update");
        Policies("ManagersOnly");
    }
}

Declarative Security Policies

Instead of registering each security policy at startup, you can selectively specify security requirements for each endpoint in the endpoint configuration itself like so:

RestrictedEndpoint.cs
public class RestrictedEndpoint : Endpoint<RestrictedRequest>
{
    public override void Configure()
    {
        Post("/api/restricted");
        Claims("AdminID", "EmployeeID");
        Roles("Admin", "Manager");
        Permissions("UpdateUsersPermission", "DeleteUsersPermission");
        Policy(x => x.RequireAssertion(...));
    }
}

Claims() method

With this method you are specifying that if a user principal has ANY of the specified claims, access should be granted. If the requirement is to allow access only if ALL specified claims are present, you can use the ClaimsAll() method.

Permissions() method

Just like above, you can specify that ANY of the specified permissions should allow access. Or require ALL of the specified permissions by using the PermissionsAll() method.

Roles() method

Similarly, you are specifying that if a user principal has ANY of the given roles, access should be allowed.

Policy() method

You can specify an action to be performed on an AuthorizationPolicyBuilder for specifying any other authorization requirements that can't be satisfied by the above methods.

AllowAnonymous() method

Call this method if unauthenticated users are to be allowed access to a particular endpoint.

It is also possible to specify which http verbs you'd like to allow anonymous access to if your endpoint is listening on multiple verbs & routes like so:

RestrictedEndpoint.cs
public class RestrictedEndpoint : Endpoint<RestrictedRequest>
{
    public override void Configure()
    {
        Verbs(Http.POST, Http.PUT, Http.PATCH);
        Routes("/api/restricted");
        AllowAnonymous(Http.POST);
    }
}

The above endpoint is listening for all 3 http methods on the same route but only POST method is allowed to be accessed anonymously. It is useful for example when you'd like to use the same handler logic for create/replace/update scenarios and create operation is allowed to be done by anonymous users. Using just AllowAnonymous() without any arguments means all verbs are allowed anonymous access.


Other Auth Providers

All auth providers compatible with the ASP.NET middleware pipeline can be registered and used like above.

INFO

Here's an example project using Auth0 with permission based authorization.


Multiple Authentication Schemes

Multiple schemes can be configured as you'd typically do in the ASP.NET middleware pipeline and specify per endpoint which schemes are to be used for authenticating incoming requests.

Program.cs
bld.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options => options.SlidingExpiration = true) // cookie auth
.AddJwtBearer(options =>                                // jwt bearer auth
{
    options.Authority = $"https://{bld.Configuration["Auth0:Domain"]}/";
    options.Audience = bld.Configuration["Auth0:Audience"];
});
Endpoint.cs
public override void Configure()
{
    Get("/account/profile");
    AuthSchemes(JwtBearerDefaults.AuthenticationScheme);
}

In the above example, we're registering both Cookie and JWT Bearer schemes and in the endpoint we're saying only JWT Bearer scheme should be used for authenticating incoming requests to the endpoint. You can specify multiple schemes and if an incoming request isn't using any of the said schemes, access will not be allowed.

The Default Authentication Scheme

When using the provided convenience methods such as AddAuthenticationJwtBearer() and AddAuthenticationCookie() together, whichever scheme registered last becomes the default scheme. For example, if JWT was registered first and Cookie last, then Cookie auth becomes the default. If you'd like to be explicit about what the default scheme should be, you can do so like below:

bld.Services.AddAuthenticationJwtBearer(...);
bld.Services.AddAuthenticationCookie(...);
bld.Services.AddAuthentication(o => //must be the last auth call
{
    o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
});

Default scheme with ASP.NET Identity

Explicitly setting the default auth scheme as above is essential when using Identity as well as customizing Identity models because the Add*Identity<>() call makes Cookie auth the default scheme. This may not be the desired effect when your application also registers JWT auth and you'd expect JWT to be the default. In which case, simply register your auth pipeline as follows:

bld.Services.AddIdentity<MyUser,MyRole>(...);
bld.Services.AddAuthenticationJwtBearer(...);
bld.Services.AddAuthentication(o => 
    o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; );

See here for a demo project using the default identity together with JWT and making JWT the default scheme.

Combined Authentication Scheme

Here's an example of how you'd create a custom combined auth scheme which would combine both Cookie and JWT auth when using the wrapper methods offered by FastEndpoints:

Program.cs
bld.Services
   .AddAuthenticationCookie(validFor: TimeSpan.FromMinutes(60))
   .AddAuthenticationJwtBearer(s => s.SigningKey = "Token signing key")
   .AddAuthentication(o =>
   {
       o.DefaultScheme = "Jwt_Or_Cookie";
       o.DefaultAuthenticateScheme = "Jwt_Or_Cookie";
   })
   .AddPolicyScheme("Jwt_Or_Cookie", "Jwt_Or_Cookie", o =>
   {
       o.ForwardDefaultSelector = ctx =>
       {
           if (ctx.Request.Headers.TryGetValue(HeaderNames.Authorization, out var authHeader) &&
               authHeader.FirstOrDefault()?.StartsWith("Bearer ") is true)
           {
               return JwtBearerDefaults.AuthenticationScheme;
           }
           return CookieAuthenticationDefaults.AuthenticationScheme;
       };
   });

Custom Authentication Schemes

Creating and using custom authentication schemes is no different to how you'd typically configure them in ASP.Net using an IAuthenticationHandler implementation. See below links for examples:


JWT Refresh Tokens

Implementing refresh tokens in FastEndpoints is a simple 2-step process.

Step 1 - Login Endpoint:

Create a user login endpoint which checks the supplied user credentials such as username/password and issues an initial pair of access & refresh tokens.

LoginEndpoint.cs
public class LoginEndpoint : EndpointWithoutRequest<TokenResponse>
{
    public override void Configure()
    {
        Get("/api/login");
        AllowAnonymous();
    }

    public override async Task HandleAsync(CancellationToken c)
    {
        //user credential checking has been omitted for brevity

        Response = await CreateTokenWith<MyTokenService>("user-id-001", u =>
        {
            u.Roles.AddRange(new[] { "Admin", "Manager" });
            u.Permissions.Add("Update_Something");
            u.Claims.Add(new("UserId", "user-id-001"));
        });
    }
}

The interesting bits of info here would be the following:

  • CreateTokenWith<TTokenService>(): This is a method supplied by the endpoint base class which can be used to generate the initial response dto containing the access/refresh token pair. The token service is discussed below. The parameters of the method would be the user-id and an action for configuring which user privileges (roles/claims/permissions) are to be embedded in the generated access token.
  • MyTokenService: This is your implementation of a specialized abstract endpoint class which is configured with the relevant settings such as singing key/ audience/ issuer/ expiry times/ etc. See example below.
  • TokenResponse: This is the response dto that the token service will return when token generation succeeds.

Step 2 - Token Service:

A token service is created by implementing the RefreshTokenService<TRequest, TResponse> abstract class. This class is a bit different from the typical endpoint classes that it is configured by calling Setup() in the constructor as shown below. Also, the request and response dto generic arguments are constrained to TokenRequest & TokenResponse even though you are free to subclass those types if you need to add more properties. In addition to the endpoint setup, you need to implement 3 abstract methods as explained below. There is no HandleAsync() method like in a regular endpoint.

MyTokenService.cs
public class MyTokenService : RefreshTokenService<TokenRequest, TokenResponse>
{
    public MyTokenService(IConfiguration config)
    {
        Setup(o =>
        {
            o.TokenSigningKey = config["TokenSigningKey"];
            o.AccessTokenValidity = TimeSpan.FromMinutes(5);
            o.RefreshTokenValidity = TimeSpan.FromHours(4);

            o.Endpoint("/api/refresh-token", ep =>
            {
                ep.Summary(s => s.Summary = "this is the refresh token endpoint");
            });
        });
    }

    public override async Task PersistTokenAsync(TokenResponse response)
    {
        await Data.StoreToken(response);

        // this method will be called whenever a new access/refresh token pair is being generated.
        // store the tokens and expiry dates however you wish for the purpose of verifying
        // future refresh requests.        
    }

    public override async Task RefreshRequestValidationAsync(TokenRequest req)
    {
        if (!await Data.TokenIsValid(req.UserId, req.RefreshToken))
            AddError(r => r.RefreshToken, "Refresh token is invalid!");

        // validate the incoming refresh request by checking the token and expiry against the
        // previously stored data. if the token is not valid and a new token pair should
        // not be created, simply add validation errors using the AddError() method.
        // the failures you add will be sent to the requesting client. if no failures are added,
        // validation passes and a new token pair will be created and sent to the client.        
    }

    public override Task SetRenewalPrivilegesAsync(TokenRequest request, UserPrivileges privileges)
    {
        privileges.Roles.Add("Manager");
        privileges.Claims.Add(new("ManagerID", request.UserId));
        privileges.Permissions.Add("Manage_Department");

        // specify the user privileges to be embedded in the jwt when a refresh request is
        // received and validation has passed. this only applies to renewal/refresh requests
        // received to the refresh endpoint and not the initial jwt creation.        
    }
}
INFO

Here's an example project showcasing refresh token usage.


Source Generated Access Control Lists

In a typical application that uses permission based authorization, you'd be creating either an enum or static class such as the following to define all of the different permissions the application has:

public static class Allow
{
    public const string Article_Create = "001";
    public const string Article_Approve = "002";
    public const string Article_Reject = "003";
}

You'd then use this class to specify permission requirements for endpoints like this:

public override void Configure()
{
    Post("/article");
    Permissions(Allow.Article_Create);
}

And, in order to assign this permission to an author upon login, you'd use the same static class when creating a JWT like so:

var jwtToken = JwtBearer.CreateToken(
    priviledges: u =>
    {
        u.Permissions.Add(Allow.Article_Create);
    });

Auto Generating The Permission List

Instead of manually writing and maintaining a static class like above, you can get our source generator to create it for you. Since most permissions would be tied to a single endpoint, all you have to do is specify the name of the permission when configuring the endpoint like this:

// article creation endpoint
public override void Configure()
{
    Post("/article");
    AccessControl("Article_Create");
}

// article moderation endpoint
public override void Configure()
{
    Post("/article/moderate");
    AccessControl("Article_Approve");
    AccessControl("Article_Reject"); //can be called multiple times
}

Which results in an auto generated class like this:

Allow.g.cs
public static partial class Allow
{
    public const string Article_Create = "7OR";
    public const string Article_Approve = "LVN";
    public const string Article_Reject = "ZTT";
}

The permission code (value of the const) is a hash of the permission name and will always be the same 3 letter value for any given permission name. These 3 letter hashes would only change if you rename the permission names.

When you use the AccessControl() method to generate permissions, it doesn't automatically add the generated permission as a requirement for the endpoint. You still need to specify permission requirements using the Permissions() method:

public override void Configure()
{
    Post("/article");
    AccessControl("Article_Create");
    Permissions(Allow.Article_Create) 
    //without this, even users who don't have article creation permission -
    //would be able to access this endpoint.
}

As an alternative, you can use the following overload of the AccessControl() method to get rid of the need for doing a separate Permissions() call:

public override void Configure()
{
    Post("/article");
    AccessControl(
        keyName: "Article_Create",
        behavior: Apply.ToThisEndpoint);
}

Customizing The Generated Class

When you have certain permissions that need to be applied to multiple endpoints, they can be added in a partial class of your own like this:

namespace MyProject.Auth;

public static partial class Allow
{
    public const string Admin_Do_Anything = "000";   
}

Do note the namespace above. The auto generated Allow static class will be located in <YourAssemblyName>.Auth. So you need to use the same namespace when adding your own partial class or the compiler will not be able to combine your partial class with the auto generated class.

Generating Permission Groups

It's possible to associate a permission with any number of arbitrary group names like this:

public override void Configure()
{
    Post("/article");
    AccessControl(
        keyName: "Article_Create",
        behavior: Apply.ToThisEndpoint,
        groupNames: "Author","Admin");
}

public override void Configure()
{
    Post("/article/moderate");
    AccessControl(
        keyName: "Article_Approve",
        behavior: Apply.ToThisEndpoint,
        groupNames: "Admin");
}

The above results in a generated class with a property per each unique group name with which you can access the permissions associated with a particular group:

Allow.g.cs
public static partial class Allow
{
    public static IEnumerable<string> Author => _author;
    private static readonly List<string> _author = new()
    {
        Article_Create
    };

    public static IEnumerable<string> Admin => _admin;
    private static readonly List<string> _admin = new()
    {
        Article_Create,
        Article_Approve
    };    
}

A permission group is simply a collection of permissions which you can access via the generated properties for example when signing in a user of the type Administrator like so:

var jwtToken = JwtBearer.CreateToken(
    priviledges: u =>
    {
        u.Permissions.AddRange(Allow.Admin);
    });

Custom permissions from your own partial class can also be included in the generated groups like this:

namespace MyProject.Auth;

public static partial class Allow
{
    public const string Admin_Do_Anything = "000";
    public const string Article_Delete = "001"; 
    
    static partial void Groups()
    {
        AddToAdmin(Admin_Do_Anything);
        AddToAdmin(Article_Delete);
    }     
}

Implement the static partial method called Groups() and call the generated AddTo*(...) method to add the custom permissions to the relevant groups.

XML Doc Summaries For Permissions

If you'd like the source generator to create XML summaries like the following:

/// <summary>
/// Permission for creating new articles in the system.
/// </summary>
public const string Article_Create = "7OR";

The summary text can be provided like so:

AccessControl( //Permission for creating new articles in the system.
    keyName: "Article_Create",
    behavior: Apply.ToThisEndpoint,
    groupNames: "Author","Admin");

The text must be placed as a comment right after the opening bracket of the AccessControl method. You can also look up the summary text for a given permission via the generated Allow.Descriptions dictionary if need be.

Generated Convenience Methods

The generated Allow static class has a few methods to enable looking up of permission codes/names:

// gets a list of permission names for the given list of permission codes
public static IEnumerable<string> NamesFor(IEnumerable<string> codes){}

// gets a list of permission codes for a given list of permission names
public static IEnumerable<string> CodesFor(IEnumerable<string> names){}

// gets the permission code for a given permission name
public static string? PermissionCodeFor(string permissionName){}

// gets the permission name for a given permission code
public static string? PermissionNameFor(string permissionCode){}

// gets a permission tuple using a permission name
public static (string PermissionName, string PermissionCode)? PermissionFromName(string name){}

// gets a permission tuple using it's code
public static (string PermissionName, string PermissionCode)? PermissionFromCode(string code){}

// gets a list of all permission names
public static IEnumerable<string> AllNames(){}

// gets a list of all permission codes
public static IEnumerable<string> AllCodes(){}

// gets a list of all the defined permissions as tuples
public static IEnumerable<(string PermissionName, string PermissionCode)> AllPermissions(){}

INFO

Here's an example project showcasing the use of ACL generation.


CSRF Protection For Form Submissions (Antiforgery Tokens)

For an introduction to antiforgery tokens in ASP.NET, please refer to the MS documentation. In order to enable antiforgery token verification in FastEndpoints, follow the steps below:

Startup Configuration:

Program.cs
using Microsoft.AspNetCore.Antiforgery;

var bld = WebApplication.CreateBuilder();
bld.Services
   .AddFastEndpoints()
   .AddAntiforgery(); //add this

var app = bld.Build();
app.UseAntiforgeryFE() //must come before UseFastEndpoints()
   .UseFastEndpoints();
app.Run();

Endpoint Configuration:

Token verification is implemented as a custom middleware and it only checks for the validity of incoming antiforgery tokens for endpoints that explicitly enables verification like so:

FormEndpoint.cs
public override void Configure()
{
    Post("form-submission");
    AllowFormData();
    EnableAntiforgery();
}

Obtaining Antiforgery Tokens

Tokens can be generated using the IAntiforgery service which is obtained from the DI container. Here's an example endpoint that generates antiforgery tokens and sends it down to the client as a JSON response.

TokenEndpoint.cs
sealed class TokenEndpoint : EndpointWithoutRequest
{
    public IAntiforgery Antiforgery { get; set; } //property injection

    public override void Configure() => Get("anti-forgery-token");

    public override async Task HandleAsync(CancellationToken c)
    {
        var tokenSet = Antiforgery.GetAndStoreTokens(HttpContext);
        await SendAsync(
            new
            {
                formFieldName = tokenSet.FormFieldName,
                token = tokenSet.RequestToken
            });
    }
}

Submission Of Token With Form Data

The client application can then submit the obtained token with form data. The name of the form field that the middleware is going to check for the token can be customized at startup like so:

bld.Services.AddAntiforgery(o => o.FormFieldName = "_csrf_token_");

It's also possible to submit the token via a request header instead of a form field if need be. The name of the header can be customized like so:

bld.Services.AddAntiforgery(o => o.HeaderName = "x-csrf-token");

The antiforgery token verification service/middleware will check for the presence of either the form field or a header containing a valid token and allow/deny access to incoming requests for endpoints that specifically require an antiforgery token.

Also note that when you generate a token with IAntiforgery.GetAndStoreTokens(HttpContext);, it adds a piece of a cryptographic puzzle to a response cookie which needs to be sent down to the client. This cookie must be present in the subsequent incoming request to the protected endpoint together with either the form field or header. I.e. two pieces of cryptographic data must be present in order for the antiforgery verification to pass. One piece comes in as a cookie and the other comes in as either a form field or a header value. So make sure to submit the cookies together with the previously obtained token. Typically, this is automatically handled by the HTML form submission of a web browser. You'd only need to worry about sending the cookie data if constructing the form submission request manually. The name of the cookie can be customized if needed like so:

bld.Services.AddAntiforgery(o => o.Cookie.Name = "Antiforgery")

INFO

See this gist for an example of antiforgery token usage.


© FastEndpoints 2024