- OAuth2 authentication
- Email authentication, including:
- User registration
- Password resets
- Account updates
- Account deletion
- Seamless integration with the devise token auth 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 in production.
The demo uses React, and the source can be found here.
- About this plugin
- Installation
- Configuration
- API
- Events
- Possible complications
- Using alternate response formats
- Using multiple user types
- Conceptual diagrams
- Notes on Token Management
- Notes on Batch Requests
- Token Formatting
- Internet Explorer Caveats
- Contributing
- Development
- Callouts
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 AngularJS? Use ng-token-auth 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.
-
Download this plugin and its dependencies.
# using bower: bower install j-toker --save # using npm: npm install j-toker --save
-
Ensure that the following dependencies are included:
- jquery: AJAX requests
- jquery-cookie: Persist data through browser sessions
- jquery-deparam: Querystring param deconstruction.
- PubSubJS: (optional) Event publish / subscribe.
These dependencies were pulled down automatically if you used bower or npm to install jToker.
-
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).
$.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.
$.auth.configure({
apiUrl: 'https://my-api.com/api/v1'
});
That's it! 99% of you are done.
// 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.
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.
--
Path (relative to apiUrl
) to validate authentication tokens. Read more.
--
Path (relative to apiUrl
) to de-authenticate the current user. This will destroy the user's token both server-side and client-side.
--
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.
--
Path (relative to apiUrl
) for submitting new email registrations. Read more.
--
Path (relative to apiUrl
) for submitting account update requests.
--
Path (relative to apiUrl
) for submitting account deletion requests.
--
The absolute url to which the API should redirect after users visit the link contained in email-registration confirmation emails.
--
Path (relative to apiUrl
) for signing in to an existing email account.
--
Path (relative to apiUrl
) for requesting password reset emails.
--
The absolute url to which the API should redirect after users visit the link contained in password-reset confirmation emails.
--
Path (relative to apiUrl
) for submitting new passwords for authenticated users.
--
The method used to persist tokens between sessions. Allowed values are cookies
and localStorage
.
--
Older browsers have troubel with CORS. Pass a method to this option that will determine whether or not a proxy should be used.
--
If the proxyIf
method returns true
, this is the URL that will be used in place of apiUrl
.
--
A template for authentication tokens. The template will be provided a context with the following keys:
- access-token
- client
- uid
- expiry
The above strings will be replaced with their corresponding values as found in the response headers.
--
A function that will return the token's expiry from the current headers. null
is returned if no expiry is found.
--
A function used to identify and return the current user's account info (id
, username
, etc.) from the response of a successful login request.
--
A function used to identify and return the current user's info (id
, username
, etc.) from the response of a successful account update request.
--
A function used to identify and return the current user's info (id
, username
, etc.) from the response of a successful token validation request.
jToker can be used as a jQuery plugin, or as a CommonJS module.
$.auth.configure({apiUrl: '/api'});
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 that will be resolved upon success, or rejected upon failure.
$.auth
.oAuthSignIn({provider: 'github'})
.then(function(user) {
alert('Welcome ' + user.name + '!');
})
.fail(function(resp) {
alert('Authentication failure: ' + resp.errors.join(' '));
});
--
An object representing the current user. In addition to the attributes of your user model, the following additional attributes are available:
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 bedefault
.
--
Initiate an OAuth2 authentication.
-
provider
: the name of the target provider service as represented in theauthProviderPaths
config hash.$.auth.authenticate({provider: 'github'});
-
params
: an object containing additional fields user attributes and values.$.auth.authenticate({ provider: 'github', params: { favorite_color: 'flesh' } });
-
config
: the name of the configuration to be used when multiple auth configurations are available.$.auth.authenticate({ provider: 'github', config: 'altUser', params: { favorite_color: 'amaranth' } });
--
Create an account using email for confirmation.
-
email
-
password
-
password_confirmation
$.auth.emailSignUp({ email: 'test@test.com', password: '*****', password_confirmation: '*****' });
-
config
: the name of the configuration to be used when multiple configurations are available.$.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:
$.auth.emailSignUp({ email: 'test@test.com', password: '*****', password_confirmation: '*****', favorite_color: 'black' });
--
Authenticate a user that registered via email.
-
email
-
password
$.auth.emailSignIn({ email: 'test@test.com', password: '*****' });
-
config
: name of the config to be used when multiple configurations are available.$.auth.emailSignIn({ email: 'test@test.com', password: '*****', config: 'altUser' });
--
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.
// 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>
);
}
});
--
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'
});
--
Send password reset instructions to a user that was registered by email.
email
$.auth.requestPasswordReset({email: 'test@test.com'});
--
Change the current user's password. This only applies to users that were registered by email.
password
password_confirmation
$.auth.updatePassword({
password: '*****',
password_confirmation: '*****'
});
--
De-authenticates the current user. This will destroy the current user's client-side and server-side auth credentials.
$.auth.signOut();
--
Destroy the current user's account.
$.auth.destroyAccount();
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.
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 successful user authentication. Event message contains the user object.
$.auth.emailSignIn
$.auth.oAuthSignIn
$.auth.validateToken
PubSub.subscribe('auth.validation.success', function(ev, user) {
alert('Welcome' + user.name + '!');
});
--
Broadcast after failed user authentication. Event message contains errors related to the failure.
$.auth.emailSignIn
$.auth.oAuthSignIn
$.auth.validateToken
PubSub.subscribe('auth.validation.error', function(ev, err) {
alert('Validation failure.');
});
--
Broadcast after email sign up request was successfully completed.
$.auth.emailSignUp
PubSub.subscribe('auth.emailRegistration.success', function(ev, msg) {
alert('Check your email!');
});
--
Broadcast after email sign up requests fail.
$auth.emailSignUp
PubSub.subscribe('auth.emailRegistration.error', function(ev, msg) {
alert('There was a error submitting your request. Please try again!');
});
--
Broadcast after password reset requests complete successfully.
$.auth.passwordResetRequest
PubSub.subscribe('auth.passwordResetRequest.success', function(ev, msg) {
alert('Check your email!');
});
--
Broadcast after password reset requests fail.
$.auth.passwordResetRequest
PubSub.subscribe('auth.passwordResetRequest.error', function(ev, msg) {
alert('There was an error submitting your request. Please try again!');
});
--
Broadcast upon visiting the link contained in a registration confirmation email if the subsequent validation succeeds.
$.auth.validateToken
PubSub.subscribe('auth.emailConfirmation.success', function(ev, msg) {
alert('Welcome' + $.auth.user.name + '!');
});
--
Broadcast upon visiting the link contained in a registration confirmation email if the subsequent validation fails.
$.auth.validateToken
PubSub.subscribe('auth.passwordResetRequest.error', function(ev, msg) {
alert('There was an error authenticating your new account!');
});
--
Broadcast upon visiting the link contained in a password reset confirmation email if the subsequent validation succeeds.
$.auth.validateToken
PubSub.subscribe('auth.emailConfirmation.success', function(ev, msg) {
alert('Welcome' + $.auth.user.name + '! Change your password!');
});
--
Broadcast upon visiting the link contained in a password reset confirmation email if the subsequent validation fails.
$.auth.validateToken
PubSub.subscribe('auth.passwordResetRequest.error', function(ev, msg) {
alert('There was an error authenticating your account!');
});
--
Broadcast after a user successfully completes authentication using an email account.
$.auth.emailSignIn
PubSub.subscribe('auth.emailSignIn.success', function(ev, msg) {
alert('Welcome' + $.auth.user.name + '! Change your password!');
});
--
Broadcast after a user fails to authenticate using their email account.
$.auth.emailSignIn
PubSub.subscribe('auth.emailSignIn.error', function(ev, msg) {
alert('There was an error authenticating your account!');
});
--
Broadcast after a user successfully authenticates with an OAuth2 provider.
$.auth.oAuthSignIn
#####Example:
PubSub.subscribe('auth.oAuthSignIn.success', function(ev, msg) {
alert('Welcome' + $.auth.user.name + '!');
});
--
Broadcast after a user fails to authenticate using an OAuth2 provider.
$.auth.oAuthSignIn
PubSub.subscribe('auth.oAuthSignIn.error', function(ev, msg) {
alert('There was an error authenticating your account!');
});
--
Broadcast after a user successfully signs in using either email or OAuth2 authentication.
$.auth.emailSignIn
$.auth.oAuthSignIn
PubSub.subscribe('auth.oAuthSignIn.success', function(ev, msg) {
alert('Welcome' + $.auth.user.name + '!');
});
--
Broadcast after a user fails to sign in using either email or OAuth2 authentication.
$.auth.emailSignIn
$.auth.oAuthSignIn
PubSub.subscribe('auth.signIn.error', function(ev, msg) {
alert('There was an error authenticating your account!');
});
--
Broadcast after a user successfully signs out.
$.auth.signOut
PubSub.subscribe('auth.signOut.success', function(ev, msg) {
alert('Goodbye!');
});
--
Broadcast after a user fails to sign out.
$.auth.signOut
PubSub.subscribe('auth.signOut.success', function(ev, msg) {
alert('There was a problem with your sign out attempt. Please try again!');
});
--
Broadcast after a user successfully updates their account info.
$.auth.updateAccount
PubSub.subscribe('auth.accountUpdate.success', function(ev, msg) {
alert('Your account has been updated!');
});
--
Broadcast when an account update request fails.
$.auth.updateAccount
PubSub.subscribe('auth.accountUpdate.error', function(ev, msg) {
alert('There was an error while trying to update your account.');
});
--
Broadcast after a user's account has been successfully destroyed.
$.auth.destroyAccount
PubSub.subscribe('auth.destroyAccount.success', function(ev, msg) {
alert('Goodbye!');
});
--
Broadcast after an attempt to destroy a user's account fails.
$.auth.destroyAccount
PubSub.subscribe('auth.destroyAccount.error', function(ev, msg) {
alert('There was an error while trying to destroy your account.');
});
--
Broadcast after a user successfully changes their password.
$.auth.updatePassword
PubSub.subscribe('auth.passwordUpdate.success', function(ev, msg) {
alert('Your password has been changed!');
});
--
Broadcast after an attempt to change a user's password fails.
$.auth.updatePassword
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.
$.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:
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:
$.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
.
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.
When authenticating with a 3rd party provider, the following steps will take place.
- An external window will be opened to the provider's authentication page.
- Once the user signs in, they will be redirected back to the API at the callback uri that was registered with the oauth2 provider.
- 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.
<!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>
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.
This plugin also provides support for email registration. The following diagram illustrates this process.
The password reset flow is similar to the email registration 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:
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.
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:
$.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:
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:
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.
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
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.
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.):
- navigate from the client site to the API
- navigate from the API to the provider
- navigate from the provider to the API
- navigate from the API back to the client
These steps are taken automatically when using this plugin with IE8+.
- Create a feature branch with your changes.
- Write some test cases.
- Make all the tests pass.
- 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.
cd
to the root of this project.npm install
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:
cd
into the root of this projectnpm install
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
- this SO post on string templating
- this brilliant AngularJS module
WTFPL © Lynn Dylan Hurley