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

@w3lcome/feathers-refresh-token

v0.1.1

Published

Refresh token hooks for @feathers/authentication

Downloads

236

Readme

Refresh tokens hooks for Feathers

Forked from TheSinding/authentication-refresh-token There are three major differences of my implementation:

  1. Implement refresh token via Feathers standalone service
  2. The form of refresh token is actual JWT
  3. Support all authentication strategies (local, oAuth)
  4. Support multi-devices login

Key features

Leveraging existing Feathers built-in authentication service and JWT support to implement refresh token functionalities via couple hooks:

  1. issueRefreshToken - issuing refresh token after user authenticated successfully and save it via custom refresh-tokens service
  2. refreshAccessToken - issuing new access token by making a POST request to /refresh-tokens endpoint along with user Id and a valid refresh token
  3. revokeRefreshToken - revoke refresh token by making PATCH request to /refresh-tokens endpoint
  4. logoutUser - remove the refresh token by making a DELETE request to /refresh-tokens endpoint

This is still new, use with caution


How to use it

  1. Create a Feathers App (feathers generate app)
  2. Authentication should be enable, authentication strategies and user entity service is setup properly
  3. Import feathers-refresh-token
  4. Add a custom service (feathers generate service)
  5. Add refresh-token config in default.json
  6. Add hooks to authentication service and customer service created on step 4

Import this package to your Feathers App project

npm install @jackywxd/feathers-refresh-token or yarn add @jackywxd/feathers-refresh-token

Add 'refresh-token' config section in default.json under authentication section. Basically it mirrors the settings of authentication. It is suggested that change access token expiresIn to 15m

  • entity: the refresh token entity name
  • service: the refresh token service name
  • secret: secret of refresh token JWT, should be different than access token's secret
  • jwtOptions: refresh token JWT options
  "authentication": {
    "entity": "user",
    "service": "users",
    "secret": "Mor17jj93SV4Q26GvivuvOySqA0=",
    "authStrategies": ["jwt", "local"],
    "jwtOptions": {
      "header": {
        "typ": "access"
      },
      "audience": "https://yourdomain.com",
      "issuer": "feathers",
      "algorithm": "HS512",
      "expiresIn": "15m"
    },
    "refresh-token": {
      "entity": "refreshToken",
      "service": "refresh-tokens",
      "secret": "oQQjDiCO/Okmm/AUMN7aqKXod+M=asdfasdfasdf99kdsl)(&&3mc,",
      "jwtOptions": {
        "header": {
          "typ": "refresh"
        },
        "audience": "https://example.com",
        "issuer": "example",
        "algorithm": "HS256",
        "expiresIn": "360d"
      }
    },

If "refresh-token" config section is missing in default.json file, the default refresh-token options will be used as below

export const defaultOptions = {
  service: 'refresh-tokens', // refresh-token service name
  entity: 'refreshToken', // refresh-token entity
  secret: 'supersecret', // secret for Refresh token
  jwtOptions: {
    header: {
      typ: 'refresh'
    },
    audience: 'https://example.com',
    issuer: 'example',
    algorithm: 'HS256',
    expiresIn: '360d'
  }
};

Configure a service as refresh token endpoint, the name should match the "service" name in refresh-token config options, default is refresh-tokens. This is the endpoint client used to refresh access token and logout user

refresh-tokens.service.ts

// Initializes the `refresh-tokens` service on path `/refresh-tokens`
import { ServiceAddons } from '@feathersjs/feathers';
import { Application } from '../../declarations';
import { RefreshTokens } from './refresh-tokens.class';
import createModel from '../../models/refresh-tokens.model';
import hooks from './refresh-tokens.hooks';

// Add this service to the service type index
declare module '../../declarations' {
  interface ServiceTypes {
    'refresh-tokens': RefreshTokens & ServiceAddons<any>;
  }
}

export default function (app: Application) {
  const options = {
    Model: createModel(app),
    paginate: app.get('paginate')
  };

  // Initialize our service with any options it requires
  app.use('/refresh-tokens', new RefreshTokens(options, app));

  // Get our initialized service so that we can register hooks
  const service = app.service('refresh-tokens');

  service.hooks(hooks as any);
}

Depends on the DB model you are using, you may need to configure refresh-tokens model. Below is the refresh-token data type interface

export type RefreshTokenData = {
  id?: string; // id filed for refresh token
  _id?: string;
  userId: string; // user Id
  refreshToken: string; // refresh token
  isValid: boolean; // refresh token is valid or not
  deviceId?: string; // user login device Id, provied by client
  location?: string; // user login location, provided by client
  createdAt?: string; // user login time (refresh-tokenn creation time)
  updatedAt?: string;
};

Below is the model file for mongoose refresh-tokens.model.ts

export default function (app: Application) {
  const modelName = 'refreshTokens';
  const mongooseClient = app.get('mongooseClient');
  const { Schema } = mongooseClient;

  const schema = new Schema(
    {
      userId: { type: String, required: true },
      refreshToken: { type: String, required: true },
      isValid: { type: Boolean, required: true }, // refresh token is valid or not
      deviceId: String
    },
    {
      validateBeforeSave: false,
      timestamps: true
    }
  );

  // This is necessary to avoid model compilation errors in watch mode
  // see https://mongoosejs.com/docs/api/connection.html#connection_Connection-deleteModel
  if (mongooseClient.modelNames().includes(modelName)) {
    mongooseClient.deleteModel(modelName);
  }
  return mongooseClient.model(modelName, schema);
}

model file for sequelize:

// See http://docs.sequelizejs.com/en/latest/docs/models-definition/
// for more of what you can do here.
import { Sequelize, DataTypes } from 'sequelize';
import { Application } from '../declarations';

export default function (app: Application) {
  const sequelizeClient: Sequelize = app.get('sequelizeClient');
  const refreshTokens = sequelizeClient.define(
    'refresh_tokens',
    {
      userId: {
        type: DataTypes.STRING,
        allowNull: false
      },
      refreshToken: {
        type: DataTypes.TEXT,
        allowNull: false
      },
      deviceId: {
        type: DataTypes.STRING,
        allowNull: true
      },
      isValid: {
        type: DataTypes.BOOLEAN,
        allowNull: false
      }
    },
    {
      hooks: {
        beforeCount(options: any) {
          options.raw = true;
        }
      }
    }
  );

  // eslint-disable-next-line no-unused-vars
  (refreshTokens as any).associate = function (models: any) {
    // Define associations here
    // See http://docs.sequelizejs.com/en/latest/docs/associations/
  };

  return refreshTokens;
}

Add issueRefreshToken to Feathers Authentication after create hook

authentication.ts

export default function (app: Application) {
  const authentication = new AuthenticationService(app);

  authentication.register('jwt', new MyJwtStrategy());
  authentication.register('local', new LocalStrategy());

  app.use('/authentication', authentication);
  app.service('authentication').hooks({
    after: {
      create: [issueRefreshToken()]
    }
  });
  app.configure(expressOauth());
}

Update refresh-tokens.hooks.ts to add refreshAccessToken, revokeRefreshToken and logoutUser hooks

refresh-tokens.hooks.ts

export default {
  before: {
    all: [],
    find: [],
    get: [],
    create: [refreshAccessToken()],
    update: [],
    patch: [authenticate('jwt'), revokeRefreshToken()],
    remove: [authenticate('jwt'), logoutUser()]
  },

  after: {
    all: [],
    find: [],
    get: [],
    create: [],
    update: [],
    patch: [],
    remove: [logoutUser()]
  },

  error: {
    all: [],
    find: [],
    get: [],
    create: [],
    update: [],
    patch: [],
    remove: []
  }
};

Examples

Authenticate user with local or oAuth strategies, to support multi-devices login, client must provide "deviceId" in authentication request. After authenticated successfully, client must save the user Id and refresh token in secure local storage for future use

Authentication request:

POST http://localhost:3030/authentication
Content-Type: application/json

{
  "strategy": "local",
  "email": "[email protected]",
  "password": "a",
  "deviceId": "device1"
}

Authentication response:

HTTP/1.1 201 Created
{
  "accessToken": "...JWT...",
  "authentication": {
    "strategy": "local"
  },
  "user": {
    "strategy": "local",
    "email": "[email protected]",
    "_id": "user ID"
  },
  "refreshToken": "...JWT..."
}

After access token expiration, make a POST request to /refresh-tokens endpoint along with userID and refresh token to get a new access token

POST http://localhost:3030/refresh-tokens
Content-Type: application/json

{
  "_id": "user ID",
  "refreshToken": <refresh_token>
}

response:

HTTP/1.1 201 Created
{
  "accessToken": "new access_token"
}

To revoke refresh-token, make a PATCH request to /refresh-tokens endpoint. Authorization header should be set as it is required a protected endpoint

PATCH http://localhost:3030/refresh-tokens
Content-Type: application/json
Authorization: <access_token>

{
  "refreshToken": <refresh_token>
}

To logout user, client makes a DELETE request to /refresh-tokens/userID endpoint. Same as revokeRefreshToken, DELETE request is protected, client needs to set the Authorization header to access it

DELETE http://localhost:3030/refresh-tokens?refreshToken=<refresh_token>
Authorization: <access_token>

Change-log

0.2.0 - Add revokeRefreshToken hook, unit testing, support deviceId for multiple device login and update utility funtions
0.1.0 - Simply and align refresh-token config options with existing authentication options; update typescript typing
0.0.6 - initial release