Tags
January 12, 2024
by
Alexander VerbruggenRead more about this author

Security

Security is an important part of any enterprise solution, you want as much control as possible over who can access which data. While security can be applied at any level, it usually applies at the fringes of the application where external systems interact with your solution: the API.

There are two very important yet distinct questions that need to be answered in a security context:

  • authentication: who is the user that wants to do something?
  • authorization: once we know who the user is we need to establish what is he allowed to do

Nabu uses a pluggable system where you can add your own providers to implement custom security solutions but it also comes with some default providers that provide a more plug-and-play experience.

Authentication

Authentication is the act of establishing an identify for the user who wants to interact with your system.

How

There are many ways to do this, in general there are three ways to establish an identity:

  • something you know: a password is a shared secret between the user and the system which the user can use to prove that he is who he claims to be
  • something you have: on registration you might have provided a phone number. we can send a text to that phone with a secret in it. By entering that secret into our application you prove that you have access to the phone and thus are who you say you are.
  • something you are: this is in the realm of biometrics where we can check your face, fingerprints, iris scans,... whatever is unique about you as an individual that can not easily be copied by someone else. It is often the "easiest" form of security but does come with a substantial downside: you can't change your fingerprints if they are ever compromised.

In multi-factor authentication you combine at least 2 of these, for example after you enter a password you might be prompted to enter a code sent to your phone. This includes knowing the password and having the phone.

Who

You can implement authentication directly in your own solution, Nabu has the necessary tooling to provide both single and multi factor authentication. However, for companies it is often more interesting to have a centralized authentication process rather than having each application do their own thing.

This brings us to single sign on (SSO) which is where your application trusts another application to perform the authentication. That external application will tell you who the user is without you being able to validate the identity yourself.

From a user perspective this is often also preferable because local accounts in every application is more cumbersome to manage rather than having one account that is automatically shared across applications.

Nabu supports a number of standard ways to centralize authentication (SAML, LDAP,...) but in my experience by far the most widespread one these days is OpenID which builds on OAuth2.

What are you?

In the above you might have been imaging human users which is of course often the case when building web applications. However for API's there is a lot of server-to-server traffic as well where the "user" is not necessarily a human.

While the actual authentication is done differently (we are not prompting a user to manually type in a password), the principles are largely the same. When a system uses an API key, it is functionally equivalent to a password though usually with more complexity in it.

OAuth2 has largely won the battle for SSO, in my experience this includes system-to-system communication. However instead of the more traditional user flow, we often use the client credentials flow which was specifically designed for this type of authentication.

Protocols

As a human yourself, you are likely familiar with most common authentication schemes for human interaction. At the system level there are many ways to authenticate.

There is however a particular standard exchange that is interesting to be aware of.

Suppose you perform an HTTP API call to an endpoint and you don't pass in any kind of credentials. The server might respond with a 401 code indicating you are not authenticated and an additional header WWW-Authenticate in which it explains how you can authenticate yourself.

The most simplistic is this:

WWW-Authenticate: Basic realm=<realm>

The server says you should use "basic" authentication for the given realm. The client can then retry the call with another standardized header that is unfortunately named Authorization. The naming is unfortunate because it is actually used for authentication.

The "basic" authentication referred to is simply taking a username and password, mashing them together with a : in between and base64 encoding the result. You then pass replay the original request and add it as a header:

...
Authorization: Basic <base64>
...

Apart from "basic", there are a few other options, by far the most important one is "bearer" where you obtain a token in some way and pass it in as a bearer token.

...
Authorization: Bearer <token>
...

Of course apart from standardized authentication schemes there are infinite custom ones which are all slight variations on one another. Nabu supports a lot of standard authentication schemes but also makes it easy to plug in custom ones.

Tokens

In more simplistic authentication scenarios where the only identification is through a shared secret (password, API key,...) it is possible to authenticate every call with the same secret.

In more complex setups this is no longer possible though. Imagine you received an sms with a new code every time you clicked a button on a site. The same goes for system to system calls: you don't want to perform a new client credential authentication for every actual API call you want to make.

Instead in most cases once authenticated you receive a token of some sort that is valid for a limited period of time. In all subsequent actions you can give us this token and we will assume that your previously established identity is still valid.

There are in general two types of tokens:

1) A json web token (JWT) is signed by the authentication provider. This has the distinct advantage that if you can trust the token once the signature checks out without performing any additional checks.

There are some downside to JWT tokens though:

  • a JWT token is an encoded JSON object. It does not require any key to read so anyone can read the information held within. This means you might need to be careful with putting any personally identiable information (PII) in it. This in turn means learning anything "valuable" about the client might still require a roundtrip
  • there are two ways to sign a JWT token: symmetric and asymmetric encryption. While asymmetric tokens are considerably bigger, even tokens signed with symmetric encryption are rather large when compared to typical things like API keys, passwords,...
  • if you validate a JWT token only by its signature (which is one of its main advantages), they can not be revoked

A JWT token has a field that determines how long it is valid for. Set it for too long and you might need to worry about being able to revoke it. Set it too short and you need to request a lot of JWT tokens.

2) Opaque tokens are generally much shorter randomly generated identifiers. Because they are unsigned it is imperative that they are generated in a secure way because anyone can generate them. It is also imperative that anyone who receives the token roundtrips to the server that generated it to validate that it is, in fact, a valid token.

While Nabu can generate and use JWT tokens, by default we use opaque tokens.

Token limitations

There are a number of limitations you can put on the tokens we generate:

  • valid from: you can generate a token that is only valid in the future
  • valid until: you can limit the token to only be valid until a certain moment in time
  • timeout: you can set a timeout on a token based on when it was last used
  • uses: you can limit the amount of times a token can be used. For instance there are a lot of usecases for a "one time use" token that allows you to use it exactly once
  • usage: you can limit what a token can be used for. If you have a valid token to call an API endpoint, it might not be a valid token to update a forgotten password.
  • device: you can limit a token to only be usable on a certain device
  • correlation id: you can limit a token to only be used for a particular instance. for example you might have a valid token to download a particular file but not another file even though it has the exact same permissions.

You can combine these limitations as needed.

The tokens can also be revoked which prematurely ends their validity.

Refresh tokens

Once your token is invalidated (due to one of the rules or explicit revocation) you need to obtain a new token in order to proceed.

You can of course re-authenticate using whatever protocol you used before but some protocols (e.g. OAuth2) also support the concept of a refresh token. This is typically a one-time use token that can be used for only one thing: getting a new execution token.

Default Nabu Setup

If you create a default web application in Nabu, upon login the frontend will receive an execution token that is valid for 24 hours but with a timeout of 30 minutes.
If you toggle the "remember" feature, the frontend will also receive a HTTPOnly cookie that contains a refresh token that is valid for a much longer time but can only be used once.

Page builder will use the execution token when performing calls to the backend. When it notices a 401 return from the backend, it will try to retrieve a new execution token using its refresh token.
If that is successful a new refresh token is set in the HTTPOnly cookie and the frontend will automatically replay the call that failed with the new execution token.

From a user perspective this means the action you performed while your token was invalid takes a tiny bit longer than normal but other than that he has no idea his authentication had actually expired.

Note that both the execution token and the refresh token are only usable from the device that requested them. The device cookie that identifies the device is also a HTTPOnly cookie which increases the security of said tokens.

Transport

It is important at this point to at least mention the importance of the transport medium that will carry the token. A token is in essence a shared secret, it establishes who you are. If someone steals that token, he might be able to impersonate you. This is the primary reason that we want to limit the lifecycle of a token.
For this reason it is important that the token is only sent over a secure channel that can not be eavesdropped upon. This means all endpoints in Nabu should be secured with HTTPs rather than relying on plain HTTP.

Nabu comes with ACME support which enables easy certificate generation for public endpoints. It also supports custom inhouse certificate authorities for local solutions.

Once SSL is enabled, all cookies mentioned above are automatically set as Secure which prevents HTTP downgrade attacks.

Authorization

Once we know who you are, we want to know what you can do.

Nabu uses two different types of authorization.

Role-based authorization

In the more simplistic role-based approach Nabu asks a provider to determine whether a given user has a particular role. This is a global binary question: the user either has a role or he doesn't.
While this approach is easy to use and might suffice for simple setups, it is generally not enough because it does not allow data-based differentiation.

For instance the user might have permission to view data from company A, but not company B.

It is however sometimes used in conjunction with the second type of security because it tends to be faster which allows the system to weed out bad actors more quickly without potentially expensive security checks.

Permission-based authorization

By default we use permission-based security, where the Nabu server asks a provider to determine whether the user has a particular permission in a particular context.

For example suppose we want to check if a user can view the details on a particular company, the system might ask something like:

permission: company.get
context: 550e8400-e29b-41d4-a716-446655440000

The context here is something to identify an underlying object, in this case it is a uuid that points to an entity in the database.

Given these two concepts, the server might ask a few other questions.

Does the user have the permission in any context:

permission: contract.update
context: null

Does the user have any permission in a given context:

permission: null
context: 550e8400-e29b-41d4-a716-446655440000

Which permissions exists is fully up to the creators of the application, we do have a standard convention where we use <object>.<action> but you can do something else entirely if needed.

Example

Suppose we have this REST service:

rest service

And we want to ensure that only particular users have access to this endpoint, we could configure this security:

rest service security

As you can see the permission context in this case starts with a = which indicates that it will be evaluated against the input of the service when it is called:

rest service input