cybertooth-io/ember-simple-auth-aws-amplify

View on GitHub
addon/authenticators/aws-amplify-authenticator.js

Summary

Maintainability
A
0 mins
Test Coverage
import { inject as service } from '@ember/service';
import { assign } from '@ember/polyfills';
import { task, timeout } from 'ember-concurrency';
import { getOwner } from '@ember/application';
import BaseAuthenticator from 'ember-simple-auth/authenticators/base';

/**
 * An implementation of `ember-simple-auth` Authenticator.
 *
 * Guess what?  You won't ever invoke the `authenticate()` function.  You'll instead use
 * the `session`'s `signIn(...)` and `confirmSignIn(...)` functions.
 */
export default BaseAuthenticator.extend({

  /**
   * Implementation of the `authenticate` method is dead simple; ask AWS Amplify's `Auth` to
   * reach out to AWS Cognito User Pool and grab the `currentAuthenticatedUser(...)` which returns a
   * `CognitoUser` instance.  User details are then returned from the success-side of this request
   * for the `currentAuthenticatedUser(...)`.
   *
   * This might seem odd, where's the user's credentials and all that jazz?  Because of MFA we can't
   * use the `authenticate()` method as we have in other _auth_ implementations.  As such the
   * `session` service has a `signIn(...)` and `confirmSignIn(...)` function.
   *
   * If simple single-factor authentication is configured for the user, that will be detected
   * by the `signIn(...)` function and simply invoke this `authenticate()` call automatically.  Should
   * multi-factor authentication be detected during the `signIn(...)`, this `authenticate()` function
   * will be called once the user provides the appropriate passcode to the `confirmSignIn(...)` function
   * of the `session`.
   *
   * When `authenticate()` is called, it should be noted that an `ember-concurrency` timed task is created
   * that will refresh the session's token according to the `exp` value that was returned with the access token
   * payload.
   *
   * @return {Promise<any>} the promise from #_refreshUser
   */
  authenticate() {
    return this._refreshUser();
  },

  /**
   * A singleton service that provides universal access to AWS Amplify's `Auth` instance.
   */
  awsAmplify: service('aws-amplify.auth'),

  /**
   * Takes no arguments.
   *
   * AWS Amplify's `Auth.signOut()` is called to destroy the session in the AWS Cognito Pool.  The browser's
   * local storage is purged of all of the session data that was stored by AWS Amplify.
   *
   * The `ember-concurrency` task that was refreshing the access token periodically will be
   * cancelled.
   *
   * @return {Promise<any>}
   * @see https://aws-amplify.github.io/amplify-js/api/classes/authclass.html#signout
   */
  invalidate(/*data*/) {
    return this.get('awsAmplify.auth')
      .signOut()
      .finally(() => this.get('_refreshAccessTokenTask').cancelAll());
  },

  /**
   * Similar to `authenticate()` we let AWS Amplify's `Auth` instance do all the heavy lifting here.
   * The `CognitoUser` instance will be summoned by a call to the `currentAuthenticatedUser(...)`.  The
   * resulting information will be placed into local storage and returned to the `session.data.authenticated`
   * property storage.
   *
   * @return {Promise<any>} the promise from #_refreshUser
   */
  restore(/*data*/) {
    return this._refreshUser();
  },

  /* Private
   * ---------------------------------------------------------------------------------------------------------------- */

  /**
   * This is an `ember-concurrency` task that waits until access token expiry before triggering
   * a `getOwner(this).lookup('session:main').restore()` on the session.  I saw this technique being
   * employed in `ember-simple-auth/addon/initializers/setup-session-restoration.js`.
   *
   * The reason I needed to do this was so that the `restore()` invocation actually altered and replaced
   * the `session.data.authenticated` information.  Simply calling `this.restore()` does not do what I require.
   * I am a bit of a dummy though.
   *
   * @private
   */
  _refreshAccessTokenTask: task(function* (exp) {
    const wait = exp * 1000 - Date.now();
    console.warn('Scheduled token refresh will occur at ', new Date(exp * 1000));

    yield timeout(wait);

    console.warn('Commencing refresh of the access token at ', new Date());
    return getOwner(this).lookup('session:main').restore();   // TODO: try this.restore()?  trigger restore events?
  }),

  /**
   * Grab the current authenticated user from AWS Cognito authentication server directly; do not use any
   * cached information in local storage/cookie.  Merge the attributes, idPayload, accessPayload,
   * and preferredMFA into a hash that is returned from the success side of the returned promise.
   *
   * Setup a ember-concurrency timed task that will refresh the current user from AWS Cognito automatically in
   * the background.
   *
   * Here's a mapping of `CognitoUser` properties to Ember-Simple-Auth `session.data.authenticated` properties:
   *
   * `cognitoUser.signInUserSession.accessToken.payload` -> `session.data.authenticated.accessPayload`
   * `cognitoUser.signInUserSession.accessToken.jwtToken` -> `session.data.authenticated.accessToken`
   * `cognitoUser.attributes` -> `session.data.authenticated.attributes`
   * `cognitoUser.signInUserSession.idToken.payload` -> `session.data.authenticated.idPayload`
   * `cognitoUser.signInUserSession.idToken.jwtToken` -> `session.data.authenticated.idToken`
   * `cognitoUser.preferredMFA` -> `session.data.authenticated.preferredMFA`
   *
   * @return {Promise<any>} the processed `CognitoUser` information turned into a hash that will be written
   * to the `session.data.authenticated` property storage.
   * @private
   */
  _refreshUser() {
    return this.get('awsAmplify.auth')
      .currentAuthenticatedUser({ bypassCache: true })
      .then(cognitoUser => {
        this.get('_refreshAccessTokenTask').cancelAll();
        this.get('_refreshAccessTokenTask').perform(cognitoUser.signInUserSession.accessToken.payload.exp);
        const data = assign({ accessPayload: cognitoUser.signInUserSession.accessToken.payload }, { accessToken: cognitoUser.signInUserSession.accessToken.jwtToken });
        assign(data, { attributes: cognitoUser.attributes });
        assign(data, { idPayload: cognitoUser.signInUserSession.idToken.payload });
        assign(data, { idToken: cognitoUser.signInUserSession.idToken.jwtToken });
        assign(data, { preferredMFA: cognitoUser.preferredMFA });
        return data;
      });
  }
});