npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

loopback-supertokens

v1.1.0

Published

LoopBack extensions for authentication and authorization using SuperTokens

Downloads

29

Readme

loopback-supertokens

LoopBack extension for SuperTokens.

It integrates SuperTokens with Loopback:

  • Use @authenticate('supertokens') for protected endpoints;
  • Use @authorize for role-based access control (RBAC);
  • Use webhooks to tap into user flows and extend your application;

Setup

This assumes SuperTokens is already set up with your application. If that's not the case, you will need to install supertokens-node and properly initialize it with your application. Head to SuperTokens > Docs > EmailPassword recipe > Backend for detailed instructions.

Install the dependency:

pnpm add \
    loopback-supertokens \
    @loopback/authentication \
    @loopback/authorization

Open src/application.ts and mount SupertokensComponent after the AuthenticationComponent and AuthorizationComponent:

// ...
import { SupertokensComponent } from 'loopback-supertokens';

export class TestApplication extends BootMixin(
  ServiceMixin(RepositoryMixin(RestApplication)),
) {
  constructor(options: ApplicationConfig = {}) {
    super(options);

    // Mount authentication, authorization and supertokens in that order:
    this.component(AuthenticationComponent);
    this.component(AuthorizationComponent);
    this.component(SupertokensComponent);
  }

  // ...
}

That's pretty much it.

Usage

Authentication

Use @authenticate('supertokens') to annotate/decorate controllers or controller endpoints as you would normally do with any other LoopBack authentication strategy.

import { authenticate } from '@loopback/authentication';

// ...

// Use @authenticate('supertokens') here if all methods should be protected.
export class PlaylistController {
  @authenticate('supertokens')
  @get('/playlists/{id}' /* ... */)
  async find(
    @param.path.number('id') id: number,
    @param.query.object('filter') filter?: Filter<Team>,
  ): Promise<Playlist> {
    return this.repository.findById(id, filter);
  }

  // ...
}

See LoopBack authentication component for more details.

Authorization

Use @authorize() to define allowed roles per controller or per controller method.

In that example, we are declaring that only users with role 'admin' or 'manager' can list all teams:

import { authenticate } from '@loopback/authentication';
import { authorize } from '@loopback/authorization';

// ...

export class TeamController {
  @authenticate('supertokens')
  @authorize({
    allowedRoles: ['admin', 'manager'],
  })
  @get('/teams' /* ... */)
  async find(
    @param.query.object('filter') filter?: Filter<Team>,
  ): Promise<Team[]> {
    return this.teamRepository.find(filter);
  }

  // ...
}

Refer to Loopback authorization component for more details.

Work-around: using with @loopback/rest-crud

LoopBack offers a way to generate controllers automatically which does not allow to annotate methods or controllers.

As a work-around, one can bypass the auto-wiring provided by @loopback/rest-crud and manually set up the container, manually calling the defineCrudRestController function to register/configure the controller. Instead of creating a file inside the src/model-endpoints directory, create a file inside src/controllers with the following content:

import { authenticate } from '@loopback/authentication';
import { Filter, repository } from '@loopback/repository';
import { HttpErrors } from '@loopback/rest';
import { defineCrudRestController } from '@loopback/rest-crud';
import { Team } from '../models';
import { TeamRepository } from '../repositories';

const CrudRestController = defineCrudRestController<
  Team,
  typeof Team.prototype.id,
  'id'
>(Team, { basePath: '/teams' });

@authenticate('supertokens')
export class TeamController extends CrudRestController {
  constructor(
    @repository(TeamRepository) protected entityRepository: TeamRepository,
  ) {
    super(entityRepository);
  }
}

This isn't only an issue with this extension but with any app that wants to use the @authenticate decorator with @loopback/rest-crud.

See also:

  • https://github.com/loopbackio/loopback-next/discussions/8905
  • https://github.com/loopbackio/loopback-next/tree/0ece5e7f0113dcc070ba44210c472257f8bd0e93/packages/rest-crud#creating-a-crud-controller

Getting the current user

Here's a controller action that uses Loopback's dependency injection and authentication component to get the current session and make it available to the front-end. loopback-supertokens encapsulates Supertokens and lets you rely entirely on Loopback's authentication component/mechanism.

export class AuthenticationController {
  @authenticate('supertokens')
  @get('/authentication/users/me')
  async getCurrentUser(
    @inject(SecurityBindings.USER)
    profile: UserProfile,
  ) {
    return {
      session: profile.session,
      userId: profile.userId,
      userDataInAccessToken: profile.userDataInAccessToken,
    };
  }
}

Communication with Loopback

Let's say your app is about users creating cool playlists.

Regardless if you use the managed service or self-hosted deployment, SuperTokens is an independant authentication service, separate from your app handling user sign-up, sign-in, sign-out, and other authentication-related tasks and storing users data in a separate database. It is quite a sensible pattern for services to be split up into smaller, more manageable, isolated/independant components.

Yet, it would be nice to be able to express the relationship between a Playlist entity and its owner, i.e. a User entity, leverage Loopback features to their fullest (inclusion resolvers and repositories) and ensure foreign key constraints in the database. This is a fairly typical use case where one service needs to tap into the logic (subscribe) of another service and replicate parts of its data.

import { Entity, model, property } from '@loopback/repository';

@model()
export class User extends Entity {
  @property({
    id: true,
    type: 'string',
  })
  id: string;

  constructor(data?: Partial<User>) {
    super(data);
  }
}

@model({
  settings: {
    foreignKeys: {
      fk_Playlist_User: {
        name: 'fk_Playlist_User',
        entity: 'User',
        entityKey: 'id',
        foreignKey: 'userid',
      },
    },
  },
})
export class Playlist extends Entity {
  @property({
    generated: true,
    id: true,
    type: 'number',
  })
  id: number;

  @property({
    required: true,
    type: 'string',
  })
  name: string;

  @belongsTo(() => User, undefined, {
    required: true,
  })
  userId: string;

  constructor(data?: Partial<Playlist>) {
    super(data);
  }
}

loopback-supertokens promotes a webhook pattern that integrates with SuperTokens' post callbacks such as signUpPost and signInPost and lets us handle the request the Loopback way. Read more on "Why a webhook pattern?" below.

  1. With SuperTokens signUpPost callback and SupertokensWebhookHelper class provided by loopback-supertokens, we dispatch an event to a webhook endpoint;
  2. Our webhook endpoint receives said event and use SupertokensWebhookHelper to verify the authenticity of the request/event;
  3. We persist the user to our database;

Dispatch the webhook

Most of the following code is from SuperTokens "Post sign up callbacks" documentation.

import { SupertokensWebhookHelper } from 'loopback-supertokens';
// ...

// Use `.env` to declare these:
const WEBHOOK_SIGNATURE_SECRET = 'flying microtonal banana';
const WEBHOOK_ENDPOINT_URL = 'http://localhost:9000/authentication/webhook';

const webhookHelper = new SupertokensWebhookHelper(WEBHOOK_SIGNATURE_SECRET);

supertokens.init({
  // ...
  recipeList: [
    EmailPassword.init({
      override: {
        apis: (apiImplementation) => {
          return {
            ...apiImplementation,
            signUpPOST: async (input) => {
              if (!apiImplementation.signUpPOST) {
                throw new Error('Should never happen');
              }

              // First we call the original implementation of signUpPOST.
              const response = await apiImplementation.signUpPOST(input);

              if (response.status === 'OK') {
                // Create an event to dispatch based on the response:
                const userSignUpEvent = webhookHelper
                  .getEventFactory()
                  .createUserSignUpEvent(response);

                // Dispatch the event:
                webhookHelper
                  .dispatchWebhookEvent(WEBHOOK_ENDPOINT_URL, userSignUpEvent)
                  .catch((err: Error) => {
                    console.error(err);
                  });
              }

              return response;
            },
          };
        },
      },
    }),
    // ...
  ],
  // ...
});

Write the webhook endpoint

The key bit here is to write a controller that matches the endpoint hit by the callback. You can use lb4 controller for this.

Another important aspect is to use @authenticate('supertokens-internal-webhook') to protect the endpoint, enforce signature verification and replay attack protection. This is provided by loopback-supertokens. Read more about webhook signature below.

import { authenticate } from '@loopback/authentication';
import { repository } from '@loopback/repository';
import { post, requestBody } from '@loopback/rest';
import { WebhookEvent, WebhookEventType } from 'loopback-supertokens';
import { UserRepository } from '../../repositories';

export class WebhookController {
  constructor(
    @repository(UserRepository)
    protected userRepository: UserRepository,
  ) {}

  @authenticate('supertokens-internal-webhook')
  @post('/authentication/webhook')
  async execute(
    @requestBody()
    event: WebhookEvent,
  ): Promise<void> {
    switch (event.type) {
      case WebhookEventType.UserSignUp:
        // Create the user out of the event data/payload:
        this.userRepository.create(event.data.user);

        return;
    }
  }
}

Why webhooks?

The main reason is that Supertokens callbacks are running outside of Loopback. Webhooks provide a mechanism to let Supertokens communicate with Loopback and still leverage the normal Loopback workflows and tools (i.e. dependency injection, services, repositories/models).

Additionally, webhooks is a common pattern used for integrating two or more systems in a loosely coupled way. As your app scales, you might split it up into smaller, more manageable, components and webhooks will be helpful then and allow for flexibility in how each component is designed and implemented.

Webhooks authentication method

Webhooks use hash-based message authentication code (HMAC) as webhook authentication method. This is, by far, the most popular webhook authentication method out there and if you worked with webhooks before you probably encountered this before.

This library implements the webhook signatures in a similar way as what Stripe does.

When sending a webhook, the producer uses a secret key and HMAC to compute a hash of the message (event). This hash signature is sent as a custom header along with the webhook request, i.e. webhook-signature by default.

Here's what it looks like:

Webhook-Signature: t=1683810604 v1=k3sYVKM84CvM8szBhNkXJbbYUgb3WRKpSdVe/wEG5EY=

Finally, upon receiving the webhook request, the consumer (Loopback webhook controller) computes a hash using the same shared secret key and compares it with the custom header value received. A timestamp is included to prevent replay attacks.

Read more:

Why SuperTokens?

LoopBack authentication examples and extensions can be limitating and require to roll your own implementation in many places to have a fully-fledge production-ready authentication. Security is hard and rolling your own implementation is hardly a good idea.

SuperTokens provides a all-in-one open-source authentication solution including:

  • Various auth. methods: email/password, social logins, passwordless, etc;
  • Complementary features: session/token management, multi-factor authentication, user roles (role-based access control, i.e. RBAC), reset password flow, email verification, etc;
  • Tools: such as user management dashboard and front-end SDK;

What are alternatives?