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