Booster authorization in a nutshell
Published: 2022-05-26 09:01:46

## Overview

After several successful projects using The Booster Framework, with a variety of clients and internal projects, a common recurring question is How does Booster handle Authorization?
In this article, I will shed some light on this topic, by dissecting Booster’s approach towards authorization.

## Standards, standards, standards

By design, The Booster Framework is a role-based authorized framework, which means that commands and read operations, are performed by specific roles created by the user, i.e. Admin, Customer, User, you name it. If you want to keep your API open, you can use the predefined **_all_** role, but that would be a bad idea. A better practice would be to limit the access to your API. If you want to know more about how to define roles in Booster, click [here](https://docs.booster.cloud/chapters/04_features?id=authentication-and-authorization).

One widely adopted and standard way of transmitting secure information and authorizing users is the usage of [JWT Tokens](https://jwt.io/introduction/). 

In this article, I don't want to dig deeper into the details of the JWT standard, but long story short, you will need a JWT auth provider, or create your own, which generates valid tokens for your users. Inside of those tokens includes the user roles you need as claims in your Booster application.

In the early stages of Booster development, and since we were using AWS as our first cloud provider, Booster offered endpoints to create, update, and login users, using [AWS Cognito](https://aws.amazon.com/cognito/). When you created a user, you had to specify that user's role as a part of the associated information. When the user signed in, Cognito returned a JWT token with all the information associated with the user, including the role.

Then, you had to use that token in the Authorization header in your API requests as a bearer token, and Booster internally called Cognito again to verify if the token was valid, and if it contained the right role to perform the API call.

That approach worked for a while, until some Booster users didn't want to use Cognito. Some wanted to use their auth, while others were using a different auth provider like Auth0, Firebase, Cognito, Okta, or whatever.

Once again the standards to the rescue. Even if we were using JWT, Booster was tightly coupled to Cognito to verify the token and get the information associated with it. We decided to extract that part and use a standard token verification [inside the Booster core](https://github.com/boostercloud/booster/blob/main/packages/framework-core/src/booster-token-verifier.ts), which works with the JWT tokens, no matter which provider you are using. 

![Architecture diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rxzw8q981w8su8nfttaz.png)

**Side note:** We removed the Cognito dependency from Booster, but if you still want to use it, you can include the [AWS Auth Rocket](https://github.com/boostercloud/rocket-auth-aws-infrastructure) which provides the most common Cognito features out-of-the-box for Booster applications. 

## Token verifiers

The JWT standard works by signing tokens using private and public key pairs using [asymmetric cryptography](https://en.wikipedia.org/wiki/Public-key_cryptography) and different algorithms (mostly RSA or ECDSA). So basically, the public key is well known by the clients, but the private key must be secret and will only be available on the server side.

Ok...but what do I need to use an auth in Booster?

In Booster you will need to specify [token verifiers](https://docs.booster.cloud/chapters/04_features?id=jwt-configuration). You can use more than one depending on the use case, but for the moment, let's focus on using one:

```typescript
Booster.configure('production', (config: BoosterConfig): void => {   
  config.tokenVerifiers = [
    {
      jwksUri: 'https://demoapp.firebase.com/.well-known/jwks.json',
      issuer: 'https://securetoken.google.com/demoapp',
      rolesClaim: 'firebase:groups',
    }
  ]
}) 
```

In the example above, we are using Firebase as a provider for a demo app. The _**jwksUri**_ property contains the public URL where Firebase exposes the public keys as [JSON web keys](https://datatracker.ietf.org/doc/html/rfc7517), the _**issuer**_ specifies who is emitting tokens, and the **_rolesClaim_** value is the claim where Firebase adds the roles.

Let's create a configuration using the Cognito provider:

```typescript
Booster.configure('production', (config: BoosterConfig): void => {   
  config.tokenVerifiers = [
    {
      jwksUri: 'https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json',
      issuer: 'https://cognito-idp.{region}.amazonaws.com/{userPoolId}',
      rolesClaim: 'cognito:groups',
    }
  ]
})  
```

The same here, nothing weird, except provider specific configuration.

That's great, but what if I want to test my roles as a part of the development process and I don't want to stick without any auth provider.

In Booster you can use the awesome [local provider](https://docs.booster.cloud/chapters/04_features?id=local-provider) for testing purposes and configure it alongside a local token verifier.

## Your local kingdom

First, we need to generate our public and private keys for our local JWT auth token generator. For that, you can use this [online tool](https://cryptotools.net/rsagen). **Attention! Don't use those keys in any production environment**.

After that, save both keys into proper files, like _**private.key**_ and **_public.key_** since you will need them later on.

To generate tokens for different users, with different roles, let’s create a simple **_node.js_** script:

```bash
mkdir testToken;cd testToken;npm init -y;npm install jsonwebtoken
```

Move the private.key you saved into the project into a folder inside your project i.e. keys folder.  

Then copy the following code into the index.js and change it accordingly, depending on your needs.

```javascript
const fs = require('fs')
const jwt = require('jsonwebtoken')
const path = require('path')

const privateKey = fs.readFileSync(path.join(__dirname, '.', 'keys', 'private.key'))

function forUser(email, role) {
 const keyid = 'booster'
 const issuer = 'booster'
 return jwt.sign(
   {
     id: email,
     demoRole: role,
     email,
   },
   privateKey,
   {
     algorithm: 'RS256',
     subject: email,
     issuer,
     keyid,
     expiresIn: 0
   }
 )
}

console.log('Here is your admin user token: ', forUser('test@boostercloud.com', 'Admin'))
```

The code above is self explanatory, but basically it’s using the _**jsonwebtoken**_ library to sign tokens with the private key and it’s adding some data like the email and the role in the _**demoRole**_ property.

Finally let's run it:

```bash
node index.js
Here is your admin user token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImJvb3N0ZXIifQ.eyJpZCI6InRlc3RAYm9vc3RlcmNsb3VkLmNvbSIsImRlbW9Sb2xlIjoiQWRtaW4iLCJlbWFpbCI6InRlc3RAYm9vc3RlcmNsb3VkLmNvbSIsImlhdCI6MTY1MjE5MDE1NiwiZXhwIjoxNjUyMTkwMTU2LCJpc3MiOiJib29zdGVyIiwic3ViIjoidGVzdEBib29zdGVyY2xvdWQuY29tIn0.HMBm_2MPVA_QHKr5hMTW_zmH9BFC5TplOOfSD34NrUUONzOU-1d6gKgNNRV_NX6Nem_4yksnUV64IhhLffmRNljBtIGQ-HaiVQ9S4MnNqyJCQRCArkK4xyu5EQd7RTNtLPS_xetn8kAJYzIlnO1KRNeQphplaeyEMCS5irjR9-A
```

Woohoo! you have created a valid JWT token to use in Booster.

In order to use that token in Booster, in the API request headers you will need to config the _**public.key**_ in your token verifier:

```typescript
Booster.configure('production', (config: BoosterConfig): void => {   
  config.tokenVerifiers = [
    {
      publicKey: fs.readFileSync(path.join(__dirname, '.', 'keys', 'public.key')),
      issuer: 'booster',
      rolesClaim: 'demoRole',

    }
  ]
})
```

The code above will use the _**publicKey**_ property instead of the **_jwksUri_** since we don’t have a public URL with the keys, as many providers offer.

Take into account that we are using the same issuer we used to sign in the token.

## Extra, extra!

Some users asked about token validations that will check other data encoded inside the token, and grant or deny access based on that. For this purpose, Booster config has a property function called _**extraValidation**_ which receives the decoded token as a parameter, allowing the users to do things like this:

```typescript
Booster.configure('production', (config: BoosterConfig): void => {   
  config.tokenVerifiers = [
    {
      publicKey: fs.readFileSync(path.join(__dirname, '.', 'keys', 'public.key')),
      issuer: 'booster',
      rolesClaim: 'demoRole',
      extraValidation: (decodedToken) => {
         if (!decodedToken.payload.trust) {
            throw 'We don't trust on you'
         }
      }
    }
  ]
})
```

## Conclusions

As you have seen, Booster provides a standard and easy way of authenticating requests based on user roles which will cover most of the common use cases, since the JWS is widely adopted. For those use cases that won’t fit with the defaults, Booster provides a way of extending the framework thanks to the usage of rockets, but that’s another story. If you want to know more about how to create rockets, please refer to the official [documentation](https://docs.booster.cloud/chapters/05_going-deeper?id=extending-booster-with-rockets). 

Last but not least, if you have any questions about Booster or any other topic related, we would be happy to hear your thoughts on [our community channel](https://discord.gg/bDY8MKx).