Skip to content

Instantly share code, notes, and snippets.

@pauln pauln/README.md
Last active Feb 13, 2019

Embed
What would you like to do?
ember-simple-auth oauth2 implicit grant authenticator

oauth2 Implicit Grant authenticator for ember-simple-auth

This is a sample ember-simple-auth authenticator implementation for the oauth2 Implicit Grant which implements "silent reauthentication" (fetching a new token from the IDP via the prompt=none flow). It also uses ember-master-tab to run the refresh process in only a single tab (if the application is open in multiple tabs); at time of writing, it's necessary to use the master branch rather than the version published to npm as it makes use of a recent change to try to recover from the master tab crashing (as opposed to being closed cleanly).

This implementation also expects the token to be a JWT; you may need to adjust the token-related parts if you're not using JWTs.

So how do I use this?

This gist is not a fully developed, drop-in, ready-to-use implementation. It's intended as a starting point for your own implementation, so you'll need to do some work yourself to use it - including (but not necessarily limited to):

  • Add ember-master-tab to your Ember project, if you're not already using it
    • Switch to the master branch if you're running v1.0.0 or earlier
  • Add the files in this gist (excluding this readme) to the indicated locations in your Ember app
    • Strip out the comment from the top of silent-callback.html - it's just there to tell you where to put the file
    • You may also want to remove the filename comments from the other files, but they're not critical
  • Configure the relevant parameters (oauth endpoint, client ID, requested scope)
    • client ID is currently pulled from Ember environment config (oauthClientID) - either set it there or adjust to suit
  • Send users to the authorize endpoint, with a redirect URI pointing to the callback route, in order to authenticate
  • Implement a login-failed route which displays errors (based on the error query parameter, if desired)
// app/routes/callback.js
import Ember from 'ember';
import OAuth2ImplicitGrantCallbackRouteMixin from 'ember-simple-auth/mixins/oauth2-implicit-grant-callback-route-mixin';
const { Route } = Ember;
export default Route.extend(OAuth2ImplicitGrantCallbackRouteMixin, {
authenticator: 'authenticator:oauth2-implicit-grant',
handleError: Ember.observer('error', function() {
let err = this.get('error');
if (err && err.length) {
this.transitionTo(`/login-failed?error=${err}`);
}
})
});
// 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);
}
});
<!-- public/silent-callback.html -->
<!doctype html>
<html>
<head>
<title>Silent callback</title>
<script type="text/javascript">
window.parent._ESA_Oauth2_Process_Implicit_Refresh(window.location.hash);
</script>
</head>
<body>
<h1>Silent callback</h1>
</body>
</html>
@WillEngler

This comment has been minimized.

Copy link

commented May 26, 2017

// All the routers!
router.router.router.transitionTo('login-failed', {queryParams: {error: 'session_timeout'}});

I got a kick out of that

@fredguth

This comment has been minimized.

Copy link

commented Feb 13, 2019

What does (!! frame) mean? I have never seen !! operator.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.