DEV Community

Cover image for Using JWT tokens to access an UmbracoApiController
Sebastiaan Janssen
Sebastiaan Janssen

Posted on

Using JWT tokens to access an UmbracoApiController

So far, JWT has been eluding me, I haven't had the time to look into them, I just knew they existed and worked by sending a header.

Today, I was following a question from Navorski about using JWT authentication on an UmbracoApiController, I enjoyed the responses from Dennis who had clearly done this before.

It seemed like this was easy enough to follow, so I decided to try it out for myself and see how far I could get. As it turns out, I was fearing them more than I should have done! Let's have a look.

First we need to tell our Web App (Umbraco) that JWT is one of the way to authenticate. For that, we need to install a NuGet package first to help us out with that:

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
Enter fullscreen mode Exit fullscreen mode

The next thing to do is to set up some configuration items, which can be added after the "Umbraco": section.

  "Jwt": {
    "Key": "ihuEAzo7BxW09LTTNKCz",
    "Issuer": "https://localhost:44336/",
    "Audience": "https://localhost:44336/"
  }
Enter fullscreen mode Exit fullscreen mode

In the appSettings.json it looks like this:

Screenshot of the config addition to the appSettings.json file

The Key I generated here is a secret, please don't copy it but create a unique key for yourself.

Once that is installed, the linked tutorial tells us to add some things to Startup.cs. Personally, I like adding things using an IComposer to make my code more portable in case I want to reuse it or put it in a package, for example.

So I end up with the following code, that does the same thing as it would have done if I had added it to Startup.cs.

using System.Text;
using Microsoft.IdentityModel.Tokens;
using Umbraco.Cms.Core.Composing;

namespace JwtTest;

public class JwtComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        builder.Services.AddAuthentication().AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ValidIssuer = builder.Config["Jwt:Issuer"],
                ValidAudience = builder.Config["Jwt:Audience"],
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Config["Jwt:Key"]))
            };
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Whenever we request JWT token authentication, this code will run.

Next up, we can make an UmbracoApiController with an example endpoint:

using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Web.Common.Controllers;

namespace JwtTest;

public class TestApiController : UmbracoApiController
{
    public ActionResult<string> GetAuthenticatedMessage()
    {
        return "You need to be authenticated to see this message.";
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, when we run the site and go to https://localhost:44336/umbraco/api/TestApi/GetAuthenticatedMessage, we see the message You need to be authenticated to see this message.. Great! But the message is a lie, we didn't request or provide any authentication. Let's add some.

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Web.Common.Controllers;

namespace v12final;

public class TestApiController : UmbracoApiController
{
    [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
    public ActionResult<string> GetAuthenticatedMessage()
    {
        return "You need to be authenticated to see this message.";
    }
}
Enter fullscreen mode Exit fullscreen mode

The Authorize attribute now requires us to provide a JWT bearer token in order for the request to succeed. When we now try to access the same endpoint, we get a nice 401 Unauthorized error.

We're getting there! We need a way to obtain a JWT authorization token now. Normally you'd do that by verifying a user is authenticated in the system, I'll leave that part as a exercise to the reader and focus on generating the token we need. So in the next snippet of code, we allow anyone who provides us with any username and password to get a token.

⚠️ Don't do this in real life, make sure you only hand out tokens to trusted parties, verify you can trust them.

The following code was added to the same TestApiController we had above:

private readonly IConfiguration _config;

public TestApiController(IConfiguration config)
{
    _config = config;
}

[HttpPost]
public ActionResult<string> GenerateJwtToken([FromBody] UserModel user)
{
    // TODO: Do something to verify username/password

    var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
    var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
    var claims = new[]
    {
        new Claim(ClaimTypes.NameIdentifier,user.Username),
        new Claim(ClaimTypes.Role,"apiuser")
    };
    var token = new JwtSecurityToken(_config["Jwt:Issuer"],
        _config["Jwt:Audience"],
        claims,
        expires: DateTime.Now.AddMinutes(15),
        signingCredentials: credentials);


    return new JwtSecurityTokenHandler().WriteToken(token);
}

public class UserModel
{
    public string Username { get; set; }
    public string Password { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

We've also added the following usings:

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
Enter fullscreen mode Exit fullscreen mode

We're injecting an IConfiguration because we need some values from appSettings and other than that it's mostly copied code from the tutorial. As said, there's no verification of any credentials, we just generate a token and spit it out.

Now let's try it out in Postman by sending a POST to the new endpoint at https://localhost:44336/umbraco/api/TestApi/GenerateJwtToken, adding a username and password.

Example of the request in Postman, which returns the JWT token we need

Success! We get a token that we can hopefully use in the GetAuthenticatedMessage call.

Going back to Postman, we can add a header to the GetAuthenticatedMessage request named Authorization and as the value we add Bearer, then a space and then the token we received from the GenerateJwtToken endpoint.

Example of the request in Postman, which adds the Bearer token and shows the expected success response

And as you can see in the response body, we do indeed get our message back, this time it's not lying to us: You need to be authenticated to see this message.. Correct!

And there you have it, JWT tokens demystified (for me at least 😅).

Top comments (3)

Collapse
 
d_inventor profile image
Dennis

To add to this:

What really helped me grasp the concept of authentication in .NET 6 is the understanding that authentication is just a dictionary of strategies.

See it like this: there are various ways that one might want to authenticate a request, with a cookie, with google or with JWT for example. Your application needs to tell the difference between these, so what you do, is you give each authentication method a name: the 'authentication scheme'. What's cool here is that it doesn't matter what you name it. For example, this is the part of your code that tells your application that there exists an authentication method with JWT:

builder.Services.AddAuthentication().AddJwtBearer(options =>
{
    // ... configuring options here
});
Enter fullscreen mode Exit fullscreen mode

Because you don't specify a name for this JwtBearer authentication method, it becomes equivalent to this:

builder.Services.AddAuthentication().AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
    // ... configuring options here
});
Enter fullscreen mode Exit fullscreen mode

However, you could also specify your authentication like this:

builder.Services.AddAuthentication()
    .AddJwtBearer("PinkFluffyUnicorns", options =>
    {
        // ... configuring options here
    })
    .AddJwtBearer("UmbracoIsAwesome", options =>
    {
        // ... Configure a second jwt bearer authentication method here
    });
Enter fullscreen mode Exit fullscreen mode

So basically what you're saying here is: "There exists an authentication method with the name PinkFluffyUnicorns and a method with the name UmbracoIsAwesome". Now that these methods exist, all you need to do is use them.

public class TestApiController : UmbracoApiController
{
    [Authorize(AuthenticationSchemes = "PinkFluffyUnicorns")]
    public ActionResult<string> GetAuthenticatedMessage()
    {
        return "This message is secured with the Pink fluffy unicorns jwt token";
    }

    [Authorize(AuthenticationSchemes = "UmbracoIsAwesome")]
    public ActionResult<string> GetAuthenticatedMessage()
    {
        return "This message is secured with the Umbraco is awesome jwt token";
    }
}
Enter fullscreen mode Exit fullscreen mode

For developers who come from .NET Framework, it might be easy to assume that there is some magic involved here, but once you realize that it's just a dictionary, you start to see how easy and how powerful this authentication framework is.

That's also why it's so incredibly easy to add additional authentication methods to an Umbraco 10+ site. You just add an extra method into the dictionary with a name of your choice.

Collapse
 
cultiv profile image
Sebastiaan Janssen

Most excellent explanation, thanks @d_inventor!

Collapse
 
navorski profile image
Bik Lander • Edited

Correct configuration for swagger with the JWT Bearer token

services.AddSwaggerGen(sw =>
            {
                sw.EnableAnnotations();

                sw.AddSecurityDefinition("Bearer", new Microsoft.OpenApi.Models.OpenApiSecurityScheme
                {
                    Description = "Enter the Bearer Authorization string as following: `Bearer Twist-Iot-Token`",
                    Name = "Authorization",
                    In = ParameterLocation.Header,
                    Type = SecuritySchemeType.Http,
                    Scheme = "bearer",
                    BearerFormat = "JWT"
                });

                sw.AddSecurityRequirement(new OpenApiSecurityRequirement
                {
                    {
                        new OpenApiSecurityScheme
                        {
                            Reference = new OpenApiReference
                            {
                                Type = ReferenceType.SecurityScheme,
                                Id = "Bearer"
                            }
                        },
                        Array.Empty<string>()
                    }
                });

                /* Swaggerdocs */
            });
Enter fullscreen mode Exit fullscreen mode