# Getting started

WARNING

This library is still in development and the API may still change! We appreciate any feedback you may provide to further improve this library.

This library has been inspired by Adonisjs Bouncer (opens new window). However, this library is framework agnostic and can, as such, easily be used with any framework of your choice.

# Motivations

Writing clean authorization logic is difficult, even more so if you don't know where to start off from. The countless different bad solutions you can encounter on the internet doesn't make this any easier.

Here however a few most commonly encoutered "solutions" that are mentionned most of the time:

Authorization via middlewares:

Some people put authorization code in HTTP middlewares or other similar functions that are tightly linked to your framework or transport layer. However, this solution has multiple issues:

  • If you change your transport layer (for example, when migrating some REST endpoints to GraphQL, which requires a different way to authorize your calls), you will need to change all your authorization code, or even duplicate authorization code if you want to support multiple transport layers.

  • When putting authorization code in middlewares, you sometimes won't have access to all information required to make complex authorization checks.

Example: Allow all users (including guests) to view published posts, but only allow authors to view posts that are still in draft.

In the middleware, you won't have access to the current "post", because you only fetch and handle this data in your application layer.

This gives you two (bad) choices:

  • Either duplicate some of your application layer code in your middleware (including duplicate database calls), so you can check if the user has the correct access rights are not.

  • Either move your authorization logic for this feature to the application layer... which makes your code-base inconsistent: Sometimes your authorization will be in middleware, sometimes not. Sometimes your authorization will be in your application layer, sometimes not.

Both solutions make your code harder to maintain.

Authorization via inline code:

Instead of using middlewares that depend on their transport layer, some people put all their authorization code directly in their application layer.

This is in fact a step in the right direction, as it is more flexible and allows for more complex authorization checks. However, it is once again not ideal, because authorization logic is mixed with your application logic. This makes it harder to test and understand.

As such, you should rather always extract these bits of authorization logic in small independent functions, and this library helps you do exactly that.

# Use cases

This library offers a framework agnostic and uniform way to organize and call your authorization logic, while staying very simple and low-level, so that you can easily support any kind of authorization your application may need:

  • If you only require role-based authorization? No problem!

  • If you require ACL-based authorization? Create a custom PolicyContext able to check in your database if the user has the required rights.

  • Want to add authorization middlewares and interceptors, to add special authorization logic to all your existing policies? This is supported as well.

# Installation

Install peer dependencies: npm install @apoyo/std

Install package: npm install @apoyo/policies

# Typescript configuration

To have full type-safety with this library, the following two options are required:

{
  "strictNullChecks": true,
  "strictFunctionTypes": true,
}

# Usage

This library exposes multiple concepts:

  • The UserContext, to authenticate a user for a given request / asynchroneous context (see AsyncLocalStorage (opens new window) for more information).

  • a PolicyContext, to define the context that is required to execute our policies.

  • a Policy, that for a given action defines who can access it. These policy will generally contain all your authorization logic.

  • an Authorizer, which can authorize policies for a given user.

While going through this quick preview, you will see all these concepts being used.

# Create some types

src/user.ts:

export interface User {
  id: string
  email: string
  role: 'admin' | 'moderator' | 'member'
}

src/post.ts:

export interface Post {
  id: string
  authorId: string
  status: 'draft' | 'published'
}

# Create custom user context and policy context

src/user-context.ts:

import { UserContext } from '@apoyo/policies'
import { User } from './user'

export class MyUserContext extends UserContext<User> {}

src/policy-context.ts:

import { PolicyContext } from '@apoyo/policies'
import { MyUserContext } from './user-context'

export class MyPolicyContext {
  constructor(
    private readonly _userContext: MyUserContext) {}

  public getCurrentUser(): User
  public getCurrentUser(options: { allowGuest: false }): User
  public getCurrentUser(options: { allowGuest: true }): User | null
  public getCurrentUser(options: { allowGuest: boolean } = { allowGuest: false }): User | null {
    const allowGuest = options?.allowGuest ?? false
    const user = this._userContext.getUser()
    if (!allowGuest && !user) {
      throw new NotAuthenticatedException()
    }
    return user
  }
}

src/authorizer.ts:

import { Authorizer } from '@apoyo/policies'
import { MyPolicyContext } from './policy-context'

export class MyAuthorizer extends Authorizer<MyPolicyContext> {
  public getCurrentUser(): User
  public getCurrentUser(options: { allowGuest: false }): User
  public getCurrentUser(options: { allowGuest: true }): User | null
  public getCurrentUser(options: { allowGuest: boolean } = { allowGuest: false }): User | null {
    return this.context.getCurrentUser(options)
  }
}

# Declare policies

Policies are used to tell us what can be done by who. As such, they form the cornerstone of our authorization.

Policies use Generators (opens new window) to run their logic. This enables us to early exit the code (and cancelling the remaining verifications) if necessary.

As such, when a boolean is yielded or returned by the policy, the policy finishes and will either succeed or fail (and throw an NotAuthorizedException) depending on if the boolean is true or false.

src/policies/posts.policy.ts:

import { BasePolicy } from '@apoyo/policies'
import { MyPolicyContext } from './policy-context'
import { Post } from './post'

export class ViewPostPolicy implements BasePolicy {
  public async *authorize(ctx: MyPolicyContext, post: Post) {
    if (post.status === 'published') {
      return true
    }

    const user = ctx.getCurrentUser()
    return user.id === post.authorId
  }
}

# Use authorizer

src/use-cases/view-post.ts:

import { Authorizer } from '@apoyo/policies'
import { PostRepository } from '../repositories/post.repository'
import { ViewPostPolicy } from '../policies/posts.policy'

export class ViewPostUseCase {
  constructor(private readonly authorizer: Authorizer, private readonly postRepository: PostRepository) {}

  public async execute(id: string) {
    const post = await this.postRepository.findById(id)

    // Authorize the given policy
    // Required parameters are automatically inferred, and typescript will complain on missing parameters
    await this.authorizer.authorize(ViewPostPolicy, post)

    return post
  }
}

# Authenticating a user

const userContext = new MyUserContext()
const policyContext = new MyPolicyContext(userContext)
const authorizer = new MyAuthorizer(policyContext)

// User to authenticate
const user: User | null = {
  id: 'xxxx',
  email: 'test@example.com',
  role: 'member'
}

await userContext.forUser(user, async () => {
  // Authenticated user is available in this scope

  console.log(authorizer.getCurrentUser())
})