Skip to content

Commit

Permalink
feat: Add exploratory tests on json web tokens aka jwt
Browse files Browse the repository at this point in the history
  • Loading branch information
Tom Brereton authored and Tom Brereton committed May 21, 2024
1 parent dba2f90 commit e1ce9c4
Show file tree
Hide file tree
Showing 2 changed files with 173 additions and 7 deletions.
23 changes: 16 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,42 @@
# Introduction
To showcase my preferred patterns and practices, I have created a simple appointment scheduling api that allows
users to create, edit, and delete appointments to a calendar. Users can also create an account and a calendar.
An appointment is an object which is added to a calendar, and that calendar is associated with a user account.

To showcase my preferred patterns and practices, I have created a simple appointment scheduling api that allows
users to create, edit, and delete appointments to a calendar.

Users create an account and a calendar; Appointments are then added to the calendar.
Appointments can be updated and deleted.

## To Do

- REPR
- Validation
- Logging
- Error Handling
- CQRS
- Clean Architecture
- Clean Architecture

## Technologies

- Testcontainers
- xUnit
- EF Core for writing to the datastore
- Dapper for reading from the datastore
- FluentValidation
- FluentValidation
- Serilog

## Patterns

- Clean Architecture
- CQRS
- REPR (Request Endpoint Response)
- Repository Pattern
- Feature Folders

## Prerequisites

- .NET 8 SDK
- The LATEST Docker for [Mac](https://docs.docker.com/desktop/install/mac-install/)/[Windows](https://docs.docker.com/desktop/install/windows-install/)
- The LATEST Docker
for [Mac](https://docs.docker.com/desktop/install/mac-install/)/[Windows](https://docs.docker.com/desktop/install/windows-install/)

On Mac enable Rosetta in the beta features as shown in the image below:

Expand All @@ -46,8 +54,9 @@ calendar. We will ignore authentication in this exercise.
- Run `dotnet test` from the root of the project

# Requirements

The requirements are written in the Given When Then format. This is a common format used
for acceptance criteria & tests. The requirements are written in a way that is not specific to any
for acceptance criteria & tests. The requirements are written in a way that is not specific to any
particular technology. This allows us to write the tests first and then implement the
functionality to make the tests pass.

Expand Down
157 changes: 157 additions & 0 deletions tests/Infrastructure.IntegrationTests/Authentication/TokenShould.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text.RegularExpressions;
using FluentAssertions;
using Microsoft.IdentityModel.Tokens;

namespace Appointer.Infrastructure.IntegrationTests.Authentication;

public class TokenShould
{
private const string Issuer = "TestIssuer";
private const string Audience = "TestAudience";
private const string Secret = "super secret key that should be stored in a secure place and not in code";
private const string JwtPattern = @"^[A-Za-z0-9-_]+?\.[A-Za-z0-9-_]+?\.[A-Za-z0-9-_]+$";


[Fact]
public void BeCreated()
{
// arrange
var regex = new Regex(JwtPattern);

// act
var token = CreateToken();

// assert
token.Should().NotBeEmpty();
regex.IsMatch(token).Should().BeTrue();
}

[Fact]
public void BeValidated()
{
// arrange
var token = CreateToken();
var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(Secret));
var validationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
ValidateIssuer = true,
ValidIssuer = Issuer,
ValidateAudience = true,
ValidAudience = Audience,
ValidateLifetime = true
};
var handler = new JwtSecurityTokenHandler();

// act
var principal = handler.ValidateToken(token, validationParameters, out _);

// assert
var claimsIdentity = new ClaimsIdentity(principal.Claims, "Bearer");
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
claimsPrincipal.Identity?.IsAuthenticated.Should().BeTrue();
claimsPrincipal.Identity?.AuthenticationType.Should().Be("Bearer");
claimsPrincipal.Identity?.Name.Should().Be("Test User");
claimsPrincipal.Claims.First(c => c.Type == ClaimTypes.Role).Value.Should().Be("Admin");
claimsPrincipal.Claims.First(c => c.Type == ClaimTypes.Name).Value.Should().Be("Test User");
}

[Fact]
public void NotBeValidBySigningKey()
{
// arrange
var wrongSecret = "wrong secret key";
var token = CreateToken();
var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(wrongSecret));
var validationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
ValidateIssuer = true,
ValidIssuer = Issuer,
ValidateAudience = true,
ValidAudience = Audience,
};
var handler = new JwtSecurityTokenHandler();

// act
Action act = () => handler.ValidateToken(token, validationParameters, out _);

// assert
act.Should().Throw<SecurityTokenSignatureKeyNotFoundException>();
}

[Fact]
public void NotBeValidByAudience()
{
// arrange
var token = CreateToken();
var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(Secret));
var validationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
ValidateIssuer = true,
ValidIssuer = Issuer,
ValidateAudience = true,
ValidAudience = "wrong audience",
};
var handler = new JwtSecurityTokenHandler();

// act
Action act = () => handler.ValidateToken(token, validationParameters, out _);

// assert
act.Should().Throw<SecurityTokenInvalidAudienceException>();
}

[Fact]
public void NotBeValidByIssuer()
{
// arrange
var token = CreateToken();
var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(Secret));
var validationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
ValidateIssuer = true,
ValidIssuer = "wrong issuer",
ValidateAudience = true,
ValidAudience = Audience,
};
var handler = new JwtSecurityTokenHandler();

// act
Action act = () => handler.ValidateToken(token, validationParameters, out _);

// assert
act.Should().Throw<SecurityTokenInvalidIssuerException>();
}

private string CreateToken()
{
var claims = new List<Claim>
{
new(ClaimTypes.Name, "Test User"),
new(ClaimTypes.Role, "Admin"),
};

// this symmetric key is used to both sign and verify the token
// a public and private key pair can be used instead for asymmetric encryption
var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(Secret));
var cred = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);
var token = new JwtSecurityToken(
issuer: Issuer,
audience: Audience,
claims: claims,
expires: DateTime.UtcNow.AddDays(1),
signingCredentials: cred
);
var jwt = new JwtSecurityTokenHandler().WriteToken(token);
return jwt;
}
}

0 comments on commit e1ce9c4

Please sign in to comment.