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.
One widely adopted and standard way of transmitting secure information and authorizing users is the usage of JWT Tokens.
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. 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, which works with the JWT tokens, no matter which provider you are using.
Side note: We removed the Cognito dependency from Booster, but if you still want to use it, you can include the AWS Auth Rocket 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 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. You can use more than one depending on the use case, but for the moment, let's focus on using one:
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, 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:
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 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. 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:
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.
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:
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:
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:
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.
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.
Top comments (0)