DEV Community

Graham Cox
Graham Cox

Posted on

A Hypermedia powered Authentication flow

Hypermedia and REST are big things right now. By choosing to write your APIs in a way that follows the HTTP rules correctly you open a lot of doors to all sorts of behaviour, and by embracing hypermedia you allow the server to dictate to the client how things should behave, which in turn means that the server can change functionality and the client will have little or no changes to make to take advantage of it.

Here I am going to demonstrate how a rich Hypermedia API could be built using SIREN to allow for an authentication flow.

Note that we are not building an app here. There is no server nor client-side code for this, only a discussion of how the API could work.

What is SIREN

SIREN is a standard hypermedia format that allows us to return the data for our resource as well as metadata describing what we can do with it. This metadata consists of:

  • Links to other resources. These typically are used for navigation or for links to non-hypermedia resources.
  • Entities representing other hypermedia resources. These can either be just a link or else an actual representation of the resource.
  • Actions that can be performed on the current resource.

These "actions" are the important detail here. If we have a client that is able to react correctly to SIREN responses, we will see how the server can indicate what actions need to be performed, what fields need to be captured and the client can just react accordingly.

API Requirements

Our requirements here are:

  • The user enters their username on the first screen.
  • If the username is unknown then they are presented with a registration form to capture the required details.
  • If the username is known then they are presented with an authentication form to capture the required details.
  • If they have MFA enabled then the required details are different than if it's disabled. When MFA is enabled we need both the password and the authentication code from their app.

Username Screen

When the client wants to allow authentication, it will first make a GET request to /authentication. (This will have been identified by a link from the API home page.) This response could look something like this:

{
  "class": ["authentication"],
  "links": [
    {"rel": ["self"], "href", "/authentication"}
  },
  "actions": [
    {
      "name": "authenticate",
      "title": "Log In / Register",
      "method": "POST",
      "href": "/authentication",
      "type": "application/x-www-form-urlencoded",
      "fields": [
        {
          "name": "username",
          "title": "Username",
          "type": "text"
        }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

This looks complicated, so let's work through it.

What we have here is a link indicating what the "self" link is for this resource - this is literally the link to the resource itself.

We also define a single action with the name "authenticate". This is our submission for this resource, and indicates that we are performing authentication. This action has a title that can be used in the UI if desired, a single field for the username, and indicates that submissions should be a POST to /authentication.

Already this gives us enough information to build the first screen needed to start authentication.

Registration

When the above form is submitted, the server will determine if the username is known or not. If it's not known then a new user registration is needed. This could be indicated with the following API response:

{
  "class": ["authentication"],
  "links": [
    {"rel": ["self"], "href", "/authentication"}
  },
  "actions": [
    {
      "name": "register",
      "title": "Register",
      "method": "POST",
      "href": "/register",
      "type": "application/x-www-form-urlencoded",
      "fields": [
        {
          "name": "username",
          "type": "hidden",
          "value": "newUser"
        },
        {
          "name": "email",
          "title": "Email Address",
          "type": "email"
        },
        {
          "name": "password",
          "title": "Password",
          "type": "password"
        }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Our action looks more complicated here, but actually it's only an extension of what we saw before. We've got a different href to send the fields to, and we've now got three fields. The first is a hidden field containing the username that was entered on the first screen. The other two are the fields that we need to capture to register the user - their email address and desired password.

We can render this using the exact same rules as the first one, and suddenly we'll get a registration form instead, just by following the API response.

Simple Authentication

If the server determined that the username entered was already known then the API response could instead be something like this:

{
  "class": ["authentication"],
  "links": [
    {"rel": ["self"], "href", "/authentication"}
  },
  "actions": [
    {
      "name": "authenticate",
      "title": "Log In",
      "method": "POST",
      "href": "/authenticate",
      "type": "application/x-www-form-urlencoded",
      "fields": [
        {
          "name": "username",
          "type": "hidden",
          "value": "existingUser"
        },
        {
          "name": "password",
          "title": "Password",
          "type": "password"
        }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

This actually looks very similar to the registration response. We have different values for the title and href, and we only need to capture the password in addition to the already known username.

MFA Authentication

What about if the user has multi-factor authentication enabled? This is actually easily handled now - we simply return an additional field in our response:

{
  "class": ["authentication"],
  "links": [
    {"rel": ["self"], "href", "/authentication"}
  },
  "actions": [
    {
      "name": "authenticate",
      "title": "Log In",
      "method": "POST",
      "href": "/authenticate",
      "type": "application/x-www-form-urlencoded",
      "fields": [
        {
          "name": "username",
          "type": "hidden",
          "value": "existingUser"
        },
        {
          "name": "password",
          "title": "Password",
          "type": "password"
        },
        {
          "name": "code",
          "title": "Auth Code",
          "type": "text"
        }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The only difference here is the addition of the code field in our response. This will indicate to the client that they need to capture an additional value when logging in. This will then be passed to the server which will be able to check both the password and the authentication code and act accordingly.

Summary

Here we can see that the only changes we've had to make are in the API response indicating which actions are available, and the client can automatically display the correct forms to implement both a user registration and authentication flow.

This also means that we can start to capture different values for user registration by only changing the API response, and having the client automatically react to it.

This gives a huge amount of flexibility - suddenly we can have clients that build themselves correctly based on what the server instructs them, so changes propagate automatically without needing to update both the server and client in sync with each other.

Of particular note are the APIs that we've not written. We've not had to have any API to look up if a username exists, nor any API to determine if a user is partaking in multi-factor authentication. The API response from submitting the username gives us all of this in one simple package.

This also means that the client isn't making any decisions on how to work. There is no need for the client to determine which form to display, nor which fields to display. It just gets the response from the server and displays what it's told to. Which puts all of the complexity in one single place.

Top comments (0)