@travetto/auth-model

Model-based authentication and registration support for the travetto framework

Usage no npm install needed!

<script type="module">
  import travettoAuthModel from 'https://cdn.skypack.dev/@travetto/auth-model';
</script>

README

Model Auth Source

Model-based authentication and registration support for the travetto framework

Install: @travetto/auth-model

npm install @travetto/auth-model

This module provides the integration between the Authentication module and the Data Modeling Support.

The asset module requires an CRUD to provide functionality for reading and storing user information. You can use any existing providers to serve as your CRUD, or you can roll your own.

Install: provider

npm install @travetto/model-{provider}

Currently, the following are packages that provide CRUD:

The module itself is fairly straightforward, and truly the only integration point for this module to work is defined at the model level. The contract for authentication is established in code as providing translation to and from a RegisteredIdentity

A registered identity extends the base concept of an identity, by adding in additional fields needed for local registration, specifically password management information.

Code: Registered Identity

export interface RegisteredIdentity extends Identity {
  /**
   * Password hash
   */
  hash: string;
  /**
   * Password salt
   */
  salt: string;
  /**
   * Temporary Reset Token
   */
  resetToken?: string;
  /**
   * End date for the reset token
   */
  resetExpires?: Date;
  /**
   * The actual password, only used on password set/update
   */
  password?: string;
}

Code: A valid user model

import { Model, BaseModel } from '@travetto/model';
import { RegisteredIdentity } from '@travetto/auth-model';

@Model()
export class User extends BaseModel implements RegisteredIdentity {
  id: string;
  source: string;
  details: Record<string, unknown>;
  password?: string;
  salt: string;
  hash: string;
  resetToken?: string;
  resetExpires?: Date;
  permissions: string[];
}

Configuration

Additionally, there exists a common practice of mapping various external security principals into a local contract. These external identities, as provided from countless authentication schemes, need to be homogeonized for use. This has been handled in other frameworks by using external configuration, and creating a mapping between the two set of fields. Within this module, the mappings are defined as functions in which you can translate to the model from an identity or to an identity from a model.

Code: Principal Source configuration

import { InjectableFactory } from '@travetto/di';
import { ModelPrincipalSource, RegisteredIdentity } from '@travetto/auth-model';

import { User } from './model';

class AuthConfig {
  @InjectableFactory()
  static getModelPrincipalSource() {
    return new ModelPrincipalSource(
      User,
      (u: User) => ({    // This converts User to a RegisteredIdentity
        source: 'model',
        provider: 'model',
        id: u.id,
        permissions: u.permissions,
        hash: u.hash,
        salt: u.salt,
        resetToken: u.resetToken,
        resetExpires: u.resetExpires,
        password: u.password,
        details: u,
      }),
      (u: Partial<RegisteredIdentity>) => User.from(({   // This converts a RegisteredIdentity to a User
        id: u.id,
        permissions: [...(u.permissions || [])],
        hash: u.hash,
        salt: u.salt,
        resetToken: u.resetToken,
        resetExpires: u.resetExpires,
      })
      )
    );
  }
}

Code: Sample usage

import { AppError } from '@travetto/base';
import { Injectable, Inject } from '@travetto/di';
import { ModelPrincipalSource } from '@travetto/auth-model';

import { User } from './model';

@Injectable()
class UserService {

  @Inject()
  private auth: ModelPrincipalSource<User>;

  async authenticate(identity: User) {
    try {
      return await this.auth.authenticate(identity.id!, identity.password!);
    } catch (err) {
      if (err instanceof AppError && err.category === 'notfound') {
        return await this.auth.register(identity);
      } else {
        throw err;
      }
    }
  }
}