Simple, secure token authentication for jQuery.
npm install j-toker![j-toker][logo]




* OAuth2 authentication
* Email authentication, including:
* User registration
* Password resets
* Account updates
* Account deletion
* Seamless integration with the [devise token auth][dta] Rails gem.
* Extensive event notifications
* Allows for total configuration to work with any API
* Session support using cookies or localStorage
* Tested with Chrome, Safari, Firefox and IE8+
---
This project comes bundled with a test app. You can run the demo locally by following these instructions, or you can use it [here][j-toker-demo] in production.
The demo uses [React][react], and the source can be found here.
---
* About this plugin
* Installation
* Configuration
* API
* $.auth.user
* $.auth.oAuthSignIn
* $.auth.emailSignUp
* $.auth.emailSignIn
* $.auth.validateToken
* $.auth.updateAccount
* $.auth.requestPasswordReset
* $.auth.updatePassword
* $.auth.signOut
* $.auth.destroyAccount
* Events
* Possible complications
* Using alternate response formats
* Using multiple user types
* Conceptual diagrams
* OAuth2 Authentication
* Token Validation
* Email Registration
* Email Sign In
* Password Reset Requests
* Notes on Token Management
* Notes on Batch Requests
* Token Formatting
* Internet Explorer Caveats
* Contributing
* Development
* Callouts
---
This plugin relies on [token based authentication][token-auth-wiki]. 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][dta] gem, but it's flexible enough to be used in just about any environment.
Oh wait you're using [AngularJS][angular]? Use [ng-token-auth][ng-token-auth] instead.
About security: [read here][so-post] for more information on securing your token auth system. The [devise token auth][dta] gem has adequate security measures in place, and this plugin was built to work seamlessly with that gem.
---
1. Download this plugin and its dependencies.
~~~sh
# using bower:
bower install j-toker --save
# using npm:
npm install j-toker --save
~~~
2. Ensure that the following dependencies are included:
* [jquery][jquery]: AJAX requests
* [jquery-cookie][jquery-cookie]: Persist data through browser sessions
* [jquery-deparam][jquery-deparam]: Querystring param deconstruction.
* [PubSubJS][pubsub-js]: (optional) Event publish / subscribe.
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][browserify] or similar, this will look like this:
~~~javascript
// this will resolve the dependencies automatically
var Auth = require('j-toker');
~~~
* Otherwise you will need to include jToker and its dependencies manually:
~~~html
~~~
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).
---
$.auth.configure will need to be called before this plugin can be used.
When this plugin is used with [devise token auth][dta], you may only need to set the apiUrl config option.
##### Simple configuration example:
~~~javascript
$.auth.configure({
apiUrl: 'https://my-api.com/api/v1'
});
~~~
That's it! 99% of you are done.
##### Complete configuration example
~~~javascript
// 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.
--
apiUrl) to validate authentication tokens. Read more.--
apiUrl) to de-authenticate the current user. This will destroy the user's token both server-side and client-side.--
apiUrl. Read more.--
apiUrl) for submitting new email registrations. Read more.--
apiUrl) for submitting account update requests.--
apiUrl) for submitting account deletion requests.--
--
apiUrl) for signing in to an existing email account.--
apiUrl) for requesting password reset emails.--
--
apiUrl) for submitting new passwords for authenticated users.--
cookies and localStorage.--
--
proxyIf method returns true, this is the URL that will be used in place of apiUrl.--
* access-token
* client
* uid
* expiry
The above strings will be replaced with their corresponding values as found in the response headers.
--
null is returned if no expiry is found.--
id, username, etc.) from the response of a successful login request.--
id, username, etc.) from the response of a successful account update request.--
id, username, etc.) from the response of a successful token validation request.---
jToker can be used as a [jQuery][jquery] plugin, or as a [CommonJS][common-js] module.
##### jQuery plugin usage:
~~~javascript
$.auth.configure({apiUrl: '/api'});
~~~
##### CommonJS usage:
~~~javascript
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.
--
All of the following methods are asynchronous. Each method returns a [jQuery.Deferred() promise][dfd] that will be resolved upon success, or rejected upon failure.
##### Deferred usage example:
~~~javascript
$.auth
.oAuthSignIn({provider: 'github'})
.then(function(user) {
alert('Welcome ' + user.name + '!');
})
.fail(function(resp) {
alert('Authentication failure: ' + resp.errors.join(' '));
});
~~~
--
* signedIn: (boolean) will be true if there is a current user.
* configName: (string) the name of the configuration used to authenticate the current user. When using a single API configuration (most cases), this will be default.
--
##### arguments:
* provider: the name of the target provider service as represented in the authProviderPaths config hash.
~~~javascript
$.auth.authenticate({provider: 'github'});
~~~
* params: an object containing additional fields user attributes and values.
~~~javascript
$.auth.authenticate({
provider: 'github',
params: {
favorite_color: 'flesh'
}
});
~~~
* config: the name of the configuration to be used when multiple auth configurations are available.
~~~javascript
$.auth.authenticate({
provider: 'github',
config: 'altUser',
params: {
favorite_color: 'amaranth'
}
});
~~~
--
##### arguments:
* email
* password
* password_confirmation
~~~javascript
$.auth.emailSignUp({
email: 'test@test.com',
password: '*',
password_confirmation: '*'
});
~~~
* config: the name of the configuration to be used when multiple configurations are available.
~~~javascript
$.auth.emailSignUp({
email: 'test@test.com',
password: '*',
password_confirmation: '*',
config: 'altUser'
});
~~~
* You can also pass any arbitrary attrubute / values to be assigned to the user at account creation:
~~~javascript
$.auth.emailSignUp({
email: 'test@test.com',
password: '*',
password_confirmation: '*',
favorite_color: 'black'
});
~~~
--
##### arguments:
* email
* password
~~~javascript
$.auth.emailSignIn({
email: 'test@test.com',
password: '*'
});
~~~
* config: name of the config to be used when multiple configurations are available.
~~~javascript
$.auth.emailSignIn({
email: 'test@test.com',
password: '*',
config: 'altUser'
});
~~~
--
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
~~~javascript
// 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 (
Welcome {this.state.username}!
--
~~~javascript
$.auth.updateAccount({
favorite_book: 'Molloy'
});
~~~
--
##### arguments:
* email
~~~javascript
$.auth.requestPasswordReset({email: 'test@test.com'});
~~~
--
##### arguments:
* password
* password_confirmation
~~~javascript
$.auth.updatePassword({
password: '*',
password_confirmation: '*'
});
~~~
--
~~~javascript
$.auth.signOut();
~~~
--
~~~javascript
$.auth.destroyAccount();
~~~
If the [PubSubJS][pubsub-js] library is included, the following events will be broadcast.
A nice feature of [PubSubJS][pubsub-js] 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:
~~~javascript
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));
},
// ...
});
~~~
--
##### Broadcast after:
* $.auth.emailSignIn
* $.auth.oAuthSignIn
* $.auth.validateToken
##### Example:
~~~javascript
PubSub.subscribe('auth.validation.success', function(ev, user) {
alert('Welcome' + user.name + '!');
});
~~~
--
##### Broadcast after:
* $.auth.emailSignIn
* $.auth.oAuthSignIn
* $.auth.validateToken
##### Example:
~~~javascript
PubSub.subscribe('auth.validation.error', function(ev, err) {
alert('Validation failure.');
});
~~~
--
##### Broadcast after:
* $.auth.emailSignUp
~~~javascript
PubSub.subscribe('auth.emailRegistration.success', function(ev, msg) {
alert('Check your email!');
});
~~~
--
##### Broadcast after:
* $auth.emailSignUp
##### Example:
~~~javascript
PubSub.subscribe('auth.emailRegistration.error', function(ev, msg) {
alert('There was a error submitting your request. Please try again!');
});
~~~
--
##### Broadcast after:
* $.auth.passwordResetRequest
##### Example:
~~~javascript
PubSub.subscribe('auth.passwordResetRequest.success', function(ev, msg) {
alert('Check your email!');
});
~~~
--
##### Broadcast after:
* $.auth.passwordResetRequest
##### Example:
~~~javascript
PubSub.subscribe('auth.passwordResetRequest.error', function(ev, msg) {
alert('There was an error submitting your request. Please try again!');
});
~~~
--
##### Broadcast after:
* $.auth.validateToken
##### Example:
~~~javascript
PubSub.subscribe('auth.emailConfirmation.success', function(ev, msg) {
alert('Welcome' + $.auth.user.name + '!');
});
~~~
--
##### Broadcast after:
* $.auth.validateToken
##### Example:
~~~javascript
PubSub.subscribe('auth.passwordResetRequest.error', function(ev, msg) {
alert('There was an error authenticating your new account!');
});
~~~
--
##### Broadcast after:
* $.auth.validateToken
##### Example:
~~~javascript
PubSub.subscribe('auth.emailConfirmation.success', function(ev, msg) {
alert('Welcome' + $.auth.user.name + '! Change your password!');
});
~~~
--
##### Broadcast after:
* $.auth.validateToken
##### Example:
~~~javascript
PubSub.subscribe('auth.passwordResetRequest.error', function(ev, msg) {
alert('There was an error authenticating your account!');
});
~~~
--
##### Broadcast after:
* $.auth.emailSignIn
##### Example:
~~~javascript
PubSub.subscribe('auth.emailSignIn.success', function(ev, msg) {
alert('Welcome' + $.auth.user.name + '! Change your password!');
});
~~~
--
##### Broadcast after:
* $.auth.emailSignIn
##### Example:
~~~javascript
PubSub.subscribe('auth.emailSignIn.error', function(ev, msg) {
alert('There was an error authenticating your account!');
});
~~~
--
##### Broadcast after:
* $.auth.oAuthSignIn
#####Example:
~~~javascript
PubSub.subscribe('auth.oAuthSignIn.success', function(ev, msg) {
alert('Welcome' + $.auth.user.name + '!');
});
~~~
--
##### Broadcast after:
* $.auth.oAuthSignIn
##### Example:
~~~javascript
PubSub.subscribe('auth.oAuthSignIn.error', function(ev, msg) {
alert('There was an error authenticating your account!');
});
~~~
--
##### Broadcast after:
* $.auth.emailSignIn
* $.auth.oAuthSignIn
##### Example:
~~~javascript
PubSub.subscribe('auth.oAuthSignIn.success', function(ev, msg) {
alert('Welcome' + $.auth.user.name + '!');
});
~~~
--
##### Broadcast after:
* $.auth.emailSignIn
* $.auth.oAuthSignIn
##### Example:
~~~javascript
PubSub.subscribe('auth.signIn.error', function(ev, msg) {
alert('There was an error authenticating your account!');
});
~~~
--
##### Broadcast after:
* $.auth.signOut
##### Example:
~~~javascript
PubSub.subscribe('auth.signOut.success', function(ev, msg) {
alert('Goodbye!');
});
~~~
--
##### Broadcast after:
* $.auth.signOut
~~~javascript
PubSub.subscribe('auth.signOut.success', function(ev, msg) {
alert('There was a problem with your sign out attempt. Please try again!');
});
~~~
--
##### Broadcast after:
* $.auth.updateAccount
##### Example:
~~~javascript
PubSub.subscribe('auth.accountUpdate.success', function(ev, msg) {
alert('Your account has been updated!');
});
~~~
--
##### Broadcast after:
* $.auth.updateAccount
##### Example:
~~~javascript
PubSub.subscribe('auth.accountUpdate.error', function(ev, msg) {
alert('There was an error while trying to update your account.');
});
~~~
--
##### Broadcast after:
* $.auth.destroyAccount
##### Example:
~~~javascript
PubSub.subscribe('auth.destroyAccount.success', function(ev, msg) {
alert('Goodbye!');
});
~~~
--
##### Broadcast after:
* $.auth.destroyAccount
##### Example:
~~~javascript
PubSub.subscribe('auth.destroyAccount.error', function(ev, msg) {
alert('There was an error while trying to destroy your account.');
});
~~~
--
##### Broadcast after:
* $.auth.updatePassword
##### Example:
~~~javascript
PubSub.subscribe('auth.passwordUpdate.success', function(ev, msg) {
alert('Your password has been changed!');
});
~~~
--
##### Broadcast after:
* $.auth.updatePassword
##### Example:
~~~javascript
PubSub.subscribe('auth.passwordUpdate.error', function(ev, msg) {
alert('There was an error while trying to change your password.');
});
~~~
---
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
~~~javascript
$.ajaxSetup({
beforeSend: function(xhr, settings) {
// append outbound auth headers
$.auth.appendAuthHeaders(xhr, settings);
// now do whatever you want
}
});
~~~
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][dta]. 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
~~~javascript
$.auth.configure({
apiUrl: 'http://api.example.com'
handleLoginResponse: function(response) {
return response;
},
handleAccountUpdateResponse: function(response) {
return response;
},
handleTokenValidationResponse: function(response) {
return response;
}
});
~~~
---
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
~~~javascript
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'
}
}
}
]);
~~~
The following API methods accept a config option that can be used to specify the desired configuration.
* $.auth.oAuthSignIn
* $.auth.validateUser
* $.auth.emailSignUp
* $.auth.emailSignIn
* $.auth.requestPasswordReset
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).
The following diagrams illustrate the authentication processes used by this plugin.
The following diagram illustrates the steps necessary to authenticate a client using an oauth2 provider.
![OAuth2 Flow][o-auth-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:
* message - this must contain the value "deliverCredentials"
* auth-token - a unique token set by your server.
* uid - the id that was returned by the provider. For example, the user's facebook id, twitter id, etc.
##### Example redirect_uri destination:
~~~html
Redirecting...
The client's tokens are stored in cookies using the [jquery-cookie][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][token-validation-flow]
This plugin also provides support for email registration. The following diagram illustrates this process.
![Email registration][email-registration-flow]
![Email authentication][email-sign-in-flow]
The password reset flow is similar to the email registration flow.
![Password reset][password-reset-flow]
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.
Tokens should be invalidated after each request to the API. The following diagram illustrates this concept:
![Token handling][token-handling-diagram]
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][dta] gem.
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
~~~javascript
$.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][batch-request-a]
The "5 second" buffer in the diagram is the default used by the [devise token auth][dta] 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][batch-request-b]
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][dta] gem automatically manages batch requests, and it provides settings to fine-tune how batch request groups are identified.
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 (8, 9, 10, & 11) present the following obstacles:
* IE8 & IE9 don't really support cross origin requests (CORS).
* IE8+ postMessage implementations don't work for our purposes.
* IE8 & IE9 both try to cache ajax requests.
The following measures are necessary when dealing with these older browsers.
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.
You will need to set up an API proxy if the following conditions are both true:
* your API lives on a different domain than your client
* you wish to support IE8 and IE9
##### Example proxy using express for node.js
~~~javascript
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 = requestreq.method.toLowerCase();
} 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.
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
1. navigate from the API to the provider
1. navigate from the provider to the API
1. navigate from the API back to the client
These steps are taken automatically when using this plugin with IE8+.
---
1. Create a feature branch with your changes.
1. Write some test cases.
1. Make all the tests pass.
1. Issue a pull request.
I will grant you commit access if you send quality pull requests.
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.
1. npm install
1. grunt serve
A dev server will start on localhost:7777. The test suite will be run as well.
If you just want to run the tests, follow these steps:
1. cd into the root of this project
1. npm install
1. grunt
This plugin was built against this API. You can use this, or feel free to use your own.
---
Thanks to the following contributors:
Code and ideas were stolen from the following sources:
* [this SO post on token-auth security][so-post]
* this SO post on string templating
* [this brilliant AngularJS module][ng-token-auth]
---
WTFPL © Lynn Dylan Hurley
[ng-token-auth]: https://github.com/lynndylanhurley/ng-token-auth
[dta]: https://github.com/lynndylanhurley/devise_token_auth
[token-auth-wiki]: http://stackoverflow.com/questions/1592534/what-is-token-based-authentication
[so-post]: http://stackoverflow.com/questions/18605294/is-devises-token-authenticatable-secure
[jquery]: https://jquery.com/
[jquery-cookie]: https://github.com/carhartl/jquery-cookie
[jquery-deparam]: https://www.npmjs.com/package/jquery-deparam
[pubsub-js]: https://github.com/mroderick/PubSubJS
[bower]: http://bower.io/
[npm]: https://www.npmjs.com/
[browserify]: http://browserify.org/
[cors]: http://en.wikipedia.org/wiki/Cross-origin_resource_sharing
[common-js]: http://en.wikipedia.org/wiki/CommonJS
[dfd]: https://api.jquery.com/jQuery.Deferred/
[angular]: https://angularjs.org/
[react]: http://facebook.github.io/react/
[j-toker-demo]: http://j-toker-demo.herokuapp.com/#/
[multi-user-demo]: http://j-toker-demo.herokuapp.com/#/alt-user
[o-auth-flow]: https://github.com/lynndylanhurley/ng-token-auth/raw/master/test/app/images/flow/omniauth-flow.jpg
[token-validation-flow]: https://github.com/lynndylanhurley/ng-token-auth/raw/master/test/app/images/flow/validation-flow.jpg
[email-registration-flow]: https://github.com/lynndylanhurley/ng-token-auth/raw/master/test/app/images/flow/email-registration-flow.jpg
[email-sign-in-flow]: https://github.com/lynndylanhurley/ng-token-auth/raw/master/test/app/images/flow/email-sign-in-flow.jpg
[password-reset-flow]: https://github.com/lynndylanhurley/ng-token-auth/raw/master/test/app/images/flow/password-reset-flow.jpg
[token-handling-diagram]: https://github.com/lynndylanhurley/ng-token-auth/raw/master/test/app/images/flow/token-update-detail.jpg
[batch-request-a]: https://github.com/lynndylanhurley/ng-token-auth/raw/master/test/app/images/flow/batch-request-overview.jpg
[batch-request-b]: https://github.com/lynndylanhurley/ng-token-auth/raw/master/test/app/images/flow/batch-request-detail.jpg
[logo]: https://github.com/lynndylanhurley/j-toker/raw/master/demo/src/images/j-toker-logo.gif