Permalink
Browse files

fix(authService): listen to storage events. fixes login/logout in oth…

…er tabs

NEW: storageChangedRedirect 0 redirect automatically after storage events if needed
BREAKING CHANGE: hasDataAnalyzed renamed to responseAnalyzed
  • Loading branch information...
doktordirk committed Aug 4, 2016
1 parent e2ef686 commit 52c2f67d08e7844326d0e35c8a07502dcc77b2f3
Showing with 159 additions and 64 deletions.
  1. +7 −0 doc/Quick start.md
  2. +3 −3 doc/api_authService.md
  3. +4 −2 doc/baseConfig.md
  4. +58 −14 src/authService.js
  5. +12 −11 src/authentication.js
  6. +42 −5 src/baseConfig.js
  7. +30 −26 test/authService.spec.js
  8. +3 −3 test/authentication.spec.js
@@ -80,6 +80,13 @@ export function configure(aurelia) {
}
```
### Getting the current authentication status
There are two options:
* authService.isAuthenticated(): This function gets the current token on each call from the window storage to calucate the current authentication status. Since it's a function, Aurelia will use dirty checking, if you bind to it.
* authService.authenticated: This property gets updated by timeout and storage events to keep it accurate all the time. No dirty-checking is needed, but you might not like that there is magic used to keep it updated.
### Provide a UI for a login, signup and profile
Button actions are passed to the corresponding view model via a simple click.delegate:
@@ -44,9 +44,9 @@ import {AuthService} from 'aurelia-authentication';
### .authenticated
| Type | Description |
| ------- | ------------------------------------------------ |
| Boolean | Authentication status as set by timeout function |
| Type | Description |
| ------- | ------------------------------------------- |
| Boolean | Automatically updated authentication status |
----------
@@ -23,8 +23,10 @@ loginRoute = '/login';
loginOnSignup = true;
// If loginOnSignup == false: The SPA url to which the user is redirected after a successful signup (else loginRedirect is used)
signupRedirect = '#/login';
// redirect when token expires. 0 = don't redirect (default), 1 = use logoutRedirect, string = redirect there
expiredRedirect = 0;
// reload page when token expires. 0 = don't reload (default), 1 = do reload page
expiredReload = 0;
// reload page when storage changed aka login/logout in other tabs/windows. 0 = don't reload (default), 1 = do reload page
storageChangedReload = 0;
// API related options
@@ -53,7 +53,7 @@ export class AuthService {
// get token stored in previous format over
const oldStorageKey = config.tokenPrefix
? config.tokenPrefix + '_' + config.tokenName
? `${config.tokenPrefix}_${config.tokenName}`
: config.tokenName;
const oldToken = authentication.storage.get(oldStorageKey);
@@ -67,8 +67,33 @@ export class AuthService {
// initialize status by resetting if existing stored responseObject
this.setResponseObject(this.authentication.getResponseObject());
// listen to storage events in case the user logs in or out in another tab/window
PLATFORM.addEventListener('storage', this.storageEventHandler);
}
/**
* The handler used for storage events. Detects and handles authentication changes in other tabs/windows
*
* @param {StorageEvent}
*/
storageEventHandler = event => {
if (event.key !== this.config.storageKey) {
return;
}
LogManager.getLogger('authentication').info('Stored token changed event');
let wasAuthenticated = this.authenticated;
this.authentication.responseAnalyzed = false;
this.updateAuthenticated();
if (this.config.storageChangedRedirect && wasAuthenticated !== this.authenticated) {
PLATFORM.location.assign(this.config.storageChangedRedirect);
}
}
/**
* Getter: The configured client for all aurelia-authentication requests
*
@@ -78,6 +103,12 @@ export class AuthService {
return this.config.client;
}
/**
* Getter: The authentication class instance
*
* @return {boolean}
* @deprecated
*/
get auth() {
LogManager.getLogger('authentication').warn('AuthService.auth is deprecated. Use .authentication instead.');
return this.authentication;
@@ -96,8 +127,14 @@ export class AuthService {
&& this.authentication.getAccessToken()
&& this.authentication.getRefreshToken()) {
this.updateToken();
} else {
this.logout(this.config.expiredRedirect);
return;
}
this.setResponseObject(null);
if (this.config.expiredRedirect) {
PLATFORM.location.assign(this.config.expiredRedirect);
}
}, ttl);
}
@@ -118,10 +155,17 @@ export class AuthService {
* @param {Object} response The servers response as GOJO
*/
setResponseObject(response) {
this.clearTimeout();
this.authentication.setResponseObject(response);
this.updateAuthenticated();
}
/**
* Update authenticated. Sets login status and timeout
*/
updateAuthenticated() {
this.clearTimeout();
let wasAuthenticated = this.authenticated;
this.authenticated = this.authentication.isAuthenticated();
@@ -202,12 +246,12 @@ export class AuthService {
}
/**
* Gets authentication status
* Gets authentication status from storage
*
* @returns {Boolean} For Non-JWT and unexpired JWT: true, else: false
*/
isAuthenticated() {
this.authentication.hasTokenAnalyzed = false;
this.authentication.responseAnalyzed = false;
let authenticated = this.authentication.isAuthenticated();
@@ -307,8 +351,8 @@ export class AuthService {
let content;
if (typeof arguments[0] === 'object') {
content = arguments[0];
options = arguments[1];
content = arguments[0];
options = arguments[1];
redirectUri = arguments[2];
} else {
content = {
@@ -342,9 +386,9 @@ export class AuthService {
let content;
if (typeof arguments[0] === 'object') {
content = arguments[0];
content = arguments[0];
optionsOrRedirectUri = arguments[1];
redirectUri = arguments[2];
redirectUri = arguments[2];
} else {
content = {
'email': emailOrCredentials,
@@ -370,15 +414,15 @@ export class AuthService {
/**
* logout locally and redirect to redirectUri (if set) or redirectUri of config. Sends logout request first, if set in config
*
* @param {[String]} [redirectUri] [optional redirectUri overwrite]
* @param {[String]} [redirectUri] [optional redirectUri overwrite]
*
* @return {Promise<>|Promise<Object>|Promise<Error>} Server response as Object
*/
logout(redirectUri) {
logout(redirectUri, query) {
let localLogout = response => new Promise(resolve => {
this.setResponseObject(null);
this.authentication.redirect(redirectUri, this.config.logoutRedirect);
this.authentication.redirect(redirectUri, this.config.logoutRedirect, query);
if (typeof this.onLogout === 'function') {
this.onLogout(response);
@@ -1,4 +1,5 @@
import {PLATFORM} from 'aurelia-pal';
import {buildQueryString} from 'aurelia-path';
import {inject} from 'aurelia-dependency-injection';
import {deprecated} from 'aurelia-metadata';
import jwtDecode from 'jwt-decode';
@@ -23,7 +24,7 @@ export class Authentication {
this.idToken = null;
this.payload = null;
this.exp = null;
this.hasTokenAnalyzed = false;
this.responseAnalyzed = false;
}
@@ -86,7 +87,7 @@ export class Authentication {
this.idToken = null;
this.payload = null;
this.exp = null;
this.hasTokenAnalyzed = false;
this.responseAnalyzed = false;
this.storage.remove(this.config.storageKey);
}
@@ -95,27 +96,27 @@ export class Authentication {
/* get data, update if needed first */
getAccessToken() {
if (!this.hasTokenAnalyzed) this.getDataFromResponse(this.getResponseObject());
if (!this.responseAnalyzed) this.getDataFromResponse(this.getResponseObject());
return this.accessToken;
}
getRefreshToken() {
if (!this.hasTokenAnalyzed) this.getDataFromResponse(this.getResponseObject());
if (!this.responseAnalyzed) this.getDataFromResponse(this.getResponseObject());
return this.refreshToken;
}
getIdToken() {
if (!this.hasTokenAnalyzed) this.getDataFromResponse(this.getResponseObject());
if (!this.responseAnalyzed) this.getDataFromResponse(this.getResponseObject());
return this.idToken;
}
getPayload() {
if (!this.hasTokenAnalyzed) this.getDataFromResponse(this.getResponseObject());
if (!this.responseAnalyzed) this.getDataFromResponse(this.getResponseObject());
return this.payload;
}
getExp() {
if (!this.hasTokenAnalyzed) this.getDataFromResponse(this.getResponseObject());
if (!this.responseAnalyzed) this.getDataFromResponse(this.getResponseObject());
return this.exp;
}
@@ -169,7 +170,7 @@ export class Authentication {
this.exp = this.payload ? parseInt(this.payload.exp, 10) : NaN;
this.hasTokenAnalyzed = true;
this.responseAnalyzed = true;
return {
accessToken: this.accessToken,
@@ -243,7 +244,7 @@ export class Authentication {
return providerLogin.open(this.config.providers[name], userData);
}
redirect(redirectUrl, defaultRedirectUrl) {
redirect(redirectUrl, defaultRedirectUrl, query) {
// stupid rule to keep it BC
if (redirectUrl === true) {
LogManager.getLogger('authentication').warn('DEPRECATED: Setting redirectUrl === true to actually *not redirect* is deprecated. Set redirectUrl === 0 instead.');
@@ -258,9 +259,9 @@ export class Authentication {
return;
}
if (typeof redirectUrl === 'string') {
PLATFORM.location.href = encodeURI(redirectUrl);
PLATFORM.location.href = encodeURI(redirectUrl + (query ? `?${buildQueryString(query)}` : ''));
} else if (defaultRedirectUrl) {
PLATFORM.location.href = defaultRedirectUrl;
PLATFORM.location.href = defaultRedirectUrl + (query ? `?${buildQueryString(query)}` : '');
}
}
}
@@ -58,9 +58,10 @@ export class BaseConfig {
loginOnSignup = true;
// If loginOnSignup == false: The SPA url to which the user is redirected after a successful signup (else loginRedirect is used)
signupRedirect = '#/login';
// redirect when token expires. 0 = don't redirect (default), 1 = use logoutRedirect, string = redirect there
expiredRedirect = 0;
// The SPA url to load when the token expires
expiredRedirect = '#/';
// The SPA url to load when the authentication status changed in other tabs/windows (detected through storageEvents)
storageChangedRedirect = '#/';
// API related options
// ===================
@@ -284,13 +285,31 @@ export class BaseConfig {
};
/* deprecated defaults */
/**
* @deprecated
*/
_authToken = 'Bearer';
/**
* @deprecated
*/
_responseTokenProp = 'access_token';
/**
* @deprecated
*/
_tokenName = 'token';
/**
* @deprecated
*/
_tokenRoot = false;
/**
* @deprecated
*/
_tokenPrefix = 'aurelia';
/* deprecated methods and parameteres */
/**
* @deprecated
*/
set authToken(authToken) {
LogManager.getLogger('authentication').warn('BaseConfig.authToken is deprecated. Use BaseConfig.authTokenType instead.');
this._authTokenType = authToken;
@@ -301,6 +320,9 @@ export class BaseConfig {
return this._authTokenType;
}
/**
* @deprecated
*/
set responseTokenProp(responseTokenProp) {
LogManager.getLogger('authentication').warn('BaseConfig.responseTokenProp is deprecated. Use BaseConfig.accessTokenProp instead.');
this._responseTokenProp = responseTokenProp;
@@ -311,6 +333,9 @@ export class BaseConfig {
return this._responseTokenProp;
}
/**
* @deprecated
*/
set tokenRoot(tokenRoot) {
LogManager.getLogger('authentication').warn('BaseConfig.tokenRoot is deprecated. Use BaseConfig.accessTokenRoot instead.');
this._tokenRoot = tokenRoot;
@@ -321,6 +346,9 @@ export class BaseConfig {
return this._tokenRoot;
}
/**
* @deprecated
*/
set tokenName(tokenName) {
LogManager.getLogger('authentication').warn('BaseConfig.tokenName is deprecated. Use BaseConfig.accessTokenName instead.');
this._tokenName = tokenName;
@@ -331,6 +359,9 @@ export class BaseConfig {
return this._tokenName;
}
/**
* @deprecated
*/
set tokenPrefix(tokenPrefix) {
LogManager.getLogger('authentication').warn('BaseConfig.tokenPrefix is obsolete. Use BaseConfig.storageKey instead.');
this._tokenPrefix = tokenPrefix;
@@ -340,20 +371,26 @@ export class BaseConfig {
return this._tokenPrefix || 'aurelia';
}
/**
* @deprecated
*/
get current() {
LogManager.getLogger('authentication').warn('Getter BaseConfig.current is deprecated. Use BaseConfig directly instead.');
return this;
}
set current(_) {
throw new Error('Setter BaseConfig.current is obsolete. Use BaseConfig directly instead.');
throw new Error('Setter BaseConfig.current has been removed. Use BaseConfig directly instead.');
}
/**
* @deprecated
*/
get _current() {
LogManager.getLogger('authentication').warn('Getter BaseConfig._current is deprecated. Use BaseConfig directly instead.');
return this;
}
set _current(_) {
throw new Error('Setter BaseConfig._current is obsolete. Use BaseConfig directly instead.');
throw new Error('Setter BaseConfig._current has been removed. Use BaseConfig directly instead.');
}
}
Oops, something went wrong.

0 comments on commit 52c2f67

Please sign in to comment.