Home

Awesome

jToker

Simple, secure user authentication for jQuery

j-toker

npm version bower version Build Status Test Coverage

Features:


Live Demo

This project comes bundled with a test app. You can run the demo locally by following these instructions, or you can use it here in production.

The demo uses React, and the source can be found here.


Table of Contents


About this plugin

This plugin relies on token based authentication. This requires coordination between the client and the server. Diagrams are included to illustrate this relationship.

This plugin was designed to work out of the box with the legendary devise token auth gem, but it's flexible enough to be used in just about any environment.

Oh wait you're using Angular? Use ng-token-auth (AngularJS) or Angular2-Token (Angular2) instead.

About security: read here for more information on securing your token auth system. The devise token auth gem has adequate security measures in place, and this plugin was built to work seamlessly with that gem.


Installation

  1. Download this plugin and its dependencies.

    # using bower:
    bower install j-toker --save
    
    # using npm:
    npm install j-toker --save
    
  2. Ensure that the following dependencies are included:

    These dependencies were pulled down automatically if you used bower or npm to install jToker.

  3. Include jToker in your project.

    • If you're using browserify or similar, this will look like this:

      // this will resolve the dependencies automatically
      var Auth = require('j-toker');
      
    • Otherwise you will need to include jToker and its dependencies manually:

      <!-- in your index.html file -->
      
      <!-- dependencies - these should come BEFORE jToker -->
      <script src='/js/jquery/dist/jquery.js'></script>
      <script src='/js/jquery.cookie/jquery.cookie.js'></script>
      <script src='/js/jquery-deparam/jquery-deparam.js'></script>
      <script src='/js/pubsub-js/src/pubsub.js'></script>
      
      <!-- this should come AFTER the preceeding files -->
      <script src='/js/jquery.j-toker.js'></script>
      
      <!-- jToker will now be available at $.auth -->
      

Note: For the rest of this README, I will assume jToker is available at $.auth, where $ stands for jQuery. But when using require, jToker will be available as whatever you name the required module (Auth in the example above).


Configuration

$.auth.configure will need to be called before this plugin can be used.

When this plugin is used with devise token auth, you may only need to set the apiUrl config option.

Simple configuration example:
$.auth.configure({
  apiUrl: 'https://my-api.com/api/v1'
});

That's it! 99% of you are done.

Complete configuration example

// the following configuration shows all of the available options
// and their default settings

$.auth.configure({
  apiUrl:                '/api',
  signOutPath:           '/auth/sign_out',
  emailSignInPath:       '/auth/sign_in',
  emailRegistrationPath: '/auth',
  accountUpdatePath:     '/auth',
  accountDeletePath:     '/auth',
  passwordResetPath:     '/auth/password',
  passwordUpdatePath:    '/auth/password',
  tokenValidationPath:   '/auth/validate_token',
  proxyIf:               function() { return false; },
  proxyUrl:              '/proxy',
  validateOnPageLoad:    false,
  forceHardRedirect:     false,
  storage:               'cookies',
  cookieExpiry:          14,
  cookiePath:            '/',

  passwordResetSuccessUrl: function() {
    return window.location.href;
  },

  confirmationSuccessUrl:  function() {
    return window.location.href;
  },

  tokenFormat: {
    "access-token": "{{ access-token }}",
    "token-type":   "Bearer",
    client:         "{{ client }}",
    expiry:         "{{ expiry }}",
    uid:            "{{ uid }}"
  },

  parseExpiry: function(headers){
    // convert from ruby time (seconds) to js time (millis)
    return (parseInt(headers['expiry'], 10) * 1000) || null;
  },

  handleLoginResponse: function(resp) {
    return resp.data;
  },

  handleAccountUpdateResponse: function(resp) {
    return resp.data;
  },

  handleTokenValidationResponse: function(resp) {
    return resp.data;
  },

  authProviderPaths: {
    github:    '/auth/github',
    facebook:  '/auth/facebook',
    google:    '/auth/google_oauth2'
  }
});

Note: if you're using multiple user types, jump here for more details.

Config options

apiUrl

string

The base route to your api. Each of the following paths will be relative to this URL. Authentication headers will only be added to requests with this value as the base URL.

--

tokenValidationPath

string

Path (relative to apiUrl) to validate authentication tokens. Read more.

--

signOutUrl

string

Path (relative to apiUrl) to de-authenticate the current user. This will destroy the user's token both server-side and client-side.

--

authProviderPaths

object

An object containing paths to auth endpoints. Keys should be the names of the providers, and the values should be the auth paths relative to the apiUrl. Read more.

--

emailRegistrationPath

string

Path (relative to apiUrl) for submitting new email registrations. Read more.

--

accountUpdatePath

string

Path (relative to apiUrl) for submitting account update requests.

--

accountDeletePath

string

Path (relative to apiUrl) for submitting account deletion requests.

--

confirmationSuccessUrl

string or function

The absolute url to which the API should redirect after users visit the link contained in email-registration confirmation emails.

--

emailSignInPath

string

Path (relative to apiUrl) for signing in to an existing email account.

--

passwordResetPath

string

Path (relative to apiUrl) for requesting password reset emails.

--

passwordResetSuccessUrl

string or function

The absolute url to which the API should redirect after users visit the link contained in password-reset confirmation emails.

--

passwordUpdatePath

string

Path (relative to apiUrl) for submitting new passwords for authenticated users.

--

storage

string

The method used to persist tokens between sessions. Allowed values are cookies and localStorage.

--

proxyIf

function

Older browsers have troubel with CORS. Pass a method to this option that will determine whether or not a proxy should be used.

--

proxyUrl

string

If the proxyIf method returns true, this is the URL that will be used in place of apiUrl.

--

tokenFormat

object

A template for authentication tokens. The template will be provided a context with the following keys:

The above strings will be replaced with their corresponding values as found in the response headers.

--

parseExpiry

function

A function that will return the token's expiry from the current headers. null is returned if no expiry is found.

--

handleLoginResponse

function

A function used to identify and return the current user's account info (id, username, etc.) from the response of a successful login request.

--

handleAccountUpdateResponse

function

A function used to identify and return the current user's info (id, username, etc.) from the response of a successful account update request.

--

handleTokenValidationResponse

function

A function used to identify and return the current user's info (id, username, etc.) from the response of a successful token validation request.


Usage

jToker can be used as a jQuery plugin, or as a CommonJS module.

jQuery plugin usage:
$.auth.configure({apiUrl: '/api'});
CommonJS usage:
var Auth = require('j-toker');

Auth.configure({apiUrl: '/api'});

The two use-cases are equivalent, but the $.auth format will be used in the following examples for simplicity.

--

API

All of the following methods are asynchronous. Each method returns a jQuery.Deferred() promise that will be resolved upon success, or rejected upon failure.

Deferred usage example:
$.auth
  .oAuthSignIn({provider: 'github'})
  .then(function(user) {
    alert('Welcome ' + user.name + '!');
  })
  .fail(function(resp) {
    alert('Authentication failure: ' + resp.errors.join(' '));
  });

--

$.auth.user

An object representing the current user. In addition to the attributes of your user model, the following additional attributes are available:

--

$.auth.oAuthSignIn

Initiate an OAuth2 authentication.

arguments:

--

$.auth.emailSignUp

Create an account using email for confirmation.

arguments:

--

$.auth.emailSignIn

Authenticate a user that registered via email.

arguments:

--

$.auth.validateToken

Use this method to verify that the current user's access-token exists and is valid.

This method is called automatically after configure to check if returning users' sessions are still valid.

The promise returned by this method can be used to redirect users to the login screen when they try to visit restricted areas.

Redirection example with react and react-router
// this assumes that there is an available route named 'login'

var React = require('react'),
    Router = require('react-router'),
    Transition = Router.Transition,
    Auth = require('j-toker');

var PageComponent = React.createClass({
  getInitialState: function() {
    return {
      username: ''
    };
  },

  componentDidMount: function() {
    Auth.validateToken()
      .then(function(user) {
        this.setState({
          username: user.username
        })
      }.bind(this))
      .fail(function() {
        Transition.redirect('login');
      });
  },

  render: function() {
    return (
      <p>Welcome {this.state.username}!</p>
    );
  }
});

--

$.auth.updateAccount

Update the current user's account info. This method accepts an object that should contain valid attributes and values for the current user's model.

$.auth.updateAccount({
  favorite_book: 'Molloy'
});

--

$.auth.requestPasswordReset

Send password reset instructions to a user that was registered by email.

arguments:
$.auth.requestPasswordReset({email: 'test@test.com'});

--

$.auth.updatePassword

Change the current user's password. This only applies to users that were registered by email.

arguments:
$.auth.updatePassword({
  password: '*****',
  password_confirmation: '*****'
});

--

$.auth.signOut

De-authenticates the current user. This will destroy the current user's client-side and server-side auth credentials.

$.auth.signOut();

--

$.auth.destroyAccount

Destroy the current user's account.

$.auth.destroyAccount();

Events

If the PubSubJS library is included, the following events will be broadcast.

A nice feature of PubSubJS is event namespacing (see "topics" in the docs). So you can use a single subscription to listen for any changes to the $.auth.user object, and then propgate the changes to any related UI components.

event example using React and PubSubJS:
var React = require('react'),
    PubSub = require('pubsub-js'),
    Auth = require('j-toker');

var App = React.createClass({

  getInitialState: function() {
    return {
      user: Auth.user
    };
  },

  // update the user object on all auth-related events
  componentWillMount: function() {
    PubSub.subscribe('auth', function() {
      this.setState({user: Auth.user});
    }.bind(this));
  },

  // ...

});

--

auth.validation.success

Broadcast after successful user authentication. Event message contains the user object.

Broadcast after:
Example:
PubSub.subscribe('auth.validation.success', function(ev, user) {
  alert('Welcome' + user.name + '!');
});

--

auth.validation.error

Broadcast after failed user authentication. Event message contains errors related to the failure.

Broadcast after:
Example:
PubSub.subscribe('auth.validation.error', function(ev, err) {
  alert('Validation failure.');
});

--

auth.emailRegistration.success

Broadcast after email sign up request was successfully completed.

Broadcast after:
PubSub.subscribe('auth.emailRegistration.success', function(ev, msg) {
  alert('Check your email!');
});

--

auth.emailRegistration.error

Broadcast after email sign up requests fail.

Broadcast after:
Example:
PubSub.subscribe('auth.emailRegistration.error', function(ev, msg) {
  alert('There was a error submitting your request. Please try again!');
});

--

auth.passwordResetRequest.success

Broadcast after password reset requests complete successfully.

Broadcast after:
Example:
PubSub.subscribe('auth.passwordResetRequest.success', function(ev, msg) {
  alert('Check your email!');
});

--

auth.passwordResetRequest.error

Broadcast after password reset requests fail.

Broadcast after:
Example:
PubSub.subscribe('auth.passwordResetRequest.error', function(ev, msg) {
  alert('There was an error submitting your request. Please try again!');
});

--

auth.emailConfirmation.success

Broadcast upon visiting the link contained in a registration confirmation email if the subsequent validation succeeds.

Broadcast after:
Example:
PubSub.subscribe('auth.emailConfirmation.success', function(ev, msg) {
  alert('Welcome' + $.auth.user.name + '!');
});

--

auth.emailConfirmation.error

Broadcast upon visiting the link contained in a registration confirmation email if the subsequent validation fails.

Broadcast after:
Example:
PubSub.subscribe('auth.passwordResetRequest.error', function(ev, msg) {
  alert('There was an error authenticating your new account!');
});

--

auth.passwordResetConfirm.success

Broadcast upon visiting the link contained in a password reset confirmation email if the subsequent validation succeeds.

Broadcast after:
Example:
PubSub.subscribe('auth.emailConfirmation.success', function(ev, msg) {
  alert('Welcome' + $.auth.user.name + '! Change your password!');
});

--

auth.passwordResetConfirm.error

Broadcast upon visiting the link contained in a password reset confirmation email if the subsequent validation fails.

Broadcast after:
Example:
PubSub.subscribe('auth.passwordResetRequest.error', function(ev, msg) {
  alert('There was an error authenticating your account!');
});

--

auth.emailSignIn.success

Broadcast after a user successfully completes authentication using an email account.

Broadcast after:
Example:
PubSub.subscribe('auth.emailSignIn.success', function(ev, msg) {
  alert('Welcome' + $.auth.user.name + '! Change your password!');
});

--

auth.emailSignIn.error

Broadcast after a user fails to authenticate using their email account.

Broadcast after:
Example:
PubSub.subscribe('auth.emailSignIn.error', function(ev, msg) {
  alert('There was an error authenticating your account!');
});

--

auth.oAuthSignIn.success

Broadcast after a user successfully authenticates with an OAuth2 provider.

Broadcast after:

#####Example:

PubSub.subscribe('auth.oAuthSignIn.success', function(ev, msg) {
  alert('Welcome' + $.auth.user.name + '!');
});

--

auth.oAuthSignIn.error

Broadcast after a user fails to authenticate using an OAuth2 provider.

Broadcast after:
Example:
PubSub.subscribe('auth.oAuthSignIn.error', function(ev, msg) {
  alert('There was an error authenticating your account!');
});

--

auth.signIn.success

Broadcast after a user successfully signs in using either email or OAuth2 authentication.

Broadcast after:
Example:
PubSub.subscribe('auth.oAuthSignIn.success', function(ev, msg) {
  alert('Welcome' + $.auth.user.name + '!');
});

--

auth.signIn.error

Broadcast after a user fails to sign in using either email or OAuth2 authentication.

Broadcast after:
Example:
PubSub.subscribe('auth.signIn.error', function(ev, msg) {
  alert('There was an error authenticating your account!');
});

--

auth.signOut.success

Broadcast after a user successfully signs out.

Broadcast after:
Example:
PubSub.subscribe('auth.signOut.success', function(ev, msg) {
  alert('Goodbye!');
});

--

auth.signOut.error

Broadcast after a user fails to sign out.

Broadcast after:
PubSub.subscribe('auth.signOut.success', function(ev, msg) {
  alert('There was a problem with your sign out attempt. Please try again!');
});

--

auth.accountUpdate.success

Broadcast after a user successfully updates their account info.

Broadcast after:
Example:
PubSub.subscribe('auth.accountUpdate.success', function(ev, msg) {
  alert('Your account has been updated!');
});

--

auth.accountUpdate.error

Broadcast when an account update request fails.

Broadcast after:
Example:
PubSub.subscribe('auth.accountUpdate.error', function(ev, msg) {
  alert('There was an error while trying to update your account.');
});

--

auth.destroyAccount.success

Broadcast after a user's account has been successfully destroyed.

Broadcast after:
Example:
PubSub.subscribe('auth.destroyAccount.success', function(ev, msg) {
  alert('Goodbye!');
});

--

auth.destroyAccount.error

Broadcast after an attempt to destroy a user's account fails.

Broadcast after:
Example:
PubSub.subscribe('auth.destroyAccount.error', function(ev, msg) {
  alert('There was an error while trying to destroy your account.');
});

--

auth.passwordUpdate.success

Broadcast after a user successfully changes their password.

Broadcast after:
Example:
PubSub.subscribe('auth.passwordUpdate.success', function(ev, msg) {
  alert('Your password has been changed!');
});

--

auth.passwordUpdate.error

Broadcast after an attempt to change a user's password fails.

Broadcast after:
Example:
PubSub.subscribe('auth.passwordUpdate.error', function(ev, msg) {
  alert('There was an error while trying to change your password.');
});

Possible complications

This plugin uses the global jQuery beforeSend callback to append authentication headers to AJAX requests. This may conflict with your own use of the callback. In that case, just call the $.auth.appendAuthHeaders method during your own callback.

Custom beforeSend usage example
$.ajaxSetup({
  beforeSend: function(xhr, settings) {
    // append outbound auth headers
    $.auth.appendAuthHeaders(xhr, settings);

    // now do whatever you want
  }
});

Alternate response formats

By default, this plugin expects user info (id, name, etc.) to be contained within the data param of successful login / token-validation responses. The following example shows an example of an expected response:

Expected API login response example
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
{
  "data": {
    "id":"123",
    "name": "Slemp Diggler",
    "etc": "..."
  }
}

The above example follows the format used by the devise token gem. Usage with APIs following this format will require no additional configuration.

But not all APIs use this format. Some APIs simply return the serialized user model with no container params:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
{
  "id":"123",
  "name": "Slemp Diggler",
  "etc": "..."
}

Functions can be provided to identify and return the relevant user data from successful authentication responses. The above example response can be handled with the following configuration:

Example configuration when using alternate login response
$.auth.configure({
  apiUrl: 'http://api.example.com'

  handleLoginResponse: function(response) {
    return response;
  },

  handleAccountUpdateResponse: function(response) {
    return response;
  },

  handleTokenValidationResponse: function(response) {
    return response;
  }
});

Multiple user types

View live multi-user demo

This plugin allows for the use of multiple user authentication configurations. The following example assumes that the API supports two user classes, User and EvilUser. The following examples assume that User authentication routes are mounted at /auth, and the EvilUser authentication routes are mounted at evil_user_auth.

Example configuration for multiple user types
Auth.configure([
  {
    default: {
      apiUrl: '//devise-token-auth.dev',
      proxyIf: function() { return window.oldIE();}
    }
  }, {
    evilUser: {
      apiUrl:                  '//devise-token-auth.dev',
      proxyIf:                 function() { return window.isOldIE(); },
      signOutUrl:              '/evil_user_auth/sign_out',
      emailSignInPath:         '/evil_user_auth/sign_in',
      emailRegistrationPath:   '/evil_user_auth',
      accountUpdatePath:       '/evil_user_auth',
      accountDeletePath:       '/evil_user_auth',
      passwordResetPath:       '/evil_user_auth/password',
      passwordUpdatePath:      '/evil_user_auth/password',
      tokenValidationPath:     '/evil_user_auth/validate_token',
      authProviderPaths: {
        github:    '/evil_user_auth/github',
        facebook:  '/evil_user_auth/facebook',
        google:    '/evil_user_auth/google_oauth2'
      }
    }
  }
]);

Multiple user type usage

The following API methods accept a config option that can be used to specify the desired configuration.

All other methods ($.auth.signOut, $.auth.updateAccount, etc.) derive the configuration type from the current signed-in user.

The first available configuration will be used if none is provided (default in the example above).

Conceptual

The following diagrams illustrate the authentication processes used by this plugin.

OAuth2 Sign In

The following diagram illustrates the steps necessary to authenticate a client using an oauth2 provider.

OAuth2 Flow

When authenticating with a 3rd party provider, the following steps will take place.

  1. An external window will be opened to the provider's authentication page.
  2. Once the user signs in, they will be redirected back to the API at the callback uri that was registered with the oauth2 provider.
  3. The API will send the user's info back to the client via postMessage event, and then close the external window.

The postMessage event must include the following a parameters:

Example redirect_uri destination:
<!DOCTYPE html>
<html>
  <head>
    <script>
      window.addEventListener("message", function(ev) {

        // this page must respond to "requestCredentials"
        if (ev.data === "requestCredentials") {

          ev.source.postMessage({
             message: "deliverCredentials", // required
             auth_token: 'xxxx', // required
             uid: 'yyyy', // required

             // additional params will be added to the user object
             name: 'Slemp Diggler'
             // etc.

          }, '*');

          // close window after message is sent
          window.close();
        }
      });
    </script>
  </head>
  <body>
    <pre>
      Redirecting...
    </pre>
  </body>
</html>

Token validation

The client's tokens are stored in cookies using the jquery-cookie plugin, or localStorage if configured. This is done so that users won't need to re-authenticate each time they return to the site or refresh the page.

Token validation

Email registration

This plugin also provides support for email registration. The following diagram illustrates this process.

Email registration

Email sign in

Email authentication

Password reset

The password reset flow is similar to the email registration flow.

Password reset

When the user visits the link contained in the resulting email, they will be authenticated for a single session. An event will be broadcast that can be used to prompt the user to update their password. See the auth.passwordResetConfirm.success event for details.

Token management

Tokens should be invalidated after each request to the API. The following diagram illustrates this concept:

Token handling

During each request, a new token is generated. The access-token header that should be used in the next request is returned in the access-token header of the response to the previous request. The last request in the diagram fails because it tries to use a token that was invalidated by the previous request.

The benefit of this measure is that if a user's token is compromised, the user will immediately be forced to re-authenticate. This will invalidate the token that is now in use by the attacker.

The only case where an expired token is allowed is during batch requests.

Token management is handled by default when using this plugin with the devise token auth gem.

Batch requests

By default, the API should update the auth token for each request (read more. But sometimes it's neccessary to make several concurrent requests to the API, for example:

Example batch request
$.getJSON('/api/restricted_resource_1').success(function(resp) {
  // handle response
});

$.getJSON('/api/restricted_resource_2').success(function(resp) {
  // handle response
});

In this case, it's impossible to update the access-token header for the second request with the access-token header of the first response because the second request will begin before the first one is complete. The server must allow these batches of concurrent requests to share the same auth token. This diagram illustrates how batch requests are identified by the server:

Batch request overview

The "5 second" buffer in the diagram is the default used by the devise token auth gem.

The following diagram details the relationship between the client, server, and access tokens used over time when dealing with batch requests:

Batch request overview cont

Note that when the server identifies that a request is part of a batch request, the user's auth token is not updated. The auth token will be updated for the first request in the batch, and then that same token will be returned in the responses for each subsequent request in the batch (as shown in the diagram).

The devise token auth gem automatically manages batch requests, and it provides settings to fine-tune how batch request groups are identified.

Identifying users on the server.

The user's authentication information is included by the client in the access-token header of each request. If you're using the devise token auth gem, the header must follow the RFC 6750 Bearer Token format:

"access-token": "wwwww",
"token-type":   "Bearer",
"client":       "xxxxx",
"expiry":       "yyyyy",
"uid":          "zzzzz"

Replace xxxxx with the user's auth_token and zzzzz with the user's uid. The client field exists to allow for multiple simultaneous sessions per user. The client field defaults to default if omitted. expiry is used by the client to invalidate expired tokens without making an API request. A more in depth explanation of these values is here.

This will all happen automatically when using this plugin.

Note: You can customize the auth headers however you like. Read more.


Internet Explorer

Internet Explorer (8, 9, 10, & 11) present the following obstacles:

The following measures are necessary when dealing with these older browsers.

AJAX cache must be disabled for IE8 + IE9

IE8 + IE9 will try to cache ajax requests. This results in an issue where the request return 304 status with Content-Type set to html and everything goes haywire.

The solution to this problem is to set the If-Modified-Since headers to '0' on each of the request methods that we use in our app. This is done by default when using this plugin.

The solution was lifted from this stackoverflow post.

IE8 and IE9 must proxy CORS requests

You will need to set up an API proxy if the following conditions are both true:

Example proxy using express for node.js
var express   = require('express');
var request   = require('request');
var httpProxy = require('http-proxy');
var CONFIG    = require('config');

// proxy api requests (for older IE browsers)
app.all('/proxy/*', function(req, res, next) {
  // transform request URL into remote URL
  var apiUrl = 'http:'+CONFIG.API_URL+req.params[0];
  var r = null;

  // preserve GET params
  if (req._parsedUrl.search) {
    apiUrl += req._parsedUrl.search;
  }

  // handle POST / PUT
  if (req.method === 'POST' || req.method === 'PUT') {
    r = request[req.method.toLowerCase()]({
      uri: apiUrl,
      json: req.body
    });
  } else {
    r = request(apiUrl);
  }

  // pipe request to remote API
  req.pipe(r).pipe(res);
});

The above example assumes that you're using express, request, and http-proxy, and that you have set the API_URL value using node-config.

IE8-11 must use hard redirects for provider authentication

Most modern browsers can communicate across tabs and windows using postMessage. This doesn't work for certain browsers (IE8-11). In these cases the client must take the following steps when performing provider authentication (facebook, github, etc.):

  1. navigate from the client site to the API
  2. navigate from the API to the provider
  3. navigate from the provider to the API
  4. navigate from the API back to the client

These steps are taken automatically when using this plugin with IE8+.


Contributing

  1. Create a feature branch with your changes.
  2. Write some test cases.
  3. Make all the tests pass.
  4. Issue a pull request.

I will grant you commit access if you send quality pull requests.

Development

Running the dev server

There is a test project in the demo directory of this app. To start a dev server, perform the following steps.

  1. cd to the root of this project.
  2. yarn
  3. grunt serve

A dev server will start on localhost:7777. The test suite will be run as well.

Running the tests

If you just want to run the tests, follow these steps:

  1. cd into the root of this project
  2. yarn
  3. grunt

Testing against a live API

This plugin was built against this API. You can use this, or feel free to use your own.


Credits

Thanks to the following contributors:

Code and ideas were stolen from the following sources:


License

WTFPL © Lynn Dylan Hurley