/** * 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;