Home Reference Source

core/identity/mic.js

import Promise from 'es6-promise';
import isString from 'lodash/isString';
import url from 'url';
import urljoin from 'url-join';
import { AuthType, RequestMethod, KinveyRequest } from '../request';
import { KinveyError, MobileIdentityConnectError } from '../errors';
import { isDefined } from '../utils';
import { Popup as CorePopup } from './popup';
import { Identity } from './identity';
import { SocialIdentity } from './enums';

let Popup = CorePopup;

/**
 * Enum for Mobile Identity Connect authorization grants.
 * @property  {string}    AuthorizationCodeLoginPage   AuthorizationCodeLoginPage grant
 * @property  {string}    AuthorizationCodeAPI         AuthorizationCodeAPI grant
 */
export const AuthorizationGrant = {
  AuthorizationCodeLoginPage: 'AuthorizationCodeLoginPage',
  AuthorizationCodeAPI: 'AuthorizationCodeAPI'
};
Object.freeze(AuthorizationGrant);

/**
 * @private
 */
export class MobileIdentityConnect extends Identity {
  get identity() {
    return SocialIdentity.MobileIdentityConnect;
  }

  static get identity() {
    return SocialIdentity.MobileIdentityConnect;
  }

  static isSupported() {
    return true;
  }

  isSupported() {
    return true;
  }

  _getMicPath(options){
    let pathname = '/oauth/auth';
    let version = 3;

    if (options.version) {
      version = options.version;
    }

    if (isString(version) === false) {
      version = String(version);
    }

    return urljoin(version.indexOf('v') === 0 ? version : `v${version}`, pathname);
  }

  login(redirectUri, authorizationGrant = AuthorizationGrant.AuthorizationCodeLoginPage, options = {}) {
    if (!isString(redirectUri)) {
      return Promise.reject(new KinveyError('A redirectUri is required and must be a string.'));
    }

    let clientId = this.client.appKey;

    if (isString(options.micId)) {
      clientId = `${clientId}.${options.micId}`;
    }

    const promise = Promise.resolve()
      .then(() => {
        if (authorizationGrant === AuthorizationGrant.AuthorizationCodeLoginPage) {
          // Step 1: Request a code
          return this.requestCodeWithPopup(clientId, redirectUri, options);
        } else if (authorizationGrant === AuthorizationGrant.AuthorizationCodeAPI) {
          // Step 1a: Request a temp login url
          return this.requestTempLoginUrl(clientId, redirectUri, options)
            .then(url => this.requestCodeWithUrl(url, clientId, redirectUri, options)); // Step 1b: Request a code
        }

        throw new KinveyError(`The authorization grant ${authorizationGrant} is unsupported. ` +
          'Please use a supported authorization grant.');
      })
      .then(code => this.requestToken(code, clientId, redirectUri, options)) // Step 3: Request a token
      .then((session) => {
        session.identity = MobileIdentityConnect.identity;
        session.client_id = clientId;
        session.redirect_uri = redirectUri;
        session.protocol = this.client.micProtocol;
        session.host = this.client.micHost;
        return session;
      });

    return promise;
  }

  refresh(token, clientId, redirectUri, options = {}) {
    return Promise.resolve()
      .then(() => this.refreshToken(token, clientId, redirectUri, options))
      .then((session) => {
        session.identity = MobileIdentityConnect.identity;
        session.client_id = clientId;
        session.redirect_uri = redirectUri;
        session.protocol = this.client.micProtocol;
        session.host = this.client.micHost;
        return session;
      });
  }

  requestTempLoginUrl(clientId, redirectUri, options = {}) {
    const request = new KinveyRequest({
      method: RequestMethod.POST,
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      url: url.format({
        protocol: this.client.micProtocol,
        host: this.client.micHost,
        pathname: this._getMicPath(options)
      }),
      properties: options.properties,
      body: {
        client_id: clientId,
        redirect_uri: redirectUri,
        response_type: 'code'
      }
    });
    return request.execute()
      .then(response => response.data.temp_login_uri);
  }

  requestCodeWithPopup(clientId, redirectUri, options = {}) {
    const promise = Promise.resolve().then(() => {
      const popup = new Popup();
      return popup.open(url.format({
        protocol: this.client.micProtocol,
        host: this.client.micHost,
        pathname: this._getMicPath(options),
        query: {
          client_id: clientId,
          redirect_uri: redirectUri,
          response_type: 'code',
          scope: 'openid'
        }
      }));
    }).then((popup) => {
      const promise = new Promise((resolve, reject) => {
        let redirected = false;

        function loadCallback(event) {
          try {
            if (event.url && event.url.indexOf(redirectUri) === 0 && redirected === false) {
              const parsedUrl = url.parse(event.url, true);
              const query = parsedUrl.query || {};

              redirected = true;
              popup.removeAllListeners();
              popup.close();

              if (query.code) {
                resolve(query.code);
              } else if (query.error) {
                reject(new KinveyError(query.error, query.error_description));
              } else {
                reject(new KinveyError('The redirect uri did not contain a code or error.'))
              }
            }
          } catch (error) {
            // Just catch the error
          }
        }

        function errorCallback(event) {
          try {
            if (event.url && event.url.indexOf(redirectUri) === 0 && redirected === false) {
              const parsedUrl = url.parse(event.url, true);
              const query = parsedUrl.query || {};

              redirected = true;
              popup.removeAllListeners();
              popup.close();

              if (query.code) {
                resolve(query.code);
              } else if (query.error) {
                reject(new KinveyError(query.error, query.error_description));
              } else {
                reject(new KinveyError('The redirect uri did not contain a code or error.'))
              }
            } else if (redirected === false) {
              popup.removeAllListeners();
              popup.close();
              reject(new KinveyError(event.message, '', event.code));
            }
          } catch (error) {
            // Just catch the error
          }
        }

        function exitCallback() {
          if (redirected === false) {
            popup.removeAllListeners();
            reject(new KinveyError('Login has been cancelled.'));
          }
        }

        popup.on('loadstart', loadCallback);
        popup.on('loadstop', loadCallback);
        popup.on('error', errorCallback);
        popup.on('exit', exitCallback);
      });
      return promise;
    });

    return promise;
  }

  requestCodeWithUrl(loginUrl, clientId, redirectUri, options = {}) {
    const promise = Promise.resolve().then(() => {
      const request = new KinveyRequest({
        method: RequestMethod.POST,
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        url: loginUrl,
        properties: options.properties,
        body: {
          client_id: clientId,
          redirect_uri: redirectUri,
          response_type: 'code',
          username: options.username,
          password: options.password,
          scope: 'openid'
        },
        followRedirect: false
      });
      return request.execute();
    }).then((response) => {
      const location = response.headers.get('location');

      if (location) {
        return url.parse(location, true).query.code;
      }

      throw new MobileIdentityConnectError(`Unable to authorize user with username ${options.username}.`,
        'A location header was not provided with a code to exchange for an auth token.');
    });

    return promise;
  }

  requestToken(code, clientId, redirectUri, options = {}) {
    const request = new KinveyRequest({
      method: RequestMethod.POST,
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      authType: AuthType.Client,
      url: url.format({
        protocol: this.client.micProtocol,
        host: this.client.micHost,
        pathname: '/oauth/token'
      }),
      properties: options.properties,
      body: {
        grant_type: 'authorization_code',
        client_id: clientId,
        redirect_uri: redirectUri,
        code: code
      },
      clientId: clientId
    });
    return request.execute().then(response => response.data);
  }

  refreshToken(token, clientId, redirectUri, options = {}) {
    const request = new KinveyRequest({
      method: RequestMethod.POST,
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      authType: AuthType.Client,
      url: url.format({
        protocol: this.client.micProtocol,
        host: this.client.micHost,
        pathname: '/oauth/token'
      }),
      body: {
        grant_type: 'refresh_token',
        client_id: clientId,
        redirect_uri: redirectUri,
        refresh_token: token
      },
      clientId: clientId,
      properties: options.properties,
      timeout: options.timeout
    });
    return request.execute().then(response => response.data);
  }

  logout(user, options = {}) {
    return Promise.resolve();
  }

  /**
   * @private
   */
  static usePopupClass(PopupClass) {
    if (isDefined(PopupClass)) {
      Popup = PopupClass;
    }
  }
}