Skip to main content

Authorization

Booster uses a whitelisting approach to authorize users to perform commands and read models. This means that you must explicitly specify which users are allowed to perform each action. In order to do that you must configure the authorize policy parameter on every Command or Read Model. This parameter accepts one of the following options:

  • 'all': The command or read-model is explicitly public, any user can access it.
  • [Role1, Role2, ...]: An array of authorized Roles, this means that only those authenticated users that have any of the roles listed there are authorized to execute the command.
  • An authorizer function that matches the CommandAuthorizer interface for commands or the ReadModelAuthorizer interface for read models.

Making commands and read models public

Setting the option authorize: 'all' in a command or read model will make it publicly accessible to anyone that has access to the GraphQL endpoint. For example, the following command can be executed by anyone, even if they don't provide a valid JWT token:

src/commands/create-comment.ts
@Command({
authorize: 'all',
})
export class CreateComment {
...
}
danger

Think twice if you really need fully open GraphQL endpoints in your application, this might be useful during development, but we recommend to avoid exposing your endpoints in this way in production. Even for public APIs, it might be useful to issue API keys to avoid abuse. Booster is designed to scale to any given demand, but scaling also increases the cloud bill! (See Denial of wallet attacks)

Simple Role-based authorization

Booster provides a simple role-based authentication mechanism that will work in many standard scenarios. It is based on the concept of roles, which are just a set of permissions. For example, a User role might have the permission to create and read comments, while an Admin role might have the permission to create, read, and delete comments. You can define as many roles as you want, and then assign them to users.

Defining @Roles

As many other Booster artifacts, Booster Roles are defined as simple decorated classes. We recommend them to be defined in the src/config/roles.ts file, but it is not limited to that file. To define a role, you only need to decorate an empty class with the @Role decorator as follows:

src/config/roles.ts
@Role()
export class User {}

@Role()
export class Admin {}

Protecting commands and read models with roles

Once you have defined your roles, you can use them to protect your commands and read models. For example, the following command can only be executed by users that have the role Admin:

src/commands/create-comment.ts
@Command({
authorize: [Admin],
})
export class CreateComment {
...
}

This command will not be available to users with the role User.

Associating users with roles

Booster will read the roles from the JWT token that you provide in the request. The token must include a claim with the key you specidied in the rolesClaim field. Booster will read such field and compare it with the declared ones in the authorize field of the protected command or read model.

For example, given the following setup:

src/config/config.ts
Booster.configure('production', (config: BoosterConfig): void => {
config.appName = 'my-store'
config.providerPackage = '@boostercloud/framework-provider-x'
config.tokenVerifiers = [
new JwksUriTokenVerifier(
'https://my-auth0-tenant.auth0.com/', // Issuer
'https://my-auth0-tenant.auth0.com/.well-known/jwks.json', // JWKS URL
'firebase:groups' // <- roles are read from 'firebase:groups' claim from the token
),
]
})

Booster will check that the token contains the firebase:groups claim and that it contains the Admin role. Also, if the token doesn't contain the Admin role, the command will not be executed. As you can see, the decoded token has User as value of the firebase:groups claim, so the command will not be executed.

Extended roles using the Authentication Booster Rocket for AWS

The Authentication Rocket for AWS is an opinionated implementation of a JWT tokens issuer on top of AWS Cognito that includes out-of-the-box features like sign-up, sign-in, passwordless tokens, change password and many other features. When a user goes through the sign up and sign in mecanisms provided by the rocket, they'll get a standard JWT access token that can be included in any request as a Bearer token and will work in the same way as any other JWT token.

When you use this rocket, you can use extra configuration parameters in the @Role decorator to enable some of these features. In the following example we define Admin, User, SuperUser and SuperUserWithoutConfirmation roles. They all contain an extra auth configuration attribute that set the behavior of the authorization role for each role:

@Role({
auth: {
signUpMethods: [], // Using an empty array here prevents sign-ups (Admin has no special treatment. If you don't enable signup, you'll need to create the first admin manually in the AWS console)
},
})
export class Admin {}

@Role({
auth: {
signUpMethods: ['email'], // Enable email sign-ups for Users
},
})
export class User {}

@Role({
auth: {
signUpMethods: ['email', 'phone'], // Can sign up by email or phone
skipConfirmation: false, // It requires email or phone confirmation. The rocket will send either an email or a SMS with a confirmation link.
},
})
export class SuperUser {}

@Role({
auth: {
signUpMethods: ['email', 'phone'],
skipConfirmation: true, // It doesn't require email or phone confirmation
},
})
export class SuperUserWithoutConfirmation {}

To learn more about the Authorization rocket for AWS, please read the README in its Github repository.

Custom authorization functions

Booster also allows you to implement your own authorization functions, in case the role-based authorization model doesn't work for your application. In order to apply your own authorization functions, you need to provide them in the authorize field of the command or read model. As authorization functions are regular JavaScript functions, you can easily reuse them in your project or even in other Booster projects as a library.

Command Authorizers

As mentioned, the authorize parameter of the @Command can receive a function. However, this function must match the CommandAuthorizer type. This function will receive two parameters and return a Promise that will resolve if the user is authorized to execute the command or reject if not:

export type CommandAuthorizer = (currentUser?: UserEnvelope, input?: CommandInput) => Promise<void>
ParameterTypeDescription
currentUserUserEnvelopeUser data decoded from the provided token
inputCommandInputThe input of the command

For instance, if you want to restrict a command to users that have a permission named Permission-To-Rock in the permissions claim you can do this:


const CustomCommandAuthorizer: CommandAuthorizer = async (currentUser) => {
if (!currentUser.claims['permissions'].includes('Permission-To-Rock')) {
throw new Error(`User ${currentUser.username} should not be rocking!`) // <- This will reject the access to the command
}
}

@Command({
authorize: CustomCommandAuthorizer,
})
export class PerformIncredibleGuitarSolo {
...
}

Read Model Authorizers

As with commands, the authorize parameter of the @ReadModel decorator can also receive a function. However, this function must match the ReadModelAuthorizer type. This function will receive two parameters and return a Promise that will resolve if the user is authorized to execute the command or reject if not:

export type ReadModelAuthorizer<TReadModel extends ReadModelInterface> = (
currentUser?: UserEnvelope,
readModelRequestEnvelope?: ReadModelRequestEnvelope<TReadModel>
) => Promise<void>
ParameterTypeDescription
currentUserUserEnvelopeUser data decoded from the provided token
inputCommandInputThe input of the command

For instance, you may want to restrict access to a specific resource only to people that has been granted read permission:

const CustomReadModelAuthorizer: ReadModelAuthorizer = async (currentUser, readModelRequestEnvelope) => {
const userPermissions = Booster.entity(UserPermissions, currentUser.username)
if (!userPermissions || !userPermissions.accessTo[readModelRequestEnvelope.className].includes(readModelRequestEnvelope.key.id)) {
throw new Error(`User ${currentUser.username} should not be looking here`)
}
}

@ReadModel({
authorize: CustomReadModelAuthorizer
})

Event Stream Authorizers

You can restrict the access to the Event Stream of an Entity by providing an authorizeReadEvents function in the @Entity decorator. This function is called every time an event stream is requested. The function must match the EventStreamAuthorizer type receives the current user and the event search request as parameters. The function must return a Promise<void>. If the promise is rejected, the request will be denied. If the promise is resolved successfully, the request will be allowed.

export type EventStreamAuthorizer = (
currentUser?: UserEnvelope,
eventSearchRequest?: EventSearchRequest
) => Promise<void>

For instance, you can restrict access to entities that the current user own.

const CustomEventAuthorizer: EventStreamAuthorizer = async (currentUser, eventSearchRequest) => {
const { entityID } = eventSearchRequest.parameters
if (!entityID) {
throw new Error(`${currentUser.username} cannot list carts`)
}
const cart = Booster.entity(Cart, entityID)
if (cart.ownerUserName !== currentUser.userName) {
throw new Error(`${currentUser.username} cannot see events in cart ${entityID}`)
}
}


@Entity({
authorizeReadEvents: CustomEventAuthorizer
})
export class Cart {
public constructor(
readonly id: UUID,
readonly ownerUserName: string,
readonly cartItems: Array<CartItem>,
public shippingAddress?: Address,
public checks = 0
) {}
...
}