supertokens-node-mysql-ref-jwt-webservice

This library implements a http service using which you can easily implement user session management for websites. For a complete solution, you also need to use the supertokens-website package on your frontend.

Usage no npm install needed!

<script type="module">
  import supertokensNodeMysqlRefJwtWebservice from 'https://cdn.skypack.dev/supertokens-node-mysql-ref-jwt-webservice';
</script>

README

SuperTokens banner

License: MIT

This library implements a http service using which you can easily implement user session management for websites. For a complete solution, you also need to use the supertokens-website package on your frontend.

If you use NodeJS in your tech stack, please checkout supertokens-node-mysql-ref-jwt instead. This library is more convenient to use, but works only if you use NodeJS for you APIs

The protocol SuperTokens uses is described in detail in this article

The library has the following features:

  • It uses short-lived access tokens (JWT) and long-lived refresh tokens (Opaque).
  • Interact using http API.
  • Token theft detection: SuperTokens is able to detect token theft in a robust manner. Please see the article mentioned above for details on how this works.
  • Complete auth token management - It only stores the hashed version of refresh tokens in the database, so even if someone (an attacker or an employee) gets access to the table containing them, they would not be able to hijack any session. Furthermore, all sensitive tokens have a long length and high entropy - so brute force attack is out of the question.
  • Automatic JWT signing key generation (if you don't provide one), management and rotation - Periodic changing of this key enables maximum security as you don't have to worry much in the event that this key is compromised. Also note that doing this change will not log any user out :grinning:
  • Efficient in terms of space complexity - Needs to store just one row in a SQL table per logged in user per device.
  • Efficient in terms of time complexity - Minimises the number of DB lookups (most requests do not need a database call to authenticate at all!)
  • Built-in support for handling multiple devices per user.
  • Built-in synchronisation in case you are running multiple servers.
  • Easy to use, with well documented, modularised code and helpful error messages!
  • Using this library, you can keep a user logged in for however long you want - without worrying about any security consequences.

If you like this project and want to use it, but using a different tech stack:

  • Please contact us at team@supertokens.io and we will evaluate building a solution for your tech stack. This is on a first come, first serve basis.

Index

  1. Installation
  2. Accompanying library
  3. Usage
  4. Demo
  5. Making changes
  6. Future work
  7. Support, questions and bugs
  8. Further reading and understanding
  9. Authors

Installation

npm i --save supertokens-node-mysql-ref-jwt-webservice

Before you start using the service:

You will need to install MySQL and NodeJS in your system.

You will need to create a database in MySQL to store session info. This database can be either your already created DB or a new DB. This database name should be given as a config param to the library (See config section below).

There will be two tables created automatically for you in the provided database when you first use this library - if they don't already exist. If you want to create them yourself, you can do so with the following commands:

CREATE TABLE signing_key (
  key_name VARCHAR(128),
  key_value VARCHAR(255),
  created_at_time BIGINT UNSIGNED,
  PRIMARY KEY(key_name)
);

CREATE TABLE refresh_tokens (
  session_handle_hash_1 VARCHAR(255) NOT NULL,
  user_id VARCHAR(128) NOT NULL,
  refresh_token_hash_2 VARCHAR(128) NOT NULL,
  session_info TEXT,
  expires_at BIGINT UNSIGNED NOT NULL,
  jwt_user_payload TEXT,
  PRIMARY KEY(session_handle_hash_1)
);    

You can name these tables whatever you want, but be sure to send those to the library via the config params (see below).

Please make sure this service is only accessible by your systems and not by anyone outside.

Accompanying library

As of now, this service is designed to work if your frontend is a website. To use this service, you will also need to use the supertokens-website in your frontend code. This library is a drop-in replacement for your axios/ajax calls on the frontend.

Together this service and the supertokens-website library take into account all the failures and race conditions that can possibly occur when implementing session management.

Usage

To start the server, go into the directory of this project and run:

node index.js <path to config.json file>

You can also use pm2 to run this service in a production setting. But be sure to pass a config.json file path to the node process.

To see the various config options, please click here

HTTP API - all inputs and outputs are in JSON format.

In the responses for the APIs below, I will be writing ... OR objA OR objB OR ...
This means the response will either be objA or objB, but not both.

Create a new session

API: /session
METHOD: POST

Input: {
  userId: string, // unique ID for this user
  jwtPayload: any, // any js object, array, or primitive type. Can also be undefined
  sessionData: any  // any js object, array, or primitive type. Can also be undefined
}

Output:
status code: 200
{
  message: string,
  status: "OK",  // to understand the meaning of these status values, please see below
  session: {
    handle: string,
    userId: string,
    jwtPayload: any
  },
  accessToken: { value: string, expires: number }, // to be put in cookies as HttpOnly, secure and for all API paths
  refreshToken: { value: string, expires: number }, // to be put in cookies as HttpOnly, secure and only for refresh API path
  idRefreshToken: { value: string, expires: number }, // to be put in cookies as NOT HttpOnly, NOT secure and for all API paths
}

status code: 500 // something went wrong in the server
{ message: string }

status code: 400 // bad request
{ message: string }

Get information from a session

API: /session
METHOD: PUT // this API may have a side effect of changing the access token. Hence it is PUT and not GET

Input: {
  idRefreshToken: string OR undefined, // if this is undefined, then this API is going to throw UNAUTHORISED error anyways
  accessToken: string,
}

Output:
status code: 200
{
  message: string,
  status: "OK",
  session: {
    handle: string,
    userId: string,
    jwtPayload: any
  },
  newAccessToken: { value: string, expires: number } OR undefined,  // if this is not undefined, replace the current access token with this new one
} OR {
  message: string,
  status: "UNAUTHORISED" OR "TRY_REFRESH_TOKEN"  // to understand the implications of these status codes, please see below.
}

status code: 500 // something went wrong in the server
{ message: string }

status code: 400 // bad request
{ message: string }

Refresh a session

API: /refresh
METHOD: PUT

Input: {
  idRefreshToken: string OR undefined, // if this is undefined, then this API is going to throw UNAUTHORISED error anyways
  refreshToken: string,
}

Output:
status code: 200
{
  message: string,
  status: "OK",
  session: {
    handle: string,
    userId: string,
    jwtPayload: any
  },
  newAccessToken: { value: string, expires: number }, // replace existing access token with this
  newRefreshToken: { value: string, expires: number }, // replace existing refresh token with this
  newIdRefreshToken: { value: string, expires: number },  // replace existing idRefreshToken with this
} OR {
  message: string,
  status: "UNAUTHORISED"
  sessionTheftDetected: { 
    value: false 
  } OR {
    value: true,   // here you can use the handle or the userId to destroy either this session, or all sessions belonging to this user
    session: {
      handle: string,
      userId: string
    }
  }
}

status code: 500 // something went wrong in the server
{ message: string }

status code: 400 // bad request
{ message: string }

Delete a session using the session handle

API: /session
METHOD: DELETE

Input: {
  sessionHandle: string
}

Output:
status code: 200
{
  message: string,
  status: "OK",
  deletedAnyEntry: boolean  // will be false only if this session was missing in the db. This probably means it was removed earlier. In this case, you may not want to clear cookies for the client as they may have another live session already.
}

status code: 500 // something went wrong in the server
{ message: string }

status code: 400 // bad request
{ message: string }

Delete all sessions belonging to a user

API: /session/all
METHOD: DELETE

Input: {
  userId: string
}

Output:
status code: 200
{
  message: string,
  status: "OK"
}

status code: 500 // something went wrong in the server
{ message: string }

status code: 400 // bad request
{ message: string }

Get session data

This API does not synchronise with other processes calling this or the update session data API.

API: /session/data
METHOD: GET

Input: {
  sessionHandle: string
}

Output:
status code: 200
{
  message: string,
  status: "OK",
  sessionData: any
} OR {
  message: string,
  status: "UNAUTHORISED"
}

status code: 500 // something went wrong in the server
{ message: string }

status code: 400 // bad request
{ message: string }

Update session data

This API does not synchronise with other processes calling this or the update session data API.

API: /session/data
METHOD: PUT

Input: {
  sessionHandle: string,
  sessionData: any
}

Output:
status code: 200
{
  message: string,
  status: "OK" OR "UNAUTHORISED"
}

status code: 500 // something went wrong in the server
{ message: string }

status code: 400 // bad request
{ message: string }

Error Status Values

UNAUTHORISED

This is returned when the given session has expired. This can happen if the session does not exist in the DB, or the refresh token given has expired or is invalid. In this situation, you can clear the auth cookies for the client.

TRY_REFRESH_TOKEN

This is returned when the given access token is invalid or expired. This means that the session could still be alive, but we do not know yet. When this is returned, your frontend should query your refresh session API which should query the /refresh PUT API of this service. If you are using supertokens-website on your frontend, then returning the status code that corresponds with session expiry will take care of this automatically. Also, you do not need to do anything with the cookies when this is returned.

Config

The config file has to a be valid JSON file. It should have the following structure:

NOTE: If you do not provide a signingKey, it will create one for you and you can also configure it so that it changes after a fixed amount of time (for maximum security). The changing of the key will not log any user out.
{
    mysql: {
        host?: string,  // default localhost
        port?: number, // default 3306
        user: string, // If the tables in the database are not created already, then this user must have permission to create tables.
        password: string,
        connectionLimit?: number, // default 50
        database: string, // name of the database to connect to. This must be created before running this package
        tables?: {
            signingKey?: string, // default signing_key - table name used to store secret keys
            refreshTokens?: string // default refresh_token - table name used to store sessions
        }
    },
    tokens?: {
        accessToken?: {
            signingKey?: {
                dynamic?: boolean, // default true - if this is true, then the JWT signing key will change automatically every updateInterval hours.
                updateInterval?: number, // in hours - default 24 - should be >= 1 && <= 720. How often to change the signing key 
                keyPath?: string // default undefined - If you want to give your own JWT signing key, please give the path here. If this is given, then the dynamic boolean will be ignored as key management will be up to you. The path should be either absolute, or relative to the folder you ran the node command in
            },
            validity?: number, // in seconds, default is 3600 seconds. should be >= 10 && <= 86400000 seconds. This determines the lifetime of an access token.
            blacklisting?: boolean // default is false. If you set this to true, revoking a session will cause immediate invalidation of its access token, regardless of the token's lifetime. But know that this has an adverse effect on time effeciency of each getSession API call.
        },
        refreshToken?: {
            validity?: number, // in hours, default is 2400 (100 days). This determines how long a refresh token is alive for. So if your user is inactive for these many hours, they will be logged out.
            removalCronjobInterval?: string, // in the same style as of crontab, but with an extra seconds field as well. Default is "0 0 0 1-31/7 * *" - every 7th day of the month from 1 through 31. Defines how often the cronjob that removes expired sessions from the db should run.
        }
    },
    port: number // port where to run service.
    host: string // host where to run service.
}

Demo

You can play around with the demo project that uses the library this service is based on and the supertokens-website library. The demo demonstrates how this package behaves when it detects auth token theft (and the best part is that you are the attacker, muahahaha)!

Making changes

This service is written in TypeScript (TS). When you make any changes to the .ts files in the /src/ts/* folder, run the following command in the /src folder to compile to .js:

tsc -p tsconfig.json

If you make any changes to index.ts in the root of this repo, once you compile it to .js, remember to change the import/export path from /src/ts/* to /src/build/* in the .js file.

Future work

  • Enable this to work with mobile apps as well.
  • Add unit testing.
  • To implement info, debug and error logs in a better way.
  • Write instructions on how to install NodeJS and MySQL here, or provide a link
  • Make this into a Docker container

Support, questions and bugs

We are most accessible via team@supertokens.io and via the GitHub issues feature. Please see this for more details

Further reading and understanding

We have written a blog post about sessions in general:

  • Part 1: Introduction to session management, analysis of most commonly used session flows, and best practices
  • Part 2: Analysis of the session flow used by SuperTokens.

To understand the logic behind how sessions are created, managed and destroyed, please refer to the WiKi section in supertokens-node-mysql-ref-jwt

Authors

Created with :heart: by the folks at SuperTokens. We are a startup passionate about security and solving software challenges in a way that's helpful for everyone! Please feel free to give us feedback at team@supertokens.io, until our website is ready :grinning: