Skip to content
This repository was archived by the owner on Nov 25, 2020. It is now read-only.

Commit

Permalink
feat(project): add refresh token
Browse files Browse the repository at this point in the history
  • Loading branch information
alfkonee committed Mar 31, 2016
1 parent edc5a82 commit 77bdbe4
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 25 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,21 @@ unlinkUrl: '/unlink/',
// The HTTP method used for 'unlink' requests (Options: 'get' or 'post')
unlinkMethod: 'get',

// Refresh Token Options
// =====================

// Option to turn refresh tokens On/Off
useRefreshToken: false,
// The option to enable/disable the automatic refresh of Auth tokens using Refresh Tokens
autoUpdateToken: true,
// This allows the refresh token to be a further object deeper `{ "responseTokenProp": { "refreshTokenRoot" : { "tokenName" : '...' } } }`
refreshTokenRoot: false,
// This is the property from which to get the token `{ "responseTokenProp": { "refreshTokenName" : '...' } }`
refreshTokenName: 'refresh_token',
// Prepended to the `refreshTokenName` when kept in storage (nothing to do with)
refreshTokenPrefix: 'aurelia',
// Oauth Client Id
clientId: false,

// Token Related options
// =====================
Expand Down
40 changes: 32 additions & 8 deletions src/app.fetch-httpClient.config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {HttpClient} from 'aurelia-fetch-client';
import {Authentication} from './authentication';
import {AuthService} from './authService';
import {BaseConfig} from './baseConfig';
import {inject} from 'aurelia-dependency-injection';
import {Config, Rest} from 'spoonx/aurelia-api';

@inject(HttpClient, Config, Authentication, BaseConfig)
@inject(HttpClient, Config, AuthService, BaseConfig)
export class FetchConfig {
/**
* Construct the FetchConfig
Expand All @@ -14,10 +14,10 @@ export class FetchConfig {
* @param {Authentication} authService
* @param {BaseConfig} config
*/
constructor(httpClient, clientConfig, authentication, config) {
constructor(httpClient, clientConfig, authService, config) {
this.httpClient = httpClient;
this.clientConfig = clientConfig;
this.auth = authentication;
this.auth = authService;
this.config = config.current;
}

Expand All @@ -27,16 +27,16 @@ export class FetchConfig {
* @return {{request: Function}}
*/
get interceptor() {
let auth = this.auth;
let config = this.config;
let auth = this.auth;
let config = this.config;
let client = this.httpClient;

return {
request(request) {
if (!auth.isAuthenticated() || !config.httpInterceptor) {
return request;
}

let token = auth.getToken();
let token = auth.getCurrentToken();

if (config.authHeader && config.authToken) {
token = `${config.authToken} ${token}`;
Expand All @@ -45,6 +45,30 @@ export class FetchConfig {
request.headers.append(config.authHeader, token);

return request;
},
response(response, request) {
return new Promise((resolve, reject) => {
if (response.ok) {
return resolve(response);
}
if (response.status !== 401) {
return resolve(response);
}
if (!auth.isTokenExpired() || !config.httpInterceptor) {
return resolve(response);
}
if (!auth.getRefreshToken()) {
return resolve(response);
}
auth.updateToken().then(() => {
let token = auth.getCurrentToken();
if (config.authHeader && config.authToken) {
token = `${config.authToken} ${token}`;
}
request.headers.append('Authorization', token);
return client.fetch(request).then(resolve);
});
});
}
};
}
Expand Down
77 changes: 65 additions & 12 deletions src/authService.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import {authUtils} from './authUtils';
@inject(Authentication, OAuth1, OAuth2, BaseConfig)
export class AuthService {
constructor(auth, oAuth1, oAuth2, config) {
this.auth = auth;
this.oAuth1 = oAuth1;
this.oAuth2 = oAuth2;
this.config = config.current;
this.client = this.config.client;
this.auth = auth;
this.oAuth1 = oAuth1;
this.oAuth2 = oAuth2;
this.config = config.current;
this.client = this.config.client;
this.isRefreshing = false;
}

getMe(criteria) {
Expand All @@ -22,17 +23,36 @@ export class AuthService {
return this.client.find(this.auth.getProfileUrl(), criteria);
}

getCurrentToken() {
return this.auth.getToken();
}

getRefreshToken() {
return this.auth.getRefreshToken();
}

updateMe(body, criteria) {
if (typeof criteria === 'string' || typeof criteria === 'number') {
criteria = {id: criteria};
criteria = { id: criteria };
}
return this.client.update(this.auth.getProfileUrl(), criteria, body);
}

isAuthenticated() {
let isExpired = this.auth.isTokenExpired();
if (isExpired && this.config.autoUpdateToken) {
if (this.isRefreshing) {
return true;
}
this.updateToken();
}
return this.auth.isAuthenticated();
}

isTokenExpired() {
return this.auth.isTokenExpired();
}

getTokenPayload() {
return this.auth.getPayload();
}
Expand Down Expand Up @@ -63,28 +83,61 @@ export class AuthService {

login(email, password) {
let loginUrl = this.auth.getLoginUrl();
let content;
let config = this.config;
let clientId = this.config.clientId;
let content = {};
if (typeof arguments[1] !== 'string') {
content = arguments[0];
} else {
content = {
'email': email,
'password': password
};
content = {email: email, password: password};
if (clientId) {
content.client_id = clientId;
}
}

return this.client.post(loginUrl, content)
.then(response => {
this.auth.setTokenFromResponse(response);
if (config.useRefreshToken) {
this.auth.setRefreshTokenFromResponse(response);
}

return response;
});
}

logout(redirectUri) {
return this.auth.logout(redirectUri);
}

updateToken() {
this.isRefreshing = true;
let loginUrl = this.auth.getLoginUrl();
let refreshToken = this.auth.getRefreshToken();
let clientId = this.config.clientId;
let content = {};
if (refreshToken) {
content = {grant_type: 'refresh_token', refresh_token: refreshToken};
if (clientId) {
content.client_id = clientId;
}

return this.client.post(loginUrl, content)
.then(response => {
this.auth.setRefreshToken(response);
this.auth.setToken(response);
this.isRefreshing = false;

return response;
}).catch((err) => {
this.auth.removeToken();
this.auth.removeRefreshToken();
this.isRefreshing = false;

throw err;
});
}
}

authenticate(name, redirect, userData) {
let provider = this.oAuth2;
if (this.config.providers[name].type === '1.0') {
Expand Down
59 changes: 56 additions & 3 deletions src/authentication.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ import {authUtils} from './authUtils';
@inject(Storage, BaseConfig)
export class Authentication {
constructor(storage, config) {
this.storage = storage;
this.config = config.current;
this.storage = storage;
this.config = config.current;
}

get refreshTokenName() {
return this.config.refreshTokenPrefix ? this.config.refreshTokenPrefix + '_' + this.config.refreshTokenName : this.config.refreshTokenName;
}

get tokenName() {
Expand Down Expand Up @@ -38,13 +42,16 @@ export class Authentication {
return this.storage.get(this.tokenName);
}

getRefreshToken() {
return this.storage.get(this.refreshTokenName);
}

getPayload() {
let token = this.storage.get(this.tokenName);

if (token && token.split('.').length === 3) {
let base64Url = token.split('.')[1];
let base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');

try {
return JSON.parse(decodeURIComponent(escape(window.atob(base64))));
} catch (error) {
Expand Down Expand Up @@ -85,10 +92,44 @@ export class Authentication {
}
}

setRefreshTokenFromResponse(response) {
let refreshTokenName = this.refreshTokenName;
let refreshToken = response && response.refresh_token;
let refreshTokenPath;
let token;

if (refreshToken) {
if (authUtils.isObject(refreshToken) && authUtils.isObject(refreshToken.data)) {
response = refreshToken;
} else if (authUtils.isString(refreshToken)) {
token = refreshToken;
}
}

if (!token && response) {
token = this.config.refreshTokenRoot && response[this.config.refreshTokenRoot]
? response[this.config.refreshTokenRoot][this.config.refreshTokenName]
: response[this.config.refreshTokenName];
}
if (!token) {
refreshTokenPath = this.config.refreshTokenRoot
? this.config.refreshTokenRoot + '.' + this.config.refreshTokenName
: this.config.refreshTokenName;

throw new Error('Expecting a refresh token named "' + refreshTokenPath + '" but instead got: ' + JSON.stringify(response.content));
}

this.storage.set(refreshTokenName, token);
}

removeToken() {
this.storage.remove(this.tokenName);
}

removeRefreshToken() {
this.storage.remove(this.refreshTokenName);
}

isAuthenticated() {
let token = this.storage.get(this.tokenName);

Expand Down Expand Up @@ -119,9 +160,21 @@ export class Authentication {
return true;
}


isTokenExpired() {
let payload = this.getPayload();
let exp = payload ? payload.exp : null;
if (exp) {
return Math.round(new Date().getTime() / 1000) > exp;
}

return undefined;
}

logout(redirect) {
return new Promise(resolve => {
this.storage.remove(this.tokenName);
this.storage.remove(this.refreshTokenName);

if (this.config.logoutRedirect && !redirect) {
window.location.href = this.config.logoutRedirect;
Expand Down
15 changes: 15 additions & 0 deletions src/baseConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,21 @@ export class BaseConfig {
// The HTTP method used for 'unlink' requests (Options: 'get' or 'post')
unlinkMethod: 'get',

// Refresh Token Options
// =====================

// Option to turn refresh tokens On/Off
useRefreshToken: false,
// The option to enable/disable the automatic refresh of Auth tokens using Refresh Tokens
autoUpdateToken: true,
// This allows the refresh token to be a further object deeper `{ "responseTokenProp": { "refreshTokenRoot" : { "tokenName" : '...' } } }`
refreshTokenRoot: false,
// This is the property from which to get the token `{ "responseTokenProp": { "refreshTokenName" : '...' } }`
refreshTokenName: 'refresh_token',
// Prepended to the `refreshTokenName` when kept in storage (nothing to do with)
refreshTokenPrefix: 'aurelia',
// Oauth Client Id
clientId: false,

// Token Related options
// =====================
Expand Down
7 changes: 5 additions & 2 deletions test/app.fetch-httpClient.config.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ function getContainer() {
return container;
}

function getInterceptorStubs(isAuthenticated, httpInterceptor) {
function getInterceptorStubs(isAuthenticated, httpInterceptor, refreshTokenExists) {
let authenticationStub = {
isAuthenticated: () => { return isAuthenticated; },
getToken: () => { return 'someToken'; }
getCurrentToken: () => { return 'someToken'; },
updateToken: () => {
return refreshTokenExists ? Promise.resolve() : Promise.reject();
}
};

let configStub = {
Expand Down

0 comments on commit 77bdbe4

Please sign in to comment.