Forked from TheSinding/authentication-refresh-token There are three major differences of my implementation:
- Implement refresh token via Feathers standalone service
- The form of refresh token is actual JWT
- Support all authentication strategies (local, oAuth)
- Support multi-devices login
Leveraging existing Feathers built-in authentication service and JWT support to implement refresh token functionalities via couple hooks:
- issueRefreshToken - issuing refresh token after user authenticated successfully and save it via custom refresh-tokens service
- refreshAccessToken - issuing new access token by making a POST request to /refresh-tokens endpoint along with user Id and a valid refresh token
- revokeRefreshToken - revoke refresh token by making PATCH request to /refresh-tokens endpoint
- logoutUser - remove the refresh token by making a DELETE request to /refresh-tokens endpoint
- Create a Feathers App (
feathers generate app
) - Authentication should be enable, authentication strategies and user entity service is setup properly
- Import feathers-refresh-token
- Add a custom service (
feathers generate service
) - Add refresh-token config in default.json
- Add hooks to authentication service and customer service created on step 4
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;
}
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());
}
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: []
}
};
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": "test@test.com",
"password": "a",
"deviceId": "device1"
}
Authentication response:
HTTP/1.1 201 Created
{
"accessToken": "...JWT...",
"authentication": {
"strategy": "local"
},
"user": {
"strategy": "local",
"email": "test@test.com",
"_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>
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