Security is paramount in the web world. We have already discussed using AWS Cognito for securing our front end. Now we need to secure the back-end API to ensure that functionality and data are restricted depending on the user.
Often conflated as they come generally come as a pair, first let's distinguish the authentication and authorisation.
Authentication
Authorisation
Presuming we already have our AWS Cognito instance configured, and our front end is set up following this tutorial.
Let’s Get Started
In AWS Cognito, we’re going to create a new App Integration. We will need one of these per integration with the User Pool, this allows us to keep the service secure as we can configure token expirations and the allowed authentication flows.
First, let’s start by installing our dependencies. We will only need to install: Microsoft.AspNetCore.Authentication.JwtBearer
Our React.js front end is using AWS Amplify SDK to interact with AWS Cognito to log in, user management and such, therefore we currently don’t have a purpose for installing Amazon.AspNetCore.Identity.Cognito
.
Let's create a class to which we can cast our appsettings.json
into. This prevents the need for magic strings to live everywhere. This class will contain the necessary fields we need for configuration.
Then we can declare them in appsettings.json
. We have different files depending on our environment - appsettings.{env}.json
- for instance, appsettings.production.json
or appsettings.development.json
. When running the CreateHostBuilder
, the appropriate file will be loaded depending on the DOTNET_ENVIRONMENT
variable inside the CreateDefaultBuilder
.
I am using a Startup.cs
, however, you may have a minimal API with your ServiceCollection
being configured in Program.cs
.
Nonetheless, let's configure AddAuthentication
. We’re going to set the default scheme to be a JWT Bearer.
Then we’re going to configure the JWT Authentication by declaring the necessary fields. We will get the configuration declared in appsettings.json
and cast to our new class. Then we set the audience and authority as well as the validation options.
Then be sure to use both authentication and authorisation within the configuration of the middleware.
Next, we are going to lock down the endpoints. The same as you would with any other auth provider, decorate each endpoint with either Authorize
or AllowAnonymous
depending on which is appropriate. With the Authorize
we can specify particular schemes, policies or roles. As suggested we currently don’t have other roles, and we have set the default scheme to be JWT.
Should the passed JWT be invalid, then the endpoint will return a 401 Unauthorized
response.
Since the JWT is valid, we have now entered the endpoint. We need to identify the user who is requesting. To do so we gain the username that can be found within the claims of the JWT.
By gaining the username assigned to the JWT we can retrieve any appropriate resources related to that user and this endpoint. In BetaBud’s circumstance, perhaps we’re loading their current token count.
From the ControllerBase
class that the controller inherits we can access the ClaimsPrincipal
object User
. This we can pass to our AuthHelper
to retrieve the username.
If the username is empty then we will return a 403 Forbidden
as the user isn’t allowed to perform this action. This logic could become more complex in future.
1// snippet
2"AuthOptions": {
3 "Authority": "https://cognito-idp.{region}.amazonaws.com/{user-pool-id}",
4 "Audience": "{client-id}"
5},
6// more config
1internal class AuthOptions
2{
3 public string Authority { get; set; } = "";
4
5 public string Audience { get; set; } = "";
6}
1public class Startup
2{
3 public Startup(IConfiguration configuration)
4 {
5 Configuration = configuration;
6 }
7
8 public IConfiguration Configuration { get; }
9
10 public void ConfigureServices(IServiceCollection services)
11 {
12 // further config
13
14 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme);
15 }
16
17 public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
18 {}
19}
1public void ConfigureServices(IServiceCollection services)
2{
3 // further config
4
5 var authOptions = Configuration.GetSection(nameof(AuthOptions)).Get<AuthOptions>();
6 services.AddJwtBearer(options =>
7 {
8 options.Authority = authOptions.Authority;
9 options.Audience = authOptions.Audience;
10 options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
11 {
12 ValidIssuer = authOptions.Authority,
13 ValidateIssuerSigningKey = true,
14 ValidateIssuer = true,
15 ValidateLifetime = true,
16 ValidAudience = authOptions.Audience,
17 ValidateAudience = true
18 };
19 });
20}
1public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
2{
3 // further middleware config
4
5 app.UseAuthentication();
6 app.UseAuthorization();
7
8 // further middleware config
9}
1[Route("api/[controller]")]
2public class HelloController : ControllerBase
3{
4 [HttpGet]
5 [Authorize]
6 public async Task<IActionResult> GetAsync()
7 {
8 return Ok();
9 }
10
11 [HttpGet("anon")]
12 [AllowAnonymous]
13 public async Task<IActionResult> GetAnonymousAsync()
14 {
15 return Ok();
16 }
17}
1internal class AuthHelper
2{
3 internal string? GetUsername(ClaimsPrincipal user)
4 {
5 return user.FindFirst("username")?.Value;
6 }
7}
1public async Task<IActionResult> GetAsync()
2{
3 string? username = _authHelper.GetUsername(User);
4
5 if (string.IsNullOrWhiteSpace(username))
6 return Forbid();
7
8 return Ok();
9}