Securing a REST API using JWT and Umbraco front-end members

I have an Umbraco project with a REST API that delivers data from the Umbraco content database. The content is protected in the sense that users need a valid subscription to access it. To do this, I have used the built-in membership provider in Umbraco and built a surface with views and controller where the user can log in and manage his/her account.

Now, this content is also going to be available through a mobile app. For mobile apps, the cookie/session based authentication is not a good option, because it will require the user to log in frequently when the session/cookie expires. This is not a good user experience on a mobile app. So I decided to try and implement a stateless authentication based on JWT, JSON Web Tokens. But this turned out to be quite a journey with a steep learning curve. So I thought I’d share some of my experiences and code:-)

Creating a REST API

To create your own REST API in Umbraco, simply create a Controller in your project and then change the base class to UmbracoApiController.

public class MyApiController : UmbracoApiController
{
    [HttpGet]
    public string GetSomeContent()
    {
        IContentService cs = ApplicationContext.Services.ContentService;
        // Pull out some content from Umbraco here
        return someContent;
    }
}

Now, your api methods can be reached through this url scheme: /Umbraco/api/< api name >/< method >, so in this case /Umbraco/api/MyApi/GetSomeContent.

Securing your API

There is of course the option of going the OAuth route in your Umbraco project, in which case a lot of the authentication and security code will be quite straightforward to implement in a standardised way. But what if you - like me - are stuck with the standard Umbraco member API and protection mechanisms? Then, as we will see, things get a little more complicated.

Testing tool

The first thing you should get in place is a tool for testing your REST API. I have chosen Swagger, which has a nice YAML editor and an easy UI for sending API requests to your server. And, if you properly describe your API in YAML, you’ll also end up having a full documentation of the API so that other developers can easily understand it and use it:-) We’ll look at some YAML later on.

JWT - JSON Web Token

You should read up a little bit on the basics of this technology: About JWT at the AuthO Website To put it short, a JWT is an encrypted string consisting of three parts: header, payload and signature. And the payload is the interesting part, it is where we specify our claims and metadata. The header specifies the format and encryption method of the package, and the signature is an encrypted string that verifies the package is genuine and issued by our server. The encryption uses a secret which must be stored safely in our server, no one else must know about this. Then everything is encoded as Base64 strings and the three parts are separated by dots.

Refresh Tokens and Access Tokens

I’ll jump right to the conclusion of why we’re using two types of tokens: To avoid passing user information and look up in the member database for every protected API call. The procedure goes like this:

  • The user authenticates himself with username and password.
  • The server then creates a Refresh Token which contains the username, and store this in the member database.
  • This token is now returned to the client app. The client app stores this token safely, and typically on a mobile app, it never expires, because you don’t want the user having to log in more than once.
  • The client app uses the Refresh Token to request an Access Token. This token does not contain the username, but rather an expiration time.
  • The Access Token is used to access protected methods in the API. The server decodes the token, checks the signature and expiry time and proceeds to return data in the response if everything is ok.
  • If the signature is wrong, it returns a 401 Unauthorized with a message “Invalid Access Token”.
  • If it has expired, it also returns a 401, but with a message “Access Token Expired”. The client will then need to use the Refresh Token to request a new Access Token to be able to use the API. The server checks the Refresh Token - which contains the username - against the token stored in the member database, and if its ok, returns a new Access Token.

Integration in Umbraco

The first thing we need is a good package for handling the JWT format itself. There should be no need to reinvent the wheel here, and I found a good NuGet which does all the encoding and decoding work, it’s simply called JWT. Having installed that, let’s look at some code (paste this into your Controller class):

    private const string jwtSecret1 = "< Your 40 char secret >";
    private const string jwtSecret2 = "< Another 40 char secret >";

    private class JwtRefreshTokenPayload
    {
        public string sub { get; set; }
        public string username { get; set; }
    }

    private class JwtAccessTokenPayload
    {
        public string sub { get; set; }
        public DateTime expires { get; set; }
    }

    [HttpPost]
    public HttpResponseMessage Authenticate(string username, string password)
    {
        JwtRefreshTokenPayload payload = new JwtRefreshTokenPayload() {
            sub = "MyRefreshToken",
            username = username
        };

        if (Members.Login(username, password))
        {
            var ms = Services.MemberService;
            var member = ms.GetByUsername(username);
            string refreshToken = JsonWebToken.Encode(payload, jwtSecret1, JwtHashAlgorithm.HS256);
            // Save refresh token on member. It must be provided by the client to get access tokens for the API.
            member.Properties["refreshToken"].Value = refreshToken;
            ms.Save(member);

            return Request.CreateResponse<string>(HttpStatusCode.OK, refreshToken);
        }

        return Request.CreateResponse<string>(HttpStatusCode.Unauthorized, "Invalid Username and Password");
    }

    [HttpGet]
    public HttpResponseMessage GetAccessToken()
    {
        try
        {
            string refreshToken = Request.Headers.GetValues("refreshToken").FirstOrDefault();
            DefaultJsonSerializer jsonSerializer = new DefaultJsonSerializer();
            JwtRefreshTokenPayload payloadR = jsonSerializer.Deserialize<JwtRefreshTokenPayload>(JsonWebToken.Decode(refreshToken, jwtSecret1));

            if (payloadR.sub == "MyRefreshToken")
            {
                var ms = Services.MemberService;
                var member = ms.GetByUsername(payloadR.username);

                if (member != null)
                {
                    string storedRefreshToken = (string)member.Properties["refreshToken"].Value;
                    if (refreshToken == storedRefreshToken)
                    {
                        JwtAccessTokenPayload payloadA = new JwtAccessTokenPayload()
                        {
                            sub = "MyAccessToken",
                            expires = DateTime.Now.AddHours(20)
                        };

                        string accessToken = JsonWebToken.Encode(payloadA, jwtSecret2, JwtHashAlgorithm.HS256);
                        return Request.CreateResponse<string>(HttpStatusCode.OK, accessToken);
                    }
                }
            }
        }
        catch (Exception ex)
        {
        }

        return Request.CreateResponse<string>(HttpStatusCode.Unauthorized, "Invalid Refresh Token");
    }

    private string VerifyAccessToken()
    {
        try
        {
            string accessToken = Request.Headers.GetValues("accessToken").FirstOrDefault();
            DefaultJsonSerializer jsonSerializer = new DefaultJsonSerializer();
            string jsonToken = JsonWebToken.Decode(accessToken, jwtSecret2);
            JwtAccessTokenPayload payload = jsonSerializer.Deserialize<JwtAccessTokenPayload>(jsonToken);
            if (payload.sub != "MyAccessToken")
                return "Invalid Access Token";
            if (payload.expires > DateTime.Now)
                return "OK";
            return "Access Token Expired";
        }
        catch (Exception ex)
        {
            return "Invalid Access Token";
        }
    }

    [HttpGet]
    public HttpResponseMessage GetSomeContent()
    {
        string verifyResult = VerifyAccessToken();
        if (verifyResult != "OK")
            return Request.CreateResponse<string>(HttpStatusCode.Unauthorized, verifyResult);
        IContentService cs = ApplicationContext.Services.ContentService;
        // Pull out some content from Umbraco here
        return Request.CreateResponse<string>(HttpStatusCode.OK, someContent);
    }

The /Authenticate POST is sent with Username and Password in the query string to get a Refresh Token for the user, provided the user is a member in the Umbraco database. The Refresh Token is stored with the user in the Umbraco database.

The /GetAccessToken GET is sent with the Refresh Token in a custom header called “refreshToken”. If the Refresh Token is verified, it returns an Access Token valid for 20 hours.

The /GetSomeContent GET is sent with the Access Token in a custom header called “accessToken”. If the Access Token is verified, the method returns its data in the response.

To test this in Swagger, you can use the following YAML:

swagger: '2.0'

info:
  version: "0.0.1"
  title: My Umbraco API
  
host: localhost:XXXXX

schemes:
  - http
  
basePath: /Umbraco/api/MyApi

produces:
  - application/json
  
paths:
  /Authenticate:
    post:
      summary: My API Authentication
      description: Authenticate member for API access
      parameters:
        - name: username
          in: query
          description: Username
          required: true
          type: string
        - name: password
          in: query
          description: Password
          required: true
          type: string
      responses:
        200:
          description: Refresh token (JWT)
          schema:
            type: string
        401:
          description: Unauthorized error
          schema:
            type: string
        default:
          description: Unexpected error
          schema:
            $ref: '#/definitions/Error'
  /GetAccessToken:
    get:
      summary: My API Access Token
      description: Get an access token for My Umbraco API endpoints
      parameters:
        - name: refreshToken
          in: header
          description: Refresh token
          required: true
          type: string
      responses:
        200:
          description: Access token (JWT)
          schema:
            type: string
        401:
          description: Unauthorized error
          schema:
            type: string
        default:
          description: Unexpected error
          schema:
            $ref: '#/definitions/Error'
  /GetSomeContent:
    get:
      summary: Test Method
      description: Test My Umbraco API
      parameters:
        - name: accessToken
          in: header
          description: Access token
          required: true
          type: string
      responses:
        200:
          description: Some content
          schema:
            type: string
definitions:
  Error:
    type: object
    properties:
      code:
        type: integer
        format: int32
      message:
        type: string

AND, there is one more thing before you’ll be able to test: You must enable CORS (Cross-Origin Resource Sharing) for your API, so that Swagger (or your mobile app!) can access it via its own server. In my scenario, I needed to do two things: enable CORS in web.config:

  <system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="Access-Control-Allow-Origin" value="*" />
        <add name="Access-Control-Allow-Headers" value="refreshToken,accessToken" />
      </customHeaders>
    </httpProtocol>
  </system.webServer>

Then enable OPTIONS preflight request for all your API methods, so that they accept the requests through CORS. I chose to do this generally in Global.asax:

public class Global : UmbracoApplication
{
    //... other global.asax methods, f.ex. override OnApplicationStartup()

    private void Application_BeginRequest(object sender, EventArgs e)
    {
        try
        {
            if (Request.Headers.AllKeys.Contains("Origin") && Request.HttpMethod == "OPTIONS")
            {
                Response.Flush();
            }
        }
        catch { }
    }
}

Testing

Try /Authenticate with a valid Username and Password, and copy the JWT in the response. Paste it into the “refreshToken” parameter of /GetAccessToken and invoke. Copy the new JWT from this method into the “accessToken” parameter of the /GetSomeContent method and invoke. You should get someContent in the response. Try also tampering with the JWTs and see that you get 401s.

Final notice

Needless to say, this whole scheme should be run over https. Otherwise, someone can easily sniff out usernames, passwords and tokens from your requests.

And feel free to comment on my technique, this is my first time for almost everything covered here. If I’m lucky, someone with greater experience than me can point out any flaws (which I’m quite sure there must be)…


This is a companion discussion topic for the original entry at https://our.umbraco.com/forum/84623-securing-a-rest-api-using-jwt-and-umbraco-front-end-members