jToker
Simple, secure user authentication for jQuery
Features:
- 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+
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
- 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
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
-
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).
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:
- access-token
- client
- uid
- expiry
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:
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
.
--
$.auth.oAuthSignIn
Initiate an OAuth2 authentication.
arguments:
-
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' } });
--
$.auth.emailSignUp
Create an account using email for confirmation.
arguments:
-
email
-
password
-
password_confirmation
$.auth.emailSignUp({ email: '[email protected]', password: '*****', password_confirmation: '*****' });
-
config
: the name of the configuration to be used when multiple configurations are available.$.auth.emailSignUp({ email: '[email protected]', 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: '[email protected]', password: '*****', password_confirmation: '*****', favorite_color: 'black' });
--
$.auth.emailSignIn
Authenticate a user that registered via email.
arguments:
-
email
-
password
$.auth.emailSignIn({ email: '[email protected]', password: '*****' });
-
config
: name of the config to be used when multiple configurations are available.$.auth.emailSignIn({ email: '[email protected]', password: '*****', config: 'altUser' });
--
$.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:
email
$.auth.requestPasswordReset({email: '[email protected]'});
--
$.auth.updatePassword
Change the current user's password. This only applies to users that were registered by email.
arguments:
password
password_confirmation
$.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:
$.auth.emailSignIn
$.auth.oAuthSignIn
$.auth.validateToken
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:
$.auth.emailSignIn
$.auth.oAuthSignIn
$.auth.validateToken
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:
$.auth.emailSignUp
PubSub.subscribe('auth.emailRegistration.success', function(ev, msg) {
alert('Check your email!');
});
--
auth.emailRegistration.error
Broadcast after email sign up requests fail.
Broadcast after:
$auth.emailSignUp
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:
$.auth.passwordResetRequest
Example:
PubSub.subscribe('auth.passwordResetRequest.success', function(ev, msg) {
alert('Check your email!');
});
--
auth.passwordResetRequest.error
Broadcast after password reset requests fail.
Broadcast after:
$.auth.passwordResetRequest
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:
$.auth.validateToken
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:
$.auth.validateToken
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:
$.auth.validateToken
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:
$.auth.validateToken
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:
$.auth.emailSignIn
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:
$.auth.emailSignIn
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:
$.auth.oAuthSignIn
#####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:
$.auth.oAuthSignIn
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:
$.auth.emailSignIn
$.auth.oAuthSignIn
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:
$.auth.emailSignIn
$.auth.oAuthSignIn
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:
$.auth.signOut
Example:
PubSub.subscribe('auth.signOut.success', function(ev, msg) {
alert('Goodbye!');
});
--
auth.signOut.error
Broadcast after a user fails to sign out.
Broadcast after:
$.auth.signOut
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:
$.auth.updateAccount
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:
$.auth.updateAccount
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:
$.auth.destroyAccount
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:
$.auth.destroyAccount
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:
$.auth.updatePassword
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:
$.auth.updatePassword
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.
beforeSend
usage example
Custom $.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.
$.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).
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.
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.
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.
Email registration
This plugin also provides support for email registration. The following diagram illustrates this process.
Email sign in
Password reset
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.
Token management
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.
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:
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.
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:
- 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.
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:
- your API lives on a different domain than your client
- you wish to support IE8 and IE9
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.):
- 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+.
Contributing
- 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.
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.
cd
to the root of this project.yarn
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:
cd
into the root of this projectyarn
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:
- this SO post on token-auth security
- this SO post on string templating
- this brilliant AngularJS module
License
WTFPL © Lynn Dylan Hurley