Home JWT based Auth for .Net 6 Minimal Apis
Post

JWT based Auth for .Net 6 Minimal Apis

In this article we shall implement a minimal api with .Net 6, using Json Web Token based Authentication.

Json Web Token

But what is a Json Web Token (JWT), after all? There’s an excellent, in detail explanation of JWTs, here.

In simple terms, a JWT is a string, containing variable information in such a manner, that we can validate that it has not been altered, after it was generated.

It is signed with a key and a cryptographic algorithm, usually exlusively available to the issuing server.

Considering the needs of a RESTful API, this can be very useful to achieve some kind of state in a stateless environment, such as one promoted by a REST service.

Assume the following scenario:

  • A client sends an authentication request to a server, providing a user’s Username and Password.
  • The server does some complex calculation, and generates a JWT, which can’t be altered (If someone tampers with it, it will become invalid), and returns that token to the client. The token contains the authenticated user’s ID. His unique username for example.
  • The client can send that token as part of its’ request to an endpoint of a REST api, in all subsequent requests.
  • The server can easily verify the validity of the received JWT, and grant relevant authorization.

Since only the server has access to the signing key, no one else can forge a JWT, that can be validated by the server. So the server can be certain that it is the original issuer of the JWT, and so it can be certain it has previously verified the identity of the user making each request, without the need to authenticate the user again.

Can someone steal the JWT and use it to impersonate a user?
Unfortunately, yes. Establishing a secure SSL/TLS connection is vital to ensure security over a network. Vulnerable systems eg. Browsers, OSes, bad practices can also compromise security.

Securing a .Net 6 Minimal API with JWT

I won’t be getting into details about .Net Minimal APIs here, a certain familiarity is assumed. I will, however, go on about securing one, with a JWT based authentication/authorization.

Microsoft provides general guidelines on securing a minimal api.

I like to divide the task in certain required stages.

  • Add required NuGet Packages.
  • Setting up the authentication service.
  • Setting up the authentication endpoint.
  • Setting up the authorization service.
  • Design the API endpoints.

Required NuGet packages

Microsoft provides various methods to setup auth. Your api can even support multiple methods at the same time. In this example, we are implementing JWT based auth.

There’s only one required package, Microsoft.AspNetCore.Authentication.JwtBearer. I’m focusing on .Net 6 for this article. At the time of writing, the latest version for this package, for Net 6, is 6.0.19. So make sure to use that one.

For the CLI, the command is:

1
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 6.0.19

The Authentication Service

Authentication is a term describing the process under which an entity, i.e. a user is identified by a system. A common authentication scenario, matches a user’s credentials, a username - password pair, to a specific ID in a system’s database.

Setting up a JWT based authentication service in .Net 6 is quite easy. Firstly, we define the default authentication scheme.

1
2
3
4
5
6
7
8
9
//Reference to our Service Collection
IServiceCollection Services;

Services.AddAuthentication(options => {
	//Sets the default authentication scheme to Jwt
	options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
	options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
	options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})

The above snippet adds the authentication service to our services collection. The options, define the JWT based authentication as the default authentication scheme. More about the options can be found here.

The above snippet, only defines which type of authentication scheme should be the default. It doesn’t actually define the scheme itself. In order to add our JWT based scheme, we should actually use the following snippet.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
...

//Reference to our Service Collection
IServiceCollection Services;

//A key that the host -issuer- uses to generate JWTs
private static SymmetricSecurityKey Key = 
    new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SomeLargeKeyString));

//The JWT issuer, probably the host domain name.
//Normally retrieved from a configuration file.
private const string Issuer = "TestApi";

//Setup the authentication service
Services.AddAuthentication(options => {
	//Sets the default authentication scheme to Jwt
	options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
	options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
	options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options => {
	options.TokenValidationParameters = new TokenValidationParameters {
        IssuerSigningKey = Key, //use the host's key
        ValidateIssuerSigningKey = true, //make sure to validate key        
        
        ValidIssuer = Issuer, //this should be the issuer of the jwt
        ValidateIssuer = true, //make sure to validate issuer       
        
        //no need to validate target audience for this example
        ValidateAudience = false, 

        //JWT should only be valid for a certain amount of time
        RequireExpirationTime = true, 
        ValidateLifetime = true,
        ClockSkew = TimeSpan.Zero
    };
});

...

The AddJwtBearer method, actually defines the authorization scheme we want to use. We only support a single type of authorization scheme for our example, which is perfectly adequate to secure an API.

As we shall see later on, a JWT can contain some standard, as well as some custom, key - value pairs, called claims. Some of the default, ‘registered’ claim names, are:

  • iss : The name of the issuer, in our example “TestApi”
  • sub : The subject of the JWT, usually the authenticated user.
  • aud : The target audience. We disable this in our example.
  • exp : The Token’s Expiration time

More, in detail explanation of the registered JWT claims, can be found here.

TokenValidationParameters defines which of those claims should be checked for JWT validity, during the authorization stage, when there’s a request to access a resource. You can check the TokenValidationParameters class to review all options.

Example

  • The client sends a request to the server to generate a JWT by providing the user’s credentials: a username and a password.
  • The server confirms the user’s identity, and returns the appropriate JWT, signed with the server’s key, containing some registered claims, such as exp, and some custom claims.
  • The client requests a resource from the server, which requires authentication + authorization. AddJwtBearer defines the schema to use to check if the provided by the client JWT is valid, which is required to authenticate the client. The authorization is done in a separate step.

The Authentication Endpoint

In order for the client to receive a valid JWT, there should be an api endpoint that receives the user’s credentials, accepts them as valid or rejects them as invalid, and generates a JWT, which the client will use, to pass the authentication state on the rest of the locked api endpoints.

Said endpoint is provided below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
...

//Used to generate JWTs
private static JwtSecurityTokenHandler JwtTokenGenerator =
    newJwtSecurityTokenHandler();

//The JWT issuer, probably the host domain name.
//Normally retrieved from a configuration file.
private const string Issuer = "TestApi";

//The key that the issuer uses to generate JWTs
private static SymmetricSecurityKey Key = 
    new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SomeLargeKeyString));

//The user class can hold various required user data,
//Such as Username, password, etc...
//We only use a username in the current minimal example
public class User {
	public string Username { get; set; }
}

//jwt generation endpoint
app.MapPost("/auth", [AllowAnonymous] (User user) => {
	//host defined criteria that fail authentication
	//could be username-password authentication failure
	if(user.Username == "AuthFail")
		return Results.Unauthorized();

	//Setting up parameters for a new valid jwt
	var tokenDescriptor = new SecurityTokenDescriptor {
        
        Subject = new ClaimsIdentity(new[]{
            //Registered claim: "sub"
            new Claim(JwtRegisteredClaimNames.Sub, user.Username),

            //Custom claim 'AccessLevel'
            //Grant special access to the user with username "SpecialUser"
            new Claim("AccessLevel", user.Username ==
                "SpecialUser" ? "Special" : "Common")
        }),

        //set the 'exp' registered claim
        Expires = DateTime.UtcNow.AddMinutes(5),

        //the issuer of this JWT: "TestApi"
        Issuer = Issuer,

        //The key and algorithm to use to sign this JWT
        SigningCredentials = new SigningCredentials
        	(Key, SecurityAlgorithms.HmacSha512Signature)
    };

    //Generate jwt
    SecurityToken token = JwtTokenGenerator.CreateToken(tokenDescriptor);
    string jwt = JwtTokenGenerator.WriteToken(token);
    return Results.Ok(jwt);
});

...

Τhe above provided code snippet should be self-explanatory.

Authorization

Authorization is the process that determines whether an authenticated entity has access rights to a specific resource. We have set up the authentication process so far.

Setting up authorization is a two step process. First, we have to setup the Authorization Service and its authorization policies, and then choose which policy each endpoint should follow.

Setting up the Authorization service is quite straightforward.

1
2
3
4
5
6
7
8
9
//Reference to our Service Collection
IServiceCollection Services;

//Setup the authorization Service
Services.AddAuthorization(options => {
    //add a policy
    options.AddPolicy("SpecialAccess", policy =>
        policy.RequireClaim("AccessLevel", "Special"));
});

The AuthorizationOptions, options parameter is used to setup our policies. Multiple policies are acceptable.

The AddPolicy method requires the policy name as its first parameter. This name will be used by each endpoint to determine which authorization policy applies to it.

The second expression, defines that a claim is required by the authenticated JWT. In our test scenario, that means, the “AccessLevel” claim, and its required value is “Special”.

Different policies can be set up in a similar manner, to determine access rights to the api’s resources.

Choosing which policy each endpoint should fall under is also quite straightforward. Consider the following example case.

1
2
3
4
//restricted access to anyone who meets the "SpecialAccess" policy
app.MapPost("/restricted", (HttpContext httpContext) => {
	return "Restricted Content!";
}).RequireAuthorization("SpecialAccess");

The above endpoint, requires the “SpecialAccess” policy we defined earlier.

Please note that authorization always takes place after the authentication phase.

Example

  • The client provide’s the user’s credentials to the server
  • The server returns a JWT, which determines (among other things) whether the user has “AccessLevel” : “Special”, which in this case is a custom claim.
  • The client tries to access the resource under the “/restricted” endpoint.
  • The server first determines whether the JWT is valid, and thus the user has been authenticated.
  • After that, the server checks if the endpoint has any authorization policies registered. In this case, the “SpecialAccess” policy is registered for the “/restricted” endpoint.
  • The server checks if the JWT meets the required criteria, in our case if the “AccessLevel” claim has the required “Special” value. If it does, access is allowed to the api’s requested resource.

Conclusion

Setting up an auth scheme for a minimal api is relatively straightforward in .Net 6.

The complete code sample for this minimal example can be found here.

This post is licensed under CC BY 4.0 by the author.