|
// app/authenticators/oauth2-implicit-grant.js |
|
import Ember from 'ember'; |
|
import OAuth2ImplicitGrant from 'ember-simple-auth/authenticators/oauth2-implicit-grant'; |
|
import OAuth2ImplicitGrantMixin from 'ember-simple-auth/mixins/oauth2-implicit-grant-callback-route-mixin'; |
|
import config from '../config/environment'; |
|
|
|
const { inject: { service }, isEmpty, isPresent } = Ember; |
|
|
|
export default OAuth2ImplicitGrant.extend(OAuth2ImplicitGrantMixin, { |
|
masterTab: service(), |
|
session: service(), |
|
routing: service('-routing'), |
|
|
|
tokenPropertyName: 'access_token', |
|
tokenExpireName: 'exp', |
|
refreshAccessTokens: true, |
|
refreshLeeway: 240, |
|
refreshFrame: null, |
|
backupTimer: null, |
|
authorizeUrl: 'http://localhost:8080/oauth/authorize', |
|
requestedScope: 'global', |
|
|
|
init() { |
|
// Set up callback for hidden iframe refresh flow |
|
window._ESA_Oauth2_Process_Implicit_Refresh = (hash) => { |
|
let frame = this.get('refreshFrame'); |
|
let args = this._parseResponse(hash); |
|
let backupTimer = this.get('backupTimer'); |
|
let router = this.get('routing'); |
|
|
|
// Authenticate using provided hash data |
|
this.get('session').authenticate('authenticator:OAuth2ImplicitGrant', args).catch((err) => { |
|
// All the routers! |
|
router.router.router.transitionTo('login-failed', {queryParams: {error: 'session_timeout'}}); |
|
}); |
|
|
|
// Remove iframe / reset reference |
|
if (!!frame) { |
|
frame.remove(); |
|
this.set('refreshFrame', null); |
|
} |
|
|
|
// Remove backup schedule |
|
if (!!backupTimer) { |
|
Ember.run.cancel(backupTimer); |
|
this.set('backupTimer', null); |
|
} |
|
} |
|
}, |
|
|
|
authenticate(hash) { |
|
// Use parent authenticator, then set up for token refresh as appropriate |
|
return this._super(...arguments).then((data) => { |
|
return this.initTokenRefresh(data) |
|
}); |
|
}, |
|
|
|
restore(data) { |
|
return this.initTokenRefresh(data); |
|
}, |
|
|
|
initTokenRefresh(data) { |
|
const dataObject = Ember.Object.create(data); |
|
|
|
return new Ember.RSVP.Promise((resolve, reject) => { |
|
const now = this.getCurrentTime(); |
|
const token = dataObject.get(this.tokenPropertyName); |
|
let expiresAt = dataObject.get(this.tokenExpireName); |
|
|
|
if (isEmpty(token)) { |
|
return reject(new Error('empty token')); |
|
} |
|
|
|
if (isEmpty(expiresAt)) { |
|
// Fetch the expire time from the token data since `expiresAt` |
|
// wasn't included in the data object that was passed in. |
|
const tokenData = this.getTokenData(token); |
|
|
|
expiresAt = tokenData[this.tokenExpireName]; |
|
if (isEmpty(expiresAt)) { |
|
return resolve(data); |
|
} |
|
} |
|
|
|
if (expiresAt > now) { |
|
const wait = expiresAt - now - this.refreshLeeway; |
|
|
|
if (wait > 0) { |
|
if (this.refreshAccessTokens) { |
|
Ember.run.once(this, 'scheduleAccessTokenRefresh', wait); |
|
} |
|
return resolve(data); |
|
} else if (this.refreshAccessTokens) { |
|
// Token is still valid, but due to be refreshed. |
|
// Try getting a new one - session will be updated if successful. |
|
Ember.run.once(this, 'refreshAccessToken'); |
|
return resolve(data); |
|
} else { |
|
return reject(new Error('unable to refresh token')); |
|
} |
|
} else if (isPresent(token)) { |
|
// Fetch the expire time from the auth token data. |
|
const tokenData = this.getTokenData(token); |
|
let tokenExpiresAt = tokenData[this.tokenExpireName]; |
|
|
|
if (isPresent(tokenExpiresAt)) { |
|
if (tokenExpiresAt > now && this.refreshAccessTokens) { |
|
// Token is still valid, but due to be refreshed. |
|
// Try getting a new one - session will be updated if successful. |
|
Ember.run.once(this, 'refreshAccessToken'); |
|
return resolve(data); |
|
} else { |
|
return reject(new Error('Token is expired')); |
|
} |
|
} |
|
} else { |
|
return reject(new Error('token is expired')); |
|
} |
|
}); |
|
}, |
|
|
|
/** |
|
Returns the current time as a timestamp in seconds |
|
@method getCurrentTime |
|
@return {Integer} timestamp |
|
*/ |
|
getCurrentTime() { |
|
return Math.floor((new Date()).getTime() / 1000); |
|
}, |
|
|
|
/** |
|
Returns the decoded token with accessible returned values. |
|
|
|
@method getTokenData |
|
@return {object} An object with properties for the session. |
|
*/ |
|
getTokenData(token) { |
|
const payload = token.split('.')[1]; |
|
const tokenData = decodeURIComponent(window.escape(atob(payload))); |
|
|
|
try { |
|
return JSON.parse(tokenData); |
|
} catch (e) { |
|
return tokenData; |
|
} |
|
}, |
|
|
|
/** |
|
* Triggers a token refresh, if this is the master tab. |
|
* Otherwise, triggers a master tab contest in case the master tab has died. |
|
*/ |
|
refreshAccessToken() { |
|
const masterTab = this.get('masterTab'); |
|
|
|
masterTab.run(() => { |
|
this.doRefreshAccessToken(); |
|
}).else(() => { |
|
masterTab.contestMasterTab(); |
|
}) |
|
}, |
|
|
|
/** |
|
Attempts to silently fetch a new access token using prompt=none |
|
|
|
@method refreshAccessToken |
|
@private |
|
*/ |
|
doRefreshAccessToken() { |
|
let clientId = config.oauthClientID; |
|
let redirectURI = `${window.location.origin}/silent-callback.html`; |
|
let responseType = `token`;// `token id_token` |
|
let scope = this.get('requestedScope'); |
|
let authorizeUrl = this.get('authorizeUrl'); |
|
let refreshUrl = `${authorizeUrl}?` |
|
+ `client_id=${clientId}` |
|
+ `&redirect_uri=${redirectURI}` |
|
+ `&response_type=${responseType}` |
|
+ `&scope=${scope}&state=foobar123` |
|
+ `&prompt=none` |
|
; |
|
|
|
let frame = this.get('refreshFrame'); |
|
if (!!frame) { |
|
// Refresh already in progress? |
|
return; |
|
} |
|
|
|
frame = Ember.$(`<iframe style="display:none" src="${refreshUrl}"></iframe>`); |
|
this.set('refreshFrame', frame); |
|
Ember.$('body').append(frame); |
|
}, |
|
|
|
scheduleAccessTokenRefresh(delay) { |
|
let leeway = this.get('refreshLeeway'); |
|
let additionalDelay = Math.floor(0.5 * leeway); |
|
|
|
// Schedule main refresh attempt |
|
Ember.run.later(this, 'refreshAccessToken', delay * 1000); |
|
|
|
// Schedule backup refresh attempt (in case master tab fails / is replaced) |
|
let timer = Ember.run.later(this, 'refreshAccessToken', (delay + additionalDelay) * 1000); |
|
this.set('backupTimer', timer); |
|
} |
|
}); |
I think it is a nasty way of making sure you have a
boolean
there. It is double negation, the first one "casts" the variable to aboolean
but the negated value, so you negate again...