Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds JWT Authenticator with refresh functionality #24

Merged
merged 35 commits into from
Feb 21, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
835af6a
Added initial token refresh code inspired by oauth lib
Jan 2, 2015
d618879
Merged with upstream
Jan 2, 2015
3c6be59
Significant update to ensure proper time handling
Jan 6, 2015
04717c2
Segmented JWT authenticator out, reverted token authenticator to defa…
Jan 17, 2015
75becb7
Updated readme and gitignore
Jan 17, 2015
a2d6d8f
Removed tokenOrigIssuedAt as it was no longer used.
Jan 17, 2015
46d9ec0
Updated readme
Jan 17, 2015
48596ad
Update README.md
Jan 17, 2015
c6ecf19
Fixed missing semicolon, removed unused expiresAt
Jan 17, 2015
52e6d51
Merge branch 'master' of https://github.com/erichonkanen/ember-cli-si…
Jan 17, 2015
1c33b64
Updated readme
Jan 17, 2015
60ffe0e
Removed unused tests
Jan 18, 2015
8f97266
Removed insecure credentials warning
Jan 18, 2015
12487d4
Merge branch 'master' of https://github.com/jpadilla/ember-cli-simple…
Jan 18, 2015
d50c975
Fixed signature expired issue by properly updating the session data w…
Jan 19, 2015
7bac62b
Added restore method to jwt authenticator which allows for scheduling…
Jan 19, 2015
b32de32
Removed unused data arg
Jan 19, 2015
abd2775
Added test for restore method
Feb 9, 2015
981368c
Updated restore test to use config values
Feb 9, 2015
fb0bac7
Removed log calls
Feb 9, 2015
561d31a
Added more tests for restore method
Feb 9, 2015
dc47279
Added additional restore test
Feb 9, 2015
085bde9
Added 2 more tests to restore for checking Ember.run.later
Feb 9, 2015
18e0e56
Added temp fix for Ember.run.later
Feb 9, 2015
fabe7fd
Merged with latest upstream
Feb 9, 2015
32a5102
abstracted sinon.spy Ember.run.later to QUnit.testStart
Feb 10, 2015
bf458f1
Completed authenticate and refreshAccessToken tests
Feb 10, 2015
9559627
Removed QUnit and moved the sinon call to setup and teardown
Feb 10, 2015
eceb343
Added another restore test, fixed bug where multiple tabs caused logout
Feb 11, 2015
c25357a
Removed unused token data to ember merge
Feb 11, 2015
50beabd
Fixed expiresAt issue where it was passed to resolveTime twice
Feb 12, 2015
1cb4379
Updated comments
Feb 14, 2015
7405546
typo
Feb 14, 2015
5e040a7
Added more examples
Feb 14, 2015
5ac5a20
small tweaks
Feb 14, 2015
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@
/libpeerconnection.log
npm-debug.log
testem.log
*.sw[po]
17 changes: 17 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.

# compiled output
/dist
/tmp

# dependencies
/node_modules
/bower_components/*

# misc
/.sass-cache
/connect.lock
/coverage/*
/libpeerconnection.log
npm-debug.log
testem.log
58 changes: 53 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Ember Simple Auth Token [![Build Status](https://travis-ci.org/jpadilla/ember-cli-simple-auth-token.svg?branch=master)](https://travis-ci.org/jpadilla/ember-cli-simple-auth-token)

This is an extension to the Ember Simple Auth library that provides an
authenticator and an authorizer that are compatible with APIs with token-based authentication.
This is an extension to the Ember Simple Auth library that provides a default token authenticator, an enhanced authenticator with automatic refresh capability, and an authorizer that are compatible with APIs with token-based authentication.

**As your user's credentials as well as the token are exchanged between the
Ember.js app and the server you have to make sure that this connection uses HTTPS!**
Expand All @@ -26,9 +25,9 @@ npm install --save-dev ember-cli-simple-auth-token
ember generate simple-auth-token
```

## The Authenticator
## The Authenticators

In order to use the Token authenticator the application needs to have a login route:
In order to use the Token authenticator or the JWT authenticator, the application needs to have a login route:

```js
// app/router.js
Expand Down Expand Up @@ -58,6 +57,10 @@ session API directly; see the
[API docs for `Session`](http://ember-simple-auth.simplabs.com/ember-simple-auth-api-docs.html#SimpleAuth-Session)).
It then also needs to specify the Token authenticator to be used:

**Token Authenticator**

Default base implementation for token authentication.

```js
// app/controllers/login.js
import Ember from 'ember';
Expand All @@ -68,6 +71,40 @@ export default Ember.Controller.extend(LoginControllerMixin, {
});
```

**JWT Authenticator**

Extends the Token Authenticator and adds automatic refresh functionality.

```js
// app/controllers/login.js
import Ember from 'ember';
import LoginControllerMixin from 'simple-auth/mixins/login-controller-mixin';

export default Ember.Controller.extend(LoginControllerMixin, {
authenticator: 'simple-auth-authenticator:jwt'
});
```

Please note, the JWT authenticator will decode a token and look for the
expiration time found by looking up the token[Condig.tokenExpireName]. It then
calculated the difference between current time and the expire time to
determine when to make the next automatic token refresh request.

For example, your decoded token might look like this:

```
token = {
'user': 'george',
'email': '[email protected]'
'exp': '98343234' // <ISO-8601> UTC seconds from e.g. python backend.
}
```

In this case the token expire name is using the default `exp` as set by the
`Config.tokenExpireName` property.

An automatic token refresh request would be sent out at token[Config.tokenExpireName] - now()

## The Authorizer

The authorizer authorizes requests by adding `token` property from the session in the `Authorization` header:
Expand All @@ -87,6 +124,8 @@ ENV['simple-auth'] = {

## Available Customization Options

For the Token authenticator:

```js
// config/environment.js
ENV['simple-auth-token'] = {
Expand All @@ -96,6 +135,15 @@ ENV['simple-auth-token'] = {
tokenPropertyName: 'token',
authorizationPrefix: 'Bearer ',
authorizationHeaderName: 'Authorization',
headers: {}
headers: {},
};
```

For the JWT authenticator (in addition to the Token authenticator fields):

```
refreshAccessTokens: true,
serverTokenRefreshEndpoint: '/api-token-refresh/',
tokenExpireName: 'exp',
timeFactor: 1 // example - set to "1000" to convert incoming seconds to milliseconds.
```
269 changes: 269 additions & 0 deletions addon/authenticators/jwt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
import Ember from 'ember';
import Configuration from '../configuration';
import TokenAuthenticator from './token';

/**
JWT (JSON Web Token) Authenticator that supports automatic token refresh.

Inspired by [ember-simple-auth-oauth2](https://github.com/simplabs/ember-simple-auth/tree/master/packages/ember-simple-auth-oauth2)

The factory for this authenticator is registered as
'simple-auth-authenticator:jwt` in Ember's container.

@class JWT
@namespace SimpleAuth.Authenticators
@module simple-auth-token/authenticators/jwt
@extends TokenAuthenticator
*/
export default TokenAuthenticator.extend({
/**
The endpoint on the server for refreshing a token.
@property serverTokenRefreshEndpoint
@type String
@default '/api-token-refresh/'
*/
serverTokenRefreshEndpoint: '/api-token-refresh/',

/**
Sets whether the authenticator automatically refreshes access tokens.
@property refreshAccessTokens
@type Boolean
@default true
*/
refreshAccessTokens: true,

/**
The amount of time to wait before refreshing the token - set automatically.
@property refreshTokenTimeout
@private
*/
refreshTokenTimeout: null,

/**
The name for which decoded token field represents the token expire time.
@property tokenExpireName
@type String
@default 'exp'
*/
tokenExpireName: 'exp',

/**
Default time unit.
@property timeFactor
@type Integer
@default 1 (seconds)
*/
timeFactor: 1,

/**
@method init
@private
*/
init: function() {
this.serverTokenEndpoint = Configuration.serverTokenEndpoint;
this.serverTokenRefreshEndpoint = Configuration.serverTokenRefreshEndpoint;
this.identificationField = Configuration.identificationField;
this.tokenPropertyName = Configuration.tokenPropertyName;
this.refreshAccessTokens = Configuration.refreshAccessTokens;
this.tokenExpireName = Configuration.tokenExpireName;
this.timeFactor = Configuration.timeFactor;
this.headers = Configuration.headers;
},

/**
Restores the session from a set of session properties.

It will return a resolving promise if one of two conditions is met:

1) Both `data.token` and `data.expiresAt` are non-empty and `expiresAt`
is greater than the calculated `now`.
2) If `data.token` is non-empty and the decoded token has a key for
`tokenExpireName`.

If `refreshAccessTokens` is true, `scheduleAccessTokenRefresh` will
be called and an automatic token refresh will be initiated.

@method restore
@param {Object} data The data to restore the session from
@return {Ember.RSVP.Promise} A promise that when it resolves results
in the session being authenticated
*/
restore: function(data){
var _this = this;
return new Ember.RSVP.Promise(function(resolve, reject){
var now = (new Date()).getTime(),
expiresAt = _this.resolveTime(data.expiresAt);
if(!Ember.isEmpty(data.expiresAt) && !Ember.isEmpty(data.token) && expiresAt > now){
if(_this.refreshAccessTokens){
_this.refreshAccessToken(data.token).then(function(data){
resolve(data);
}, reject);
}else{
reject();
}
}else{
if(Ember.isEmpty(data.token)){
reject();
}else{
var tokenData = _this.getTokenData({'token': data.token}),
tokenExpiresAt = tokenData[_this.tokenExpireName];
_this.scheduleAccessTokenRefresh(tokenExpiresAt, data.token);
resolve(data);
}
}
});
},

/**
Authenticates the session with the specified `credentials`.

It will return a resolving promise if it successfully posts a request
to the `JWT.serverTokenEndpoint` with the valid credentials.

An automatic token refresh will be scheduled with the new expiration date
from the returned refresh token. That expiration will be merged with the
response and the promise resolved.

@method authenticate
@param {Object} options The credentials to authenticate the session with
@return {Ember.RSVP.Promise} A promise that resolves when an auth token is
successfully acquired from the server and rejects
otherwise
*/
authenticate: function(credentials) {
var _this = this;
return new Ember.RSVP.Promise(function(resolve, reject) {
var data = _this.getAuthenticateData(credentials);
_this.makeRequest(_this.serverTokenEndpoint, data).then(function(response) {
Ember.run(function() {
var tokenData = _this.getTokenData(response),
expiresAt = tokenData[_this.tokenExpireName];
_this.scheduleAccessTokenRefresh(expiresAt, response.token);
response = Ember.merge(response, { expiresAt: expiresAt });
resolve(_this.getResponseData(response));
});
}, function(xhr) {
Ember.run(function() {
reject(xhr.responseJSON || xhr.responseText);
});
});
});
},

/**
Schedules a token refresh request to be sent to the backend after a calculated
`wait` time has passed.

If both `token` and `expiresAt` are non-empty, and `expiresAt` is greater than
the calculated `now`, the token refresh will be scheduled through Ember.run.later.

@method scheduleAccessTokenRefresh
@private
*/
scheduleAccessTokenRefresh: function(expiresAt, token) {
if(this.refreshAccessTokens){
expiresAt = this.resolveTime(expiresAt);
var now = (new Date()).getTime(),
wait = expiresAt - now;
if(!Ember.isEmpty(token) && !Ember.isEmpty(expiresAt) && expiresAt > now){
Ember.run.cancel(this._refreshTokenTimeout);
delete this._refreshTokenTimeout;
if(!Ember.testing){
this._refreshTokenTimeout = Ember.run.later(this, this.refreshAccessToken, token, wait);
}
}
}
},

/**
Makes a refresh token request to grab a new authenticated JWT token from the server.

It will return a resolving promise if a successful POST is made to the
`JWT.serverTokenRefreshEndpoint`.

After the new token is obtained it will schedule the next automatic token refresh
based on the new `expiresAt` time.

The session will be updated via the trigger `sessionDataUpdated`.

@method refreshAccessToken
@private
*/
refreshAccessToken: function(token) {
var _this = this;
var data = {token: token};
return new Ember.RSVP.Promise(function(resolve, reject) {
_this.makeRequest(_this.serverTokenRefreshEndpoint, data).then(function(response) {
Ember.run(function() {
var tokenData = _this.getTokenData(response),
expiresAt = tokenData[_this.tokenExpireName],
data = Ember.merge(response, {expiresAt: expiresAt});
_this.scheduleAccessTokenRefresh(expiresAt, response.token);
_this.trigger('sessionDataUpdated', data);
resolve(response);
});
}, function(xhr, status, error) {
Ember.Logger.warn('Access token could not be refreshed - server responded with ' + error + '.');
reject();
});
});
},

/**
Returns the decoded token with accessible returned values.

@method getTokenData
@return {object} An object with properties for the session.
*/
getTokenData: function(response) {
var token = response.token.split('.');
if(token.length > 1){
return JSON.parse(atob(token[1]));
}else{
return JSON.parse(atob(token[0]));
}
},

/**
Accepts a `url` and `data` to be used in an ajax server request.

@method makeRequest
@private
*/
makeRequest: function(url, data) {
return Ember.$.ajax({
url: url,
type: 'POST',
data: JSON.stringify(data),
dataType: 'json',
contentType: 'application/json',
beforeSend: function(xhr, settings) {
xhr.setRequestHeader('Accept', settings.accepts.json);
},
headers: this.headers
});
},

/**
Cancels any outstanding automatic token refreshes and returns a resolving
promise.
@method invalidate
@param {Object} data The data of the session to be invalidated
@return {Ember.RSVP.Promise} A resolving promise
*/
invalidate: function() {
Ember.run.cancel(this._refreshTokenTimeout);
delete this._refreshTokenTimeout;
return new Ember.RSVP.resolve();
},

/**
Handles converting between time units for data between different systems.
Default: seconds(1)
@method resolveTime
@private
*/
resolveTime: function(time){
return new Date(time * this.timeFactor).getTime();
},
});
Loading