Source: authorization/sso-authorization.js

/**
 * Copyright© 2016, Oracle and/or its affiliates. All rights reserved.
 * Created by ddrobins on 7/28/15.
 */



/**
 * Class used to authorize a mobile user against Oracle Mobile Cloud Service. Callers should use
 * MobileBackend's [SSOAuthAuthorization()]{@link MobileBackend#authorization} property.
 * Derives from {@link Authorization}.
 * @constructor
 * @global
 */

function SSOAuthorization(config, backend, appKey, utils, platform, logger) {
  var HEADERS = utils.HEADERS;

  Authorization.call(this, backend, appKey, utils, platform, logger);

  var _clientId = utils.validateConfiguration(config.clientId);
  var _clientSecret = utils.validateConfiguration(config.clientSecret);

  var domains = {};

  if(config.hasOwnProperty('userIdentityDomainName')){
    var _tenantName = utils.validateConfiguration(config.userIdentityDomainName);
  }

  var expiredTime = null;

  var _this = this;


  /**
   * Returns the client ID for the current backend.
   * @type {String}
   */
  this.getClientId = function(){
    return _clientId;
  };

  /**
   * Returns the tenant name for the current backend.
   * @type {String}
   */
  this.getTenantName = function(){
    return _tenantName;
  };

  /**
   * Returns the client secret for the current backend.
   * @type {String}
   */
  this.getClientSecret= function(){
    return _clientSecret;
  };


     /**
     * Authenticates a user with the given credentials using federated single sign-on. The user remains logged in until logout() is called.
     * You must have SSO enabled for the mobile backend.
     * @param [successCallback] {Authorization~authenticateSuccessCallback} Optional callback invoked on success (deprecated use promises instead).
     * @param [errorCallback] {Authorization~errorCallback} Optional callback invoked on failure (deprecated use promises instead).
     * @return {Promise.<NetworkResponse|NetworkResponse>}
     */
  this.authenticate = function(successCallback, errorCallback) {

    this.logout();

    if (window.cordova) {
      var metadata = cordova.require('cordova/plugin_list').metadata;

      if (IsInAppBrowserInstalled(metadata) !== true) {
        if (errorCallback != null) {
          errorCallback(100, 'Could not find InAppBrowser, use command "cordova plugin add cordova-plugin-inappbrowser"');
          return undefined;
        } else {
          return Promise.reject(new NetworkResponse(100, 'Could not find InAppBrowser, use command "cordova plugin add cordova-plugin-inappbrowser"'));
        }
      } else if (!metadata.hasOwnProperty('oracle-mobile-cloud-cookies')) {
        if (errorCallback != null) {
          errorCallback(100, 'Could not find oracle-mobile-cloud-cookies plugin, use command "cordova plugin add {path to oracle plugin}oracle-mobile-cloud-cookies"');
          return undefined;
        } else {
          return Promise.reject(new NetworkResponse(100, 'Could not find oracle-mobile-cloud-cookies plugin, use command "cordova plugin add {path to oracle plugin}oracle-mobile-cloud-cookies"'));
        }
      } else {
        return authenticateInvoke(successCallback, errorCallback);
      }
    } else {
      if (errorCallback != null) {
        errorCallback(400, 'Bad Request - This method require Cordova framework');
        return undefined;
      } else {
        return Promise.reject(400, 'Bad Request - This method require Cordova framework');
      }
    }
  };

  function authenticateInvoke(successCallback, errorCallback){

    return new Promise(invoke)
      .then(invokeSuccess, invokeError);

    function invoke(resolve, reject){
      var sso_token = {};
      var clientId = _this.getClientId();
      var flowUrl = backend.getPlatformUrl('sso/token') + '?clientID=' + clientId + '&format=json';
      var browserRef = window.open(flowUrl, '_blank', 'location=no,clearsessioncache=yes,clearcache=yes');
      browserRef.show();

      logger.info('Opening InAppBrowser to url: ' + flowUrl);

      domains = {};

      // hide in app browser when start load last page with token
      var skipFirstLoad = true;
      browserRef.addEventListener('loadstart', function (event) {
        if (utils.isEquivalentURL(event.url, flowUrl)) {
          if(!skipFirstLoad) {
            browserRef.hide();
          }
          skipFirstLoad = false;
        }
      });

      browserRef.addEventListener('loadstop', function (event) {
        var domain = extractDomain(event.url);
        if (!domains[domain]) {
          domains[domain] = true;
        }
        if (utils.isEquivalentURL(event.url, flowUrl)) {
          console.log('Domains object:', domains);
          browserRef.executeScript({code: 'document.body.innerHTML'},
            function (htmlarray) {
              browserRef.close();
              var html = htmlarray[0];
              var start = html.indexOf('{');
              var end = html.lastIndexOf('}') + 1;
              var json = html.substring(start, end);
              sso_token = JSON.parse(json);
              if (sso_token.access_token !== undefined && sso_token.access_token !== null) {
                resolve(new NetworkResponse(200, sso_token));
              } else if (_this._getIsAuthorized() !== true && sso_token.status >= 401) {
                reject(new NetworkResponse(100, 'Cannot authenticate via a web browser'));
              } else {
                reject(new NetworkResponse(sso_token.status, sso_token))
              }
            });
        }
      });

      browserRef.addEventListener('exit', function () {
        if (_this._getIsAuthorized() !== true || sso_token.status) {
          reject(new NetworkResponse(100, 'Cannot authenticate via a web browser'));
        }
      });
    }

    function invokeSuccess(response){
      _this._authenticateSuccess(response, response.data.access_token);
      expiredTime = Date.now() + response.data.expires_in * 1000;

      if (successCallback) {
        successCallback(response.statusCode, response.data);
      }
      return response;
    }

    function invokeError(response){
      _this._authenticateError(response);
      if (errorCallback) {
        errorCallback(response.statusCode, response.data);
      } else {
        return Promise.reject(response);
      }
    }
  }

     /**
     * Authenticates an anonymous user against the service. The user remains logged in until logout() is called.
     * @param [successCallback] {Authorization~authenticateSuccessCallback} Optional callback invoked on success (deprecated use promises instead).
     * @param [errorCallback] {Authorization~errorCallback} Optional callback invoked on failure (deprecated use promises instead).
     * @return {Promise.<NetworkResponse|NetworkResponse>}
     */
  this.authenticateAnonymous = function (successCallback, errorCallback) {
    var authorizationToken = 'Basic ' + utils.encodeBase64(this.getClientId() + ':' + this.getClientSecret());
    var headers = {};
    headers[HEADERS.CONTENT_TYPE] = 'application/x-www-form-urlencoded; charset=utf-8';
    if (typeof this.getTenantName() !== 'undefined') {
      headers[HEADERS.X_USER_IDENTITY_DOMAIN_NAME] = this.getTenantName();
    }

    return this._authenticateAnonymousInvoke(authorizationToken,
      headers,
      backend.getSSOAuthTokenUrl(),
      utils.HTTP_METHODS.POST,
      'grant_type=client_credentials')
      .then(invokeServiceSuccess, invokeServiceError);

    function invokeServiceSuccess(response) {
      expiredTime = Date.now() + response.data.expires_in * 1000;

      if (successCallback) {
        successCallback(response.statusCode, response.data);
      }
      return response;
    }

    function invokeServiceError(response) {
      if(errorCallback) {
        errorCallback(response.statusCode, response.data);
      } else {
        return Promise.reject(response);
      }
    }
  };

  this._anonymousTokenResponseConverter = function(response){
    return { orgResponse: response.orgResponse, anonymousAccessToken: response.orgResponse.data.access_token };
  };

  /**
   * Checks to see if the correct plugin is installed into the application.
   * @return {boolean}
   */
  var IsInAppBrowserInstalled = function(metadata){

    var inAppBrowserNames = ['cordova-plugin-inappbrowser', 'org.apache.cordova.inappbrowser'];
    return inAppBrowserNames.some(function(name) {
      return metadata.hasOwnProperty(name);
    });
  };

  /**
   * Clears the current session cookies and logs out the user.
   */
  var clearCookies = function(){
    var metadata = cordova.require('cordova/plugin_list').metadata;

    if (!metadata.hasOwnProperty('oracle-mobile-cloud-cookies')) {
      logger.error('Could not find oracle-mobile-cloud-cookies plugin, use command "cordova plugin add {path to oracle plugin}oracle-mobile-cloud-cookies"');
      return;
    }
    // TODO: check what to do with domains array
    // clean the cookie under the sso portal domain, which is secure and could not be read
    var mainUrl = backend.getPlatformUrl('sso/token');
    var mainDomain = extractDomain(mainUrl);
    var cookieName = 'OAMAuthnCookie_' + (mainUrl.indexOf('https') === 0 ? 'https' : 'http');
    clearCookie(mainDomain, cookieName);

    cookieName = 'OAMAuthnCookie_' + mainDomain;
    if(mainUrl.indexOf('443') >= 0){
      cookieName += ':443'
    }
    clearCookie(mainDomain, cookieName);

    for(var domain in domains){
      if(domains.hasOwnProperty(domain)){
        removeCookies(domain);
      }
    }
  };

  function clearCookie(domain, cookieName){
    cordova.plugins.MCSCookies.set(domain, cookieName, '', setSuccess, setError);

    // Clear for SSO token
    function setSuccess(message) {
      console.log('Cookie ' + cookieName + ' set successful for domain:' + domain, message);
    }
    function setError() { }
  }

  function removeCookies(domain) {
    cordova.plugins.MCSCookies.remove(domain, '^(?!OAM_PREFS)(OAM.*)|^ORA_OSFS_SESSION$', removeSuccess, removeError);

    function removeSuccess(message) {
      console.log('Cookies removed successful for domain:' + domain, message);
    }

    function removeError() { }
  }


  function extractDomain(url) {
    var domain;
    //find & remove protocol (http, ftp, etc.) and get domain
    if (url.indexOf('://') > -1) {
      domain = url.split('/')[2];
    }
    else {
      domain = url.split('/')[0];
    }

    //find & remove port number
    domain = domain.split(':')[0];

    return domain;
  }

  /**
   * Checks to see if the OAuth token is null,undefined,NaN,empty string (''),0,false and also checks the timestamp
   * of when the token was first retrieved to see if it was still valid.
   * @returns {Boolean}
   */
  this.isTokenValid = function () {

    if (this.getAccessToken() || this._getAnonymousAccessToken()) {
      logger.verbose( 'Token is not null or empty');

      var currentTime = Date.now();
      if (currentTime >= expiredTime) {
        logger.info( 'Token has expired or user has not been authenticate with the service');
        return false;
      }
      else {
        logger.verbose( 'Token is still valid');
        return true;
      }
    } else {
      return false;
    }
  };

  /**
   * Logs out the current user and clears credentials and tokens and cookies.
   */
  this.logout = function() {
    this._clearState();
    expiredTime = Date.now() * 1000;
    clearCookies();
  };

    /**
     * Refreshes the authentication token if it has expired. The authentication scheme should support refresh.
     * @param successCallback {Authorization~authenticateSuccessCallback} Optional callback invoked on success (deprecated use promises instead).
     * @param errorCallback {Authorization~errorCallback} Optional callback invoked on failure (deprecated use promises instead).
     * @return {Promise.<NetworkResponse|NetworkResponse>}
     */
    this.refreshToken = function(successCallback,errorCallback) {

    var boolean = _this.isTokenValid();

    if (boolean !== false) {
      if (this.getAccessToken() == null && this._getIsAnonymous()) {
        if (successCallback) {
          logger.error( 'Anonymous token is valid, you do not need to refresh.');
          successCallback(200, this._getAnonymousAccessToken());
        }
      }
      if (!this._getAnonymousAccessToken() && !this._getIsAnonymous()) {
        if (successCallback) {
          logger.error( 'Authenticated token is valid, you do not need to refresh.');
          successCallback(200, this.getAccessToken());
        }
      }
    }
    else{
      logger.error( 'Token is not valid and has expired, refreshing token from service.');
      this.authenticate(successCallback, errorCallback);
    }
  };


  this._getHttpHeaders = function (headers) {
    if (this.getAccessToken() !== null && typeof this.getAccessToken() == 'string') {
      headers[HEADERS.AUTHORIZATION] = 'Bearer ' + this.getAccessToken();
    }
    headers[HEADERS.ORACLE_MOBILE_APPLICATION_KEY] = this._getApplicationKey();
  };


  this._getAnonymousHttpHeaders = function (headers) {
    if (this._getAnonymousAccessToken() && typeof this._getAnonymousAccessToken() == 'string') {
      headers[HEADERS.AUTHORIZATION] = 'Bearer ' + this._getAnonymousAccessToken();
    }
    headers[HEADERS.ORACLE_MOBILE_APPLICATION_KEY] = this._getApplicationKey();
  };

}


SSOAuthorization.prototype = Object.create(Authorization.prototype);
SSOAuthorization.prototype.constructor = SSOAuthorization;