Security
Securing Endpoints
Endpoints are secure by default and you'd have to call AllowAnonymous() in the configuration if you'd like to allow unauthenticated users to access a particular endpoint.
JWT Bearer Authentication
Support for easy JWT Bearer Authentication is provided. You simply need to install the FastEndpoints.Security package and register it in the middleware pipeline like so:
dotnet add package FastEndpoints.Security
global using FastEndpoints;
global using FastEndpoints.Security; //add this
var builder = WebApplication.CreateBuilder();
builder.Services.AddFastEndpoints();
builder.Services.AddJWTBearerAuth("TokenSigningKey"); //add this
var app = builder.Build();
app.UseAuthentication(); //add this
app.UseAuthorization();
app.UseFastEndpoints();
app.Run();
Generating JWT Tokens
You can generate a JWT token for sending to the client with an endpoint that signs in users like so:
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(
signingKey: "TokenSigningKey",
expireAt: DateTime.UtcNow.AddDays(1),
priviledges: u =>
{
u.Roles.Add("Manager");
u.Permissions.AddRange(new[] { "ManageUsers", "ManageInventory" });
u.Claims.Add(new("UserName", req.Username));
u["UserID"] = "001"; //indexer based claim setting
});
await SendAsync(new
{
Username = req.Username,
Token = jwtToken
});
}
else
{
ThrowError("The supplied credentials are invalid!");
}
}
}
Cookie Based Authentication
If your client applications have support for cookies, you can use cookies for auth instead of JWT. By default the following enables http-only cookies so you can store user claims in the asp.net cookie without having to worry about the safety of front-end application storage choice.
using FastEndpoints;
using FastEndpoints.Security; //add this
var builder = WebApplication.CreateBuilder();
builder.Services.AddFastEndpoints();
builder.Services.AddCookieAuth(validFor: TimeSpan.FromMinutes(10)); //configure cookie auth
var app = builder.Build();
app.UseAuthentication(); //add this
app.UseAuthorization();
app.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 and add an encrypted cookie to the response.
Endpoint Authorization
Once an authentication provider is registered such as JWT Bearer as shown above, you can restrict access 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:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ManagersOnly", x => x.RequireRole("Manager").RequireClaim("ManagerID"));
})
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:
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 allowed. 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 ANY of the given roles should allow access to a user principal who has it.
Policy() method
You can specify an action to be performed on the AuthorizationPolicyBuilder for specifying any other authorization requirements that aren't satisfied by the above methods.
AllowAnonymous() method
Use this method if you'd like to allow unauthenticated users to access a particular endpoint.
It is also possible to specify which http verbs you'd like to allow anonymous access to if you're endpoint is listening on multiple verbs & routes like so:
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.
Here's an example project using Auth0 with permissions.
Multiple Authentication Schemes
Multiple auth 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.
builder.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://{builder.Configuration["Auth0:Domain"]}/";
options.Audience = builder.Configuration["Auth0:Audience"];
});
public override void Configure()
{
Get("/account/profile");
AuthSchemes(JwtBearerDefaults.AuthenticationScheme);
}
In the above example, we're registering both Cookie and JWT Bearer auth schemes and in the endpoint we're saying only JWT Bearer auth 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 the provided convenience methods such as AddJWTBearerAuth() and AddCookieAuth() 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.AddJWTBearerAuth(...);
bld.Services.AddCookieAuth(...);
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.AddJWTBearerAuth(...);
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:
builder.Services
.AddCookieAuth(validFor: TimeSpan.FromMinutes(60))
.AddJWTBearerAuth("TokenSigningKey")
.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.
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 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.
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.
}
}
Here's an example project showcasing refresh token usage.