diff --git a/README.md b/README.md index 0505bf7f..2cb218cf 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ If you find a security issue with our libraries or services please report it to ## The Library -This is a GA released version. The current version is **1.0.10**. +This is a GA released version. The current version is **1.0.11**. You have multiple ways of getting ADAL JS: @@ -32,10 +32,10 @@ Via NPM: Via CDN: - - + + -CDN will be updated to latest version 1.0.10. +CDN will be updated to latest version 1.0.11. Via Bower: diff --git a/bower.json b/bower.json index 79e7339e..f65046cd 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "adal-angular", - "version": "1.0.10", + "version": "1.0.11", "homepage": "https://github.com/AzureAD/azure-activedirectory-library-for-js", "authors": [ "MSOpentech" @@ -37,7 +37,7 @@ }, "devDependencies": { "angular-resource": "~1.2.26", - "angular-mocks": "~1.2.16", + "angular-mocks": "~1.2.26", "jasmine": "2.0.0", "angular-route": "~1.2.26" } diff --git a/changelog.txt b/changelog.txt index 5d9fdade..24b4724a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,11 @@ +Version 1.0.11 +========================== +* Adding support for using a special html for iFrames. This prevents app reloading in the iframe. Please see this: https://github.com/AzureAD/azure-activedirectory-library-for-js/wiki/FAQs#q1-my-app-is-re-loading-every-time-adal-renews-a-token +* Fixing multiple root causes for infinte loops at the time of login or token renewal. +* Fixing url paramters getting dropped after login. +* Adding timeout to token renewal requests. Thanks @dmxfee for the Pull Request. +* Use module pattern in adal.js + Version 1.0.10 ========================== Fixing infinite loop when refreshing tokens, adding extensibility for specifying anonymous endpoints diff --git a/dist/adal-angular.min.js b/dist/adal-angular.min.js index c7bbc283..f0de0170 100644 --- a/dist/adal-angular.min.js +++ b/dist/adal-angular.min.js @@ -1,2 +1,2 @@ -/*! adal-angular v1.0.10 2016-05-09 */ -"use strict";"undefined"!=typeof module&&module.exports&&(module.exports.inject=function(a){return new AuthenticationContext(a)}),function(){if(angular){var a=angular.module("AdalAngular",[]);a.provider("adalAuthenticationService",function(){var a=null,b={isAuthenticated:!1,userName:"",loginError:"",profile:""},c=function(c){var d=a.getCachedToken(c);b.isAuthenticated=null!==d&&d.length>0;var e=a.getCachedUser()||{userName:""};b.userName=e.userName,b.profile=e.profile,b.loginError=a.getLoginError()};this.init=function(b,d){if(!b)throw new Error("You must set configOptions, when calling init");var e=window.location.hash,f=window.location.href;e&&(f=f.replace(e,"")),b.redirectUri=b.redirectUri||f,b.postLogoutRedirectUri=b.postLogoutRedirectUri||f,d&&d.interceptors&&d.interceptors.push("ProtectedResourceInterceptor"),a=new AuthenticationContext(b),c(a.config.loginResource)},this.$get=["$rootScope","$window","$q","$location","$timeout",function(d,e,f,g,h){function i(a,b){return b.requireADLogin?a.requireADLogin!==!1:!!a.requireADLogin}function j(b){if(a.config&&a.config.anonymousEndpoints)for(var c=0;c-1)return!0;return!1}var k=function(){var f=e.location.hash;if(a.isCallback(f)){var i=a.getRequestInfo(f);if(a.saveTokenFromHash(i),g.$$html5?e.location=e.location.origin+e.location.pathname:e.location.hash="",i.requestType!==a.REQUEST_TYPE.LOGIN&&(a.callback=e.parent.AuthenticationContext().callback,i.requestType===a.REQUEST_TYPE.RENEW_TOKEN&&(a.callback=e.parent.callBackMappedToRenewStates[i.stateResponse])),i.stateMatch)if("function"==typeof a.callback){if(i.requestType===a.REQUEST_TYPE.RENEW_TOKEN){if(i.parameters.access_token)return void a.callback(a._getItem(a.CONSTANTS.STORAGE.ERROR_DESCRIPTION),i.parameters.access_token);if(i.parameters.id_token)return void a.callback(a._getItem(a.CONSTANTS.STORAGE.ERROR_DESCRIPTION),i.parameters.id_token);if(i.parameters.error)return a.callback(a._getItem(a.CONSTANTS.STORAGE.ERROR_DESCRIPTION),null),void(a._renewFailed=!0)}}else c(a.config.loginResource),b.userName?(h(function(){c(a.config.loginResource),d.userInfo=b;var e=a._getItem(a.CONSTANTS.STORAGE.START_PAGE);if(e){var f=a._getItem(a.CONSTANTS.STORAGE.START_PAGE_PARAMS);if(f){var h=JSON.parse(f);g.url(e).search(h)}else g.url(e)}},1),d.$broadcast("adal:loginSuccess")):d.$broadcast("adal:loginFailure",a._getItem(a.CONSTANTS.STORAGE.ERROR_DESCRIPTION));else d.$broadcast("adal:stateMismatch",a._getItem(a.CONSTANTS.STORAGE.ERROR_DESCRIPTION))}else c(a.config.loginResource),b.isAuthenticated||!b.userName||a._renewActive||a._renewFailed||(a._renewActive=!0,a.acquireToken(a.config.loginResource,function(c,e){a._renewActive=!1,c?d.$broadcast("adal:loginFailure","auto renew failure"):e&&(b.isAuthenticated=!0)}));h(function(){c(a.config.loginResource),d.userInfo=b},1)},l=function(){a.info("Login event for:"+g.$$url),a.config&&a.config.localLoginUrl?g.path(a.config.localLoginUrl):(a._saveItem(a.CONSTANTS.STORAGE.START_PAGE,g.$$url),a.info("Start login at:"+window.location.href),d.$broadcast("adal:loginRedirect"),a.login())},m=function(c,d){d&&d.$$route&&(i(d.$$route,a.config)?b.isAuthenticated||a._renewActive||(a.info("Route change event for:"+g.$$url),l()):d.$$route.templateUrl&&!j(d.$$route.templateUrl)&&a.config.anonymousEndpoints.push(d.$$route.templateUrl))},n=function(c,d,e,f,h){d&&(i(d,a.config)?b.isAuthenticated||a._renewActive||(g.$$url=d.url,a._saveItem(a.CONSTANTS.STORAGE.START_PAGE_PARAMS,JSON.stringify(e)),a.info("State change event for:"+g.$$url),l()):d.templateUrl&&!j(d.templateUrl)&&a.config.anonymousEndpoints.push(d.templateUrl))};return d.$on("$routeChangeStart",m),d.$on("$stateChangeStart",n),d.$on("$locationChangeStart",k),c(a.config.loginResource),d.userInfo=b,{config:a.config,login:function(){a.login()},loginInProgress:function(){return a.loginInProgress()},logOut:function(){a.logOut()},getCachedToken:function(b){return a.getCachedToken(b)},userInfo:b,acquireToken:function(b){var c=f.defer();return a._renewActive=!0,a.acquireToken(b,function(d,e){a._renewActive=!1,d?(a.error("Error when acquiring token for resource: "+b,d),c.reject(d)):c.resolve(e)}),c.promise},getUser:function(){var b=f.defer();return a.getUser(function(c,d){c?(a.error("Error when getting user",c),b.reject(c)):b.resolve(d)}),b.promise},getResourceForEndpoint:function(b){return a.getResourceForEndpoint(b)},clearCache:function(){a.clearCache()},clearCacheForResource:function(b){a.clearCacheForResource(b)},info:function(b){a.info(b)},verbose:function(b){a.verbose(b)}}}]}),a.factory("ProtectedResourceInterceptor",["adalAuthenticationService","$q","$rootScope",function(a,b,c){return{request:function(c){if(c){c.headers=c.headers||{};var d=a.getResourceForEndpoint(c.url);if(a.verbose("Url: "+c.url+" maps to resource: "+d),null===d)return c;var e=a.getCachedToken(d);if(e)return a.info("Token is avaliable for this url "+c.url),c.headers.Authorization="Bearer "+e,c;if(a.loginInProgress())return a.info("login already start."),b.reject("login in progress, cancelling the request");var f=b.defer();return a.acquireToken(d).then(function(b){a.verbose("Token is avaliable"),c.headers.Authorization="Bearer "+b,f.resolve(c)},function(a){f.reject(a)}),f.promise}},responseError:function(d){if(a.info("Getting error in the response"),d){if(401===d.status){var e=a.getResourceForEndpoint(d.config.url);a.clearCacheForResource(e),c.$broadcast("adal:notAuthorized",d,e)}else c.$broadcast("adal:errorResponse",d);return b.reject(d)}}}}])}else console.error("Angular.JS is not included")}(); \ No newline at end of file +/*! adal-angular v1.0.11 2016-07-19 */ +!function(){"use strict";if("undefined"!=typeof module&&module.exports&&(module.exports.inject=function(a){return new AuthenticationContext(a)}),angular){var a=angular.module("AdalAngular",[]);a.provider("adalAuthenticationService",function(){var a=null,b={isAuthenticated:!1,userName:"",loginError:"",profile:""},c=function(c){var d=a.getCachedToken(c);b.isAuthenticated=null!==d&&d.length>0;var e=a.getCachedUser()||{userName:""};b.userName=e.userName,b.profile=e.profile,b.loginError=a.getLoginError()};this.init=function(b,d){if(!b)throw new Error("You must set configOptions, when calling init");var e=window.location.hash,f=window.location.href;e&&(f=f.replace(e,"")),b.redirectUri=b.redirectUri||f,b.postLogoutRedirectUri=b.postLogoutRedirectUri||f,d&&d.interceptors&&d.interceptors.push("ProtectedResourceInterceptor"),a=new AuthenticationContext(b),c(a.config.loginResource)},this.$get=["$rootScope","$window","$q","$location","$timeout",function(d,e,f,g,h){function i(a,b){return b.requireADLogin?a.requireADLogin!==!1:!!a.requireADLogin}function j(b){if(a.config&&a.config.anonymousEndpoints)for(var c=0;c-1)return!0;return!1}var k=function(f,i,j){a.verbose("Location change event from "+j+" to "+i);var k=e.location.hash;if(a.isCallback(k)){a.verbose("Processing the hash: "+k);var l=a.getRequestInfo(k);if(a.saveTokenFromHash(l),l.requestType!==a.REQUEST_TYPE.LOGIN&&(a.callback=e.parent.AuthenticationContext().callback,l.requestType===a.REQUEST_TYPE.RENEW_TOKEN&&(a.callback=e.parent.callBackMappedToRenewStates[l.stateResponse])),l.stateMatch)if("function"==typeof a.callback){if(f.preventDefault(),l.requestType===a.REQUEST_TYPE.RENEW_TOKEN){if(l.parameters.access_token)return void a.callback(a._getItem(a.CONSTANTS.STORAGE.ERROR_DESCRIPTION),l.parameters.access_token);if(l.parameters.id_token)return void a.callback(a._getItem(a.CONSTANTS.STORAGE.ERROR_DESCRIPTION),l.parameters.id_token);if(l.parameters.error)return void a.callback(a._getItem(a.CONSTANTS.STORAGE.ERROR_DESCRIPTION),null)}}else{c(a.config.loginResource),b.userName?(h(function(){c(a.config.loginResource),d.userInfo=b},1),d.$broadcast("adal:loginSuccess")):d.$broadcast("adal:loginFailure",a._getItem(a.CONSTANTS.STORAGE.ERROR_DESCRIPTION));var m=a._getItem(a.CONSTANTS.STORAGE.LOGIN_REQUEST);m&&(a.verbose("Redirecting to start page: "+m),f.preventDefault(),!g.$$html5&&m.indexOf("#")>-1&&g.url(m.substring(m.indexOf("#")+1)),e.location=m)}else d.$broadcast("adal:stateMismatch",a._getItem(a.CONSTANTS.STORAGE.ERROR_DESCRIPTION))}else c(a.config.loginResource),b.isAuthenticated||!b.userName||a._renewActive||(a._renewActive=!0,a.acquireToken(a.config.loginResource,function(c,e){a._renewActive=!1,c?d.$broadcast("adal:loginFailure","auto renew failure"):e&&(b.isAuthenticated=!0)}));h(function(){c(a.config.loginResource),d.userInfo=b},1)},l=function(){a.info("Login event for:"+g.$$url),a.config&&a.config.localLoginUrl?g.path(a.config.localLoginUrl):(a.info("Start login at:"+window.location.href),d.$broadcast("adal:loginRedirect"),a.login())},m=function(c,d){d&&d.$$route&&(i(d.$$route,a.config)?b.isAuthenticated||a._renewActive||a.loginInProgress()||(a.info("Route change event for:"+g.$$url),l()):d.$$route.templateUrl&&!j(d.$$route.templateUrl)&&a.config.anonymousEndpoints.push(d.$$route.templateUrl))},n=function(c,d,e,f,h){d&&(i(d,a.config)?b.isAuthenticated||a._renewActive||a.loginInProgress()||(a.info("State change event for:"+g.$$url),l()):d.templateUrl&&!j(d.templateUrl)&&a.config.anonymousEndpoints.push(d.templateUrl))},o=function(b,c,d,e,f,g){a.verbose("State change error occured. Error: "+g),g&&g.data&&(a.info("Setting defaultPrevented to true if state change error occured because adal rejected a request. Error: "+g.data),b.preventDefault())};return d.$on("$routeChangeStart",m),d.$on("$stateChangeStart",n),d.$on("$locationChangeStart",k),d.$on("$stateChangeError",o),c(a.config.loginResource),d.userInfo=b,{config:a.config,login:function(){l()},loginInProgress:function(){return a.loginInProgress()},logOut:function(){a.logOut()},getCachedToken:function(b){return a.getCachedToken(b)},userInfo:b,acquireToken:function(b){var c=f.defer();return a._renewActive=!0,a.acquireToken(b,function(d,e){a._renewActive=!1,d?(a.error("Error when acquiring token for resource: "+b,d),c.reject(d)):c.resolve(e)}),c.promise},getUser:function(){var b=f.defer();return a.getUser(function(c,d){c?(a.error("Error when getting user",c),b.reject(c)):b.resolve(d)}),b.promise},getResourceForEndpoint:function(b){return a.getResourceForEndpoint(b)},clearCache:function(){a.clearCache()},clearCacheForResource:function(b){a.clearCacheForResource(b)},info:function(b){a.info(b)},verbose:function(b){a.verbose(b)}}}]}),a.factory("ProtectedResourceInterceptor",["adalAuthenticationService","$q","$rootScope",function(a,b,c){return{request:function(c){if(c){c.headers=c.headers||{};var d=a.getResourceForEndpoint(c.url);if(a.verbose("Url: "+c.url+" maps to resource: "+d),null===d)return c;var e=a.getCachedToken(d);if(e)return a.info("Token is available for this url "+c.url),c.headers.Authorization="Bearer "+e,c;if(a.loginInProgress())return a.info("login is in progress."),c.data="login in progress, cancelling the request for "+c.url,b.reject(c);var f=b.defer();return a.acquireToken(d).then(function(b){a.verbose("Token is available"),c.headers.Authorization="Bearer "+b,f.resolve(c)},function(a){c.data=a,f.reject(c)}),f.promise}},responseError:function(d){if(a.info("Getting error in the response."),d){if(401===d.status){var e=a.getResourceForEndpoint(d.config.url);a.clearCacheForResource(e),c.$broadcast("adal:notAuthorized",d,e)}else c.$broadcast("adal:errorResponse",d);return b.reject(d)}}}}])}else console.error("Angular.JS is not included")}(); \ No newline at end of file diff --git a/dist/adal.min.js b/dist/adal.min.js index 9f99a40a..192de041 100644 --- a/dist/adal.min.js +++ b/dist/adal.min.js @@ -1,2 +1,2 @@ -/*! adal-angular v1.0.10 2016-05-09 */ -"use strict";var Logging={level:0,log:function(){}},AuthenticationContext;"undefined"!=typeof module&&module.exports&&(module.exports.inject=function(a){return new AuthenticationContext(a)}),AuthenticationContext=function(a){if(this.REQUEST_TYPE={LOGIN:"LOGIN",RENEW_TOKEN:"RENEW_TOKEN",UNKNOWN:"UNKNOWN"},this.CONSTANTS={ACCESS_TOKEN:"access_token",EXPIRES_IN:"expires_in",ID_TOKEN:"id_token",ERROR_DESCRIPTION:"error_description",SESSION_STATE:"session_state",STORAGE:{TOKEN_KEYS:"adal.token.keys",ACCESS_TOKEN_KEY:"adal.access.token.key",EXPIRATION_KEY:"adal.expiration.key",START_PAGE:"adal.start.page",START_PAGE_PARAMS:"adal.start.page.params",STATE_LOGIN:"adal.state.login",STATE_RENEW:"adal.state.renew",STATE_RENEW_RESOURCE:"adal.state.renew.resource",NONCE_IDTOKEN:"adal.nonce.idtoken",SESSION_STATE:"adal.session.state",USERNAME:"adal.username",IDTOKEN:"adal.idtoken",ERROR:"adal.error",ERROR_DESCRIPTION:"adal.error.description",LOGIN_REQUEST:"adal.login.request",LOGIN_ERROR:"adal.login.error"},RESOURCE_DELIMETER:"|",ERR_MESSAGES:{NO_TOKEN:"User is not authorized"},LOGGING_LEVEL:{ERROR:0,WARN:1,INFO:2,VERBOSE:3},LEVEL_STRING_MAP:{0:"ERROR:",1:"WARNING:",2:"INFO:",3:"VERBOSE:"}},AuthenticationContext.prototype._singletonInstance)return AuthenticationContext.prototype._singletonInstance;if(AuthenticationContext.prototype._singletonInstance=this,this.instance="https://login.microsoftonline.com/",this.config={},this.callback=null,this.popUp=!1,this._user=null,this._activeRenewals={},this._loginInProgress=!1,this._renewStates=[],window.callBackMappedToRenewStates={},window.callBacksMappedToRenewStates={},a.displayCall&&"function"!=typeof a.displayCall)throw new Error("displayCall is not a function");if(!a.clientId)throw new Error("clientId is required");a.correlationId||(a.correlationId=this._guid()),this.config=this._cloneConfig(a),this.config.loginResource||(this.config.loginResource=this.config.clientId),this.config.redirectUri||(this.config.redirectUri=window.location.href)},AuthenticationContext.prototype.login=function(){var a=this._guid();this.config.state=a,this._idTokenNonce=this._guid(),this.verbose("Expected state: "+a+" startPage:"+window.location),this._saveItem(this.CONSTANTS.STORAGE.LOGIN_REQUEST,window.location),this._saveItem(this.CONSTANTS.STORAGE.LOGIN_ERROR,""),this._saveItem(this.CONSTANTS.STORAGE.STATE_LOGIN,a),this._saveItem(this.CONSTANTS.STORAGE.NONCE_IDTOKEN,this._idTokenNonce),this._saveItem(this.CONSTANTS.STORAGE.ERROR,""),this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION,"");var b=this._getNavigateUrl("id_token",null)+"&nonce="+encodeURIComponent(this._idTokenNonce);this.frameCallInProgress=!1,this._loginInProgress=!0,this.config.displayCall?this.config.displayCall(b):this.promptUser(b)},AuthenticationContext.prototype.loginInProgress=function(){return this._loginInProgress},AuthenticationContext.prototype._hasResource=function(a){var b=this._getItem(this.CONSTANTS.STORAGE.TOKEN_KEYS);return b&&!this._isEmpty(b)&&b.indexOf(a+this.CONSTANTS.RESOURCE_DELIMETER)>-1},AuthenticationContext.prototype.getCachedToken=function(a){if(!this._hasResource(a))return null;var b=this._getItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY+a),c=this._getItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY+a),d=this.config.expireOffsetSeconds||120;return c&&c>this._now()+d?b:(this._saveItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY+a,""),this._saveItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY+a,0),null)},AuthenticationContext.prototype.getCachedUser=function(){if(this._user)return this._user;var a=this._getItem(this.CONSTANTS.STORAGE.IDTOKEN);return this._user=this._createUser(a),this._user},AuthenticationContext.prototype.registerCallback=function(a,b,c){this._activeRenewals[b]=a,window.callBacksMappedToRenewStates[a]||(window.callBacksMappedToRenewStates[a]=[]);var d=this;window.callBacksMappedToRenewStates[a].push(c),window.callBackMappedToRenewStates[a]||(window.callBackMappedToRenewStates[a]=function(c,e){for(var f=0;f-1){var a=this._user.userName.split("@");return a[a.length-1]}return""},AuthenticationContext.prototype._createUser=function(a){var b=null,c=this._extractIdToken(a);return c&&c.hasOwnProperty("aud")&&(c.aud.toLowerCase()===this.config.clientId.toLowerCase()?(b={userName:"",profile:c},c.hasOwnProperty("upn")?b.userName=c.upn:c.hasOwnProperty("email")&&(b.userName=c.email)):this.warn("IdToken has invalid aud field")),b},AuthenticationContext.prototype._getHash=function(a){return a.indexOf("#/")>-1?a=a.substring(a.indexOf("#/")+2):a.indexOf("#")>-1&&(a=a.substring(1)),a},AuthenticationContext.prototype.isCallback=function(a){a=this._getHash(a);var b=this._deserialize(a);return b.hasOwnProperty(this.CONSTANTS.ERROR_DESCRIPTION)||b.hasOwnProperty(this.CONSTANTS.ACCESS_TOKEN)||b.hasOwnProperty(this.CONSTANTS.ID_TOKEN)},AuthenticationContext.prototype.getLoginError=function(){return this._getItem(this.CONSTANTS.STORAGE.LOGIN_ERROR)},AuthenticationContext.prototype.getRequestInfo=function(a){a=this._getHash(a);var b=this._deserialize(a),c={valid:!1,parameters:{},stateMatch:!1,stateResponse:"",requestType:this.REQUEST_TYPE.UNKNOWN};if(b&&(c.parameters=b,b.hasOwnProperty(this.CONSTANTS.ERROR_DESCRIPTION)||b.hasOwnProperty(this.CONSTANTS.ACCESS_TOKEN)||b.hasOwnProperty(this.CONSTANTS.ID_TOKEN))){c.valid=!0;var d="";if(!b.hasOwnProperty("state"))return this.warn("No state returned"),c;if(this.verbose("State: "+b.state),d=b.state,c.stateResponse=d,d===this._getItem(this.CONSTANTS.STORAGE.STATE_LOGIN))return c.requestType=this.REQUEST_TYPE.LOGIN,c.stateMatch=!0,c;if(!c.stateMatch&&window.parent&&window.parent.AuthenticationContext())for(var e=window.parent.AuthenticationContext()._renewStates,f=0;f-1&&b+1-1)return this.config.endpoints[b];if(!(a.indexOf("http://")>-1||a.indexOf("https://")>-1)){if(this.config&&this.config.anonymousEndpoints)for(var c=0;c-1)return null;return this.config.loginResource}return this._getHostFromUri(a)===this._getHostFromUri(this.config.redirectUri)?this.config.loginResource:null},AuthenticationContext.prototype._getHostFromUri=function(a){var b=String(a).replace(/^(https?:)\/\//,"");return b=b.split("/")[0]},AuthenticationContext.prototype.handleWindowCallback=function(){var a=window.location.hash;if(this.isCallback(a)){var b=this.getRequestInfo(a);this.info("Returned from redirect url"),this.saveTokenFromHash(b);var c=null;if(b.requestType===this.REQUEST_TYPE.RENEW_TOKEN&&window.parent?(this.verbose("Window is in iframe"),c=window.parent.callBackMappedToRenewStates[b.stateResponse],window.src=""):window&&window.oauth2Callback&&(this.verbose("Window is redirecting"),c=this.callback),window.location.hash="",window.location=this._getItem(this.CONSTANTS.STORAGE.LOGIN_REQUEST),b.requestType===this.REQUEST_TYPE.RENEW_TOKEN)return void c(this._getItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION),b.parameters[this.CONSTANTS.ACCESS_TOKEN]||b.parameters[this.CONSTANTS.ID_TOKEN])}},AuthenticationContext.prototype._getNavigateUrl=function(a,b){var c="common";this.config.tenant&&(c=this.config.tenant),this.config.instance&&(this.instance=this.config.instance);var d=this.instance+c+"/oauth2/authorize"+this._serialize(a,this.config,b)+this._addLibMetadata();return this.info("Navigate url:"+d),d},AuthenticationContext.prototype._extractIdToken=function(a){var b=this._decodeJwt(a);if(!b)return null;try{var c=b.JWSPayload,d=this._base64DecodeStringUrlSafe(c);return d?JSON.parse(d):(this.info("The returned id_token could not be base64 url safe decoded."),null)}catch(e){this.error("The returned id_token could not be decoded",e)}return null},AuthenticationContext.prototype._extractUserName=function(a){try{var b=this._extractIdToken(a);if(b){if(b.hasOwnProperty("upn"))return b.upn;if(b.hasOwnProperty("email"))return b.email}}catch(c){this.error("The returned id_token could not be decoded",c)}return null},AuthenticationContext.prototype._base64DecodeStringUrlSafe=function(a){return a=a.replace(/-/g,"+").replace(/_/g,"/"),window.atob?decodeURIComponent(escape(window.atob(a))):decodeURIComponent(escape(this._decode(a)))},AuthenticationContext.prototype._decode=function(a){var b="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";a=String(a).replace(/=+$/,"");var c=a.length;if(c%4===1)throw new Error("The token to be decoded is not correctly encoded.");for(var d,e,f,g,h,i,j,k,l="",m=0;c>m;m+=4){if(d=b.indexOf(a.charAt(m)),e=b.indexOf(a.charAt(m+1)),f=b.indexOf(a.charAt(m+2)),g=b.indexOf(a.charAt(m+3)),m+2===c-1){h=d<<18|e<<12|f<<6,i=h>>16&255,j=h>>8&255,l+=String.fromCharCode(i,j);break}if(m+1===c-1){h=d<<18|e<<12,i=h>>16&255,l+=String.fromCharCode(i);break}h=d<<18|e<<12|f<<6|g,i=h>>16&255,j=h>>8&255,k=255&h,l+=String.fromCharCode(i,j,k)}return l},AuthenticationContext.prototype._decodeJwt=function(a){if(this._isEmpty(a))return null;var b=/^([^\.\s]*)\.([^\.\s]+)\.([^\.\s]*)$/,c=b.exec(a);if(!c||c.length<4)return this.warn("The returned id_token is not parseable."),null;var d={header:c[1],JWSPayload:c[2],JWSSig:c[3]};return d},AuthenticationContext.prototype._convertUrlSafeToRegularBase64EncodedString=function(a){return a.replace("-","+").replace("_","/")},AuthenticationContext.prototype._serialize=function(a,b,c){var d=[];return null!==b&&(d.push("?response_type="+a),d.push("client_id="+encodeURIComponent(b.clientId)),c&&d.push("resource="+encodeURIComponent(c)),d.push("redirect_uri="+encodeURIComponent(b.redirectUri)),d.push("state="+encodeURIComponent(b.state)),b.hasOwnProperty("slice")&&d.push("slice="+encodeURIComponent(b.slice)),b.hasOwnProperty("extraQueryParameter")&&d.push(b.extraQueryParameter),b.correlationId&&d.push("client-request-id="+encodeURIComponent(b.correlationId))),d.join("&")},AuthenticationContext.prototype._deserialize=function(a){var b,c=/\+/g,d=/([^&=]+)=?([^&]*)/g,e=function(a){return decodeURIComponent(a.replace(c," "))},f={};for(b=d.exec(a);b;)f[e(b[1])]=e(b[2]),b=d.exec(a);return f},AuthenticationContext.prototype._guid=function(){for(var a="xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx",b="0123456789abcdef",c=0,d="",e=0;36>e;e++)"-"!==a[e]&&"4"!==a[e]&&(c=16*Math.random()|0),"x"===a[e]?d+=b[c]:"y"===a[e]?(c&=3,c|=8,d+=b[c]):d+=a[e];return d},AuthenticationContext.prototype._expiresIn=function(a){return this._now()+parseInt(a,10)},AuthenticationContext.prototype._now=function(){return Math.round((new Date).getTime()/1e3)},AuthenticationContext.prototype._addAdalFrame=function(a){if("undefined"!=typeof a){this.info("Add adal frame to document:"+a);var b=document.getElementById(a);if(!b){if(document.createElement&&document.documentElement&&(window.opera||-1===window.navigator.userAgent.indexOf("MSIE 5.0"))){var c=document.createElement("iframe");c.setAttribute("id",a),c.style.visibility="hidden",c.style.position="absolute",c.style.width=c.style.height=c.borderWidth="0px",b=document.getElementsByTagName("body")[0].appendChild(c)}else document.body&&document.body.insertAdjacentHTML&&document.body.insertAdjacentHTML("beforeEnd",'');window.frames&&window.frames[a]&&(b=window.frames[a])}return b}},AuthenticationContext.prototype._saveItem=function(a,b){return this.config&&this.config.cacheLocation&&"localStorage"===this.config.cacheLocation?this._supportsLocalStorage()?(localStorage.setItem(a,b),!0):(this.info("Local storage is not supported"),!1):this._supportsSessionStorage()?(sessionStorage.setItem(a,b),!0):(this.info("Session storage is not supported"),!1)},AuthenticationContext.prototype._getItem=function(a){return this.config&&this.config.cacheLocation&&"localStorage"===this.config.cacheLocation?this._supportsLocalStorage()?localStorage.getItem(a):(this.info("Local storage is not supported"),null):this._supportsSessionStorage()?sessionStorage.getItem(a):(this.info("Session storage is not supported"),null)},AuthenticationContext.prototype._supportsLocalStorage=function(){try{return"localStorage"in window&&window.localStorage}catch(a){return!1}},AuthenticationContext.prototype._supportsSessionStorage=function(){try{return"sessionStorage"in window&&window.sessionStorage}catch(a){return!1}},AuthenticationContext.prototype._cloneConfig=function(a){if(null===a||"object"!=typeof a)return a;var b={};for(var c in a)a.hasOwnProperty(c)&&(b[c]=a[c]);return b},AuthenticationContext.prototype._addLibMetadata=function(){return"&x-client-SKU=Js&x-client-Ver="+this._libVersion()},AuthenticationContext.prototype.log=function(a,b,c){if(a<=Logging.level){var d=this.config.correlationId,e=(new Date).toUTCString(),f=e+":"+d+"-"+this._libVersion()+"-"+this.CONSTANTS.LEVEL_STRING_MAP[a]+" "+b;c&&(f+="\nstack:\n"+c.stack),Logging.log(f)}},AuthenticationContext.prototype.error=function(a,b){this.log(this.CONSTANTS.LOGGING_LEVEL.ERROR,a,b)},AuthenticationContext.prototype.warn=function(a){this.log(this.CONSTANTS.LOGGING_LEVEL.WARN,a,null)},AuthenticationContext.prototype.info=function(a){this.log(this.CONSTANTS.LOGGING_LEVEL.INFO,a,null)},AuthenticationContext.prototype.verbose=function(a){this.log(this.CONSTANTS.LOGGING_LEVEL.VERBOSE,a,null)},AuthenticationContext.prototype._libVersion=function(){return"1.0.10"}; \ No newline at end of file +/*! adal-angular v1.0.11 2016-07-19 */ +var AuthenticationContext=function(){"use strict";return AuthenticationContext=function(a){if(this.REQUEST_TYPE={LOGIN:"LOGIN",RENEW_TOKEN:"RENEW_TOKEN",UNKNOWN:"UNKNOWN"},this.CONSTANTS={ACCESS_TOKEN:"access_token",EXPIRES_IN:"expires_in",ID_TOKEN:"id_token",ERROR_DESCRIPTION:"error_description",SESSION_STATE:"session_state",STORAGE:{TOKEN_KEYS:"adal.token.keys",ACCESS_TOKEN_KEY:"adal.access.token.key",EXPIRATION_KEY:"adal.expiration.key",STATE_LOGIN:"adal.state.login",STATE_RENEW:"adal.state.renew",STATE_RENEW_RESOURCE:"adal.state.renew.resource",NONCE_IDTOKEN:"adal.nonce.idtoken",SESSION_STATE:"adal.session.state",USERNAME:"adal.username",IDTOKEN:"adal.idtoken",ERROR:"adal.error",ERROR_DESCRIPTION:"adal.error.description",LOGIN_REQUEST:"adal.login.request",LOGIN_ERROR:"adal.login.error",RENEW_STATUS:"adal.token.renew.status"},RESOURCE_DELIMETER:"|",ERR_MESSAGES:{NO_TOKEN:"User is not authorized"},LOADFRAME_TIMEOUT:"6000",TOKEN_RENEW_STATUS_CANCELED:"Canceled",TOKEN_RENEW_STATUS_COMPLETED:"Completed",TOKEN_RENEW_STATUS_IN_PROGRESS:"In Progress",LOGGING_LEVEL:{ERROR:0,WARN:1,INFO:2,VERBOSE:3},LEVEL_STRING_MAP:{0:"ERROR:",1:"WARNING:",2:"INFO:",3:"VERBOSE:"}},AuthenticationContext.prototype._singletonInstance)return AuthenticationContext.prototype._singletonInstance;if(AuthenticationContext.prototype._singletonInstance=this,this.instance="https://login.microsoftonline.com/",this.config={},this.callback=null,this.popUp=!1,this._user=null,this._activeRenewals={},this._loginInProgress=!1,this._renewStates=[],window.callBackMappedToRenewStates={},window.callBacksMappedToRenewStates={},a.displayCall&&"function"!=typeof a.displayCall)throw new Error("displayCall is not a function");if(!a.clientId)throw new Error("clientId is required");this.config=this._cloneConfig(a),this.config.instance&&(this.instance=this.config.instance),this.config.loginResource||(this.config.loginResource=this.config.clientId),this.config.redirectUri||(this.config.redirectUri=window.location.href),this.config.anonymousEndpoints||(this.config.anonymousEndpoints=[])},window.Logging={level:0,log:function(a){}},AuthenticationContext.prototype.login=function(){var a=this._guid();this.config.state=a,this._idTokenNonce=this._guid(),this.verbose("Expected state: "+a+" startPage:"+window.location),this._saveItem(this.CONSTANTS.STORAGE.LOGIN_REQUEST,window.location),this._saveItem(this.CONSTANTS.STORAGE.LOGIN_ERROR,""),this._saveItem(this.CONSTANTS.STORAGE.STATE_LOGIN,a),this._saveItem(this.CONSTANTS.STORAGE.NONCE_IDTOKEN,this._idTokenNonce),this._saveItem(this.CONSTANTS.STORAGE.ERROR,""),this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION,"");var b=this._getNavigateUrl("id_token",null)+"&nonce="+encodeURIComponent(this._idTokenNonce);this.frameCallInProgress=!1,this._loginInProgress=!0,this.config.displayCall?this.config.displayCall(b):this.promptUser(b)},AuthenticationContext.prototype.loginInProgress=function(){return this._loginInProgress},AuthenticationContext.prototype._hasResource=function(a){var b=this._getItem(this.CONSTANTS.STORAGE.TOKEN_KEYS);return b&&!this._isEmpty(b)&&b.indexOf(a+this.CONSTANTS.RESOURCE_DELIMETER)>-1},AuthenticationContext.prototype.getCachedToken=function(a){if(!this._hasResource(a))return null;var b=this._getItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY+a),c=this._getItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY+a),d=this.config.expireOffsetSeconds||120;return c&&c>this._now()+d?b:(this._saveItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY+a,""),this._saveItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY+a,0),null)},AuthenticationContext.prototype.getCachedUser=function(){if(this._user)return this._user;var a=this._getItem(this.CONSTANTS.STORAGE.IDTOKEN);return this._user=this._createUser(a),this._user},AuthenticationContext.prototype.registerCallback=function(a,b,c){this._activeRenewals[b]=a,window.callBacksMappedToRenewStates[a]||(window.callBacksMappedToRenewStates[a]=[]);var d=this;window.callBacksMappedToRenewStates[a].push(c),window.callBackMappedToRenewStates[a]||(window.callBackMappedToRenewStates[a]=function(c,e){for(var f=0;f-1)){var b=this._user.profile.upn.split("@");a+="&domain_hint="+encodeURIComponent(b[b.length-1])}return a},AuthenticationContext.prototype._createUser=function(a){var b=null,c=this._extractIdToken(a);return c&&c.hasOwnProperty("aud")&&(c.aud.toLowerCase()===this.config.clientId.toLowerCase()?(b={userName:"",profile:c},c.hasOwnProperty("upn")?b.userName=c.upn:c.hasOwnProperty("email")&&(b.userName=c.email)):this.warn("IdToken has invalid aud field")),b},AuthenticationContext.prototype._getHash=function(a){return a.indexOf("#/")>-1?a=a.substring(a.indexOf("#/")+2):a.indexOf("#")>-1&&(a=a.substring(1)),a},AuthenticationContext.prototype.isCallback=function(a){a=this._getHash(a);var b=this._deserialize(a);return b.hasOwnProperty(this.CONSTANTS.ERROR_DESCRIPTION)||b.hasOwnProperty(this.CONSTANTS.ACCESS_TOKEN)||b.hasOwnProperty(this.CONSTANTS.ID_TOKEN)},AuthenticationContext.prototype.getLoginError=function(){return this._getItem(this.CONSTANTS.STORAGE.LOGIN_ERROR)},AuthenticationContext.prototype.getRequestInfo=function(a){a=this._getHash(a);var b=this._deserialize(a),c={valid:!1,parameters:{},stateMatch:!1,stateResponse:"",requestType:this.REQUEST_TYPE.UNKNOWN};if(b&&(c.parameters=b,b.hasOwnProperty(this.CONSTANTS.ERROR_DESCRIPTION)||b.hasOwnProperty(this.CONSTANTS.ACCESS_TOKEN)||b.hasOwnProperty(this.CONSTANTS.ID_TOKEN))){c.valid=!0;var d="";if(!b.hasOwnProperty("state"))return this.warn("No state returned"),c;if(this.verbose("State: "+b.state),d=b.state,c.stateResponse=d,d===this._getItem(this.CONSTANTS.STORAGE.STATE_LOGIN))return c.requestType=this.REQUEST_TYPE.LOGIN,c.stateMatch=!0,c;if(!c.stateMatch&&window.parent&&window.parent.AuthenticationContext())for(var e=window.parent.AuthenticationContext()._renewStates,f=0;f-1&&b+1-1)return this.config.endpoints[b];if(!(a.indexOf("http://")>-1||a.indexOf("https://")>-1)){if(this.config&&this.config.anonymousEndpoints)for(var c=0;c-1)return null;return this.config.loginResource}return this._getHostFromUri(a)===this._getHostFromUri(this.config.redirectUri)?this.config.loginResource:null},AuthenticationContext.prototype._getHostFromUri=function(a){var b=String(a).replace(/^(https?:)\/\//,"");return b=b.split("/")[0]},AuthenticationContext.prototype.handleWindowCallback=function(){var a=window.location.hash;if(this.isCallback(a)){var b=this.getRequestInfo(a);this.info("Returned from redirect url"),this.saveTokenFromHash(b);var c=null;if(b.requestType===this.REQUEST_TYPE.RENEW_TOKEN&&window.parent&&window.parent!==window)return this.verbose("Window is in iframe"),c=window.parent.callBackMappedToRenewStates[b.stateResponse],void(c&&c(this._getItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION),b.parameters[this.CONSTANTS.ACCESS_TOKEN]||b.parameters[this.CONSTANTS.ID_TOKEN]));window&&window.oauth2Callback&&(this.verbose("Window is redirecting"),c=this.callback),window.location=this._getItem(this.CONSTANTS.STORAGE.LOGIN_REQUEST)}},AuthenticationContext.prototype._getNavigateUrl=function(a,b){var c="common";this.config.tenant&&(c=this.config.tenant);var d=this.instance+c+"/oauth2/authorize"+this._serialize(a,this.config,b)+this._addLibMetadata();return this.info("Navigate url:"+d),d},AuthenticationContext.prototype._extractIdToken=function(a){var b=this._decodeJwt(a);if(!b)return null;try{var c=b.JWSPayload,d=this._base64DecodeStringUrlSafe(c);return d?JSON.parse(d):(this.info("The returned id_token could not be base64 url safe decoded."),null)}catch(e){this.error("The returned id_token could not be decoded",e)}return null},AuthenticationContext.prototype._base64DecodeStringUrlSafe=function(a){return a=a.replace(/-/g,"+").replace(/_/g,"/"),window.atob?decodeURIComponent(escape(window.atob(a))):decodeURIComponent(escape(this._decode(a)))},AuthenticationContext.prototype._decode=function(a){var b="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";a=String(a).replace(/=+$/,"");var c=a.length;if(c%4===1)throw new Error("The token to be decoded is not correctly encoded.");for(var d,e,f,g,h,i,j,k,l="",m=0;c>m;m+=4){if(d=b.indexOf(a.charAt(m)),e=b.indexOf(a.charAt(m+1)),f=b.indexOf(a.charAt(m+2)),g=b.indexOf(a.charAt(m+3)),m+2===c-1){h=d<<18|e<<12|f<<6,i=h>>16&255,j=h>>8&255,l+=String.fromCharCode(i,j);break}if(m+1===c-1){h=d<<18|e<<12,i=h>>16&255,l+=String.fromCharCode(i);break}h=d<<18|e<<12|f<<6|g,i=h>>16&255,j=h>>8&255,k=255&h,l+=String.fromCharCode(i,j,k)}return l},AuthenticationContext.prototype._decodeJwt=function(a){if(this._isEmpty(a))return null;var b=/^([^\.\s]*)\.([^\.\s]+)\.([^\.\s]*)$/,c=b.exec(a);if(!c||c.length<4)return this.warn("The returned id_token is not parseable."),null;var d={header:c[1],JWSPayload:c[2],JWSSig:c[3]};return d},AuthenticationContext.prototype._convertUrlSafeToRegularBase64EncodedString=function(a){return a.replace("-","+").replace("_","/")},AuthenticationContext.prototype._serialize=function(a,b,c){var d=[];if(null!==b){d.push("?response_type="+a),d.push("client_id="+encodeURIComponent(b.clientId)),c&&d.push("resource="+encodeURIComponent(c)),d.push("redirect_uri="+encodeURIComponent(b.redirectUri)),d.push("state="+encodeURIComponent(b.state)),b.hasOwnProperty("slice")&&d.push("slice="+encodeURIComponent(b.slice)),b.hasOwnProperty("extraQueryParameter")&&d.push(b.extraQueryParameter);var e=b.correlationId?b.correlationId:this._guid();d.push("client-request-id="+encodeURIComponent(e))}return d.join("&")},AuthenticationContext.prototype._deserialize=function(a){var b,c=/\+/g,d=/([^&=]+)=([^&]*)/g,e=function(a){return decodeURIComponent(a.replace(c," "))},f={};for(b=d.exec(a);b;)f[e(b[1])]=e(b[2]),b=d.exec(a);return f},AuthenticationContext.prototype._guid=function(){for(var a="xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx",b="0123456789abcdef",c=0,d="",e=0;36>e;e++)"-"!==a[e]&&"4"!==a[e]&&(c=16*Math.random()|0),"x"===a[e]?d+=b[c]:"y"===a[e]?(c&=3,c|=8,d+=b[c]):d+=a[e];return d},AuthenticationContext.prototype._expiresIn=function(a){return this._now()+parseInt(a,10)},AuthenticationContext.prototype._now=function(){return Math.round((new Date).getTime()/1e3)},AuthenticationContext.prototype._addAdalFrame=function(a){if("undefined"!=typeof a){this.info("Add adal frame to document:"+a);var b=document.getElementById(a);if(!b){if(document.createElement&&document.documentElement&&(window.opera||-1===window.navigator.userAgent.indexOf("MSIE 5.0"))){var c=document.createElement("iframe");c.setAttribute("id",a),c.style.visibility="hidden",c.style.position="absolute",c.style.width=c.style.height=c.borderWidth="0px",b=document.getElementsByTagName("body")[0].appendChild(c)}else document.body&&document.body.insertAdjacentHTML&&document.body.insertAdjacentHTML("beforeEnd",'');window.frames&&window.frames[a]&&(b=window.frames[a])}return b}},AuthenticationContext.prototype._saveItem=function(a,b){return this.config&&this.config.cacheLocation&&"localStorage"===this.config.cacheLocation?this._supportsLocalStorage()?(localStorage.setItem(a,b),!0):(this.info("Local storage is not supported"),!1):this._supportsSessionStorage()?(sessionStorage.setItem(a,b),!0):(this.info("Session storage is not supported"),!1)},AuthenticationContext.prototype._getItem=function(a){return this.config&&this.config.cacheLocation&&"localStorage"===this.config.cacheLocation?this._supportsLocalStorage()?localStorage.getItem(a):(this.info("Local storage is not supported"),null):this._supportsSessionStorage()?sessionStorage.getItem(a):(this.info("Session storage is not supported"),null)},AuthenticationContext.prototype._supportsLocalStorage=function(){try{return"localStorage"in window&&window.localStorage}catch(a){return!1}},AuthenticationContext.prototype._supportsSessionStorage=function(){try{return"sessionStorage"in window&&window.sessionStorage}catch(a){return!1}},AuthenticationContext.prototype._cloneConfig=function(a){if(null===a||"object"!=typeof a)return a;var b={};for(var c in a)a.hasOwnProperty(c)&&(b[c]=a[c]);return b},AuthenticationContext.prototype._addLibMetadata=function(){return"&x-client-SKU=Js&x-client-Ver="+this._libVersion()},AuthenticationContext.prototype.log=function(a,b,c){if(a<=Logging.level){var d=(new Date).toUTCString(),e="";e=this.config.correlationId?d+":"+this.config.correlationId+"-"+this._libVersion()+"-"+this.CONSTANTS.LEVEL_STRING_MAP[a]+" "+b:d+":"+this._libVersion()+"-"+this.CONSTANTS.LEVEL_STRING_MAP[a]+" "+b,c&&(e+="\nstack:\n"+c.stack),Logging.log(e)}},AuthenticationContext.prototype.error=function(a,b){this.log(this.CONSTANTS.LOGGING_LEVEL.ERROR,a,b)},AuthenticationContext.prototype.warn=function(a){this.log(this.CONSTANTS.LOGGING_LEVEL.WARN,a,null)},AuthenticationContext.prototype.info=function(a){this.log(this.CONSTANTS.LOGGING_LEVEL.INFO,a,null)},AuthenticationContext.prototype.verbose=function(a){this.log(this.CONSTANTS.LOGGING_LEVEL.VERBOSE,a,null)},AuthenticationContext.prototype._libVersion=function(){return"1.0.11"},"undefined"!=typeof module&&module.exports&&(module.exports=AuthenticationContext,module.exports.inject=function(a){return new AuthenticationContext(a)}),AuthenticationContext}(); \ No newline at end of file diff --git a/lib/adal-angular.js b/lib/adal-angular.js index 1b72a46f..20f6c0c3 100644 --- a/lib/adal-angular.js +++ b/lib/adal-angular.js @@ -1,5 +1,5 @@ //---------------------------------------------------------------------- -// AdalJS v1.0.10 +// AdalJS v1.0.11 // @preserve Copyright (c) Microsoft Open Technologies, Inc. // All Rights Reserved // Apache License 2.0 @@ -16,16 +16,17 @@ // See the License for the specific language governing permissions and // limitations under the License. //---------------------------------------------------------------------- -'use strict'; - -if (typeof module !== 'undefined' && module.exports) { - module.exports.inject = function (conf) { - return new AuthenticationContext(conf); - }; -} (function () { // ============= Angular modules- Start ============= + 'use strict'; + + if (typeof module !== 'undefined' && module.exports) { + module.exports.inject = function (conf) { + return new AuthenticationContext(conf); + }; + } + if (angular) { var AdalModule = angular.module('AdalAngular', []); @@ -65,7 +66,7 @@ if (typeof module !== 'undefined' && module.exports) { throw new Error('You must set configOptions, when calling init'); } - // loginresource is used to set authenticated status + // loginResource is used to set authenticated status updateDataFromCache(_adal.config.loginResource); }; @@ -73,21 +74,16 @@ if (typeof module !== 'undefined' && module.exports) { // $rootScope, $window, $q, $location, $timeout are injected by Angular this.$get = ['$rootScope', '$window', '$q', '$location', '$timeout', function ($rootScope, $window, $q, $location, $timeout) { - var locationChangeHandler = function () { + var locationChangeHandler = function (event, newUrl, oldUrl) { + _adal.verbose('Location change event from ' + oldUrl + ' to ' + newUrl); var hash = $window.location.hash; if (_adal.isCallback(hash)) { // callback can come from login or iframe request - + _adal.verbose('Processing the hash: ' + hash); var requestInfo = _adal.getRequestInfo(hash); _adal.saveTokenFromHash(requestInfo); - if ($location.$$html5) { - $window.location = $window.location.origin + $window.location.pathname; - } else { - $window.location.hash = ''; - } - if (requestInfo.requestType !== _adal.REQUEST_TYPE.LOGIN) { _adal.callback = $window.parent.AuthenticationContext().callback; if (requestInfo.requestType === _adal.REQUEST_TYPE.RENEW_TOKEN) { @@ -95,12 +91,15 @@ if (typeof module !== 'undefined' && module.exports) { } } - // Return to callback if it is send from iframe + // Return to callback if it is sent from iframe if (requestInfo.stateMatch) { if (typeof _adal.callback === 'function') { + // since this is a token renewal request in iFrame, we don't need to proceed with the location change. + event.preventDefault(); + // Call within the same context without full page redirect keeps the callback if (requestInfo.requestType === _adal.REQUEST_TYPE.RENEW_TOKEN) { - // Idtoken or Accestoken can be renewed + // id_token or access_token can be renewed if (requestInfo.parameters['access_token']) { _adal.callback(_adal._getItem(_adal.CONSTANTS.STORAGE.ERROR_DESCRIPTION), requestInfo.parameters['access_token']); return; @@ -109,7 +108,6 @@ if (typeof module !== 'undefined' && module.exports) { return; } else if (requestInfo.parameters['error']) { _adal.callback(_adal._getItem(_adal.CONSTANTS.STORAGE.ERROR_DESCRIPTION), null); - _adal._renewFailed = true; return; } } @@ -117,29 +115,28 @@ if (typeof module !== 'undefined' && module.exports) { // normal full login redirect happened on the page updateDataFromCache(_adal.config.loginResource); if (_oauthData.userName) { - //IDtoken is added as token for the app $timeout(function () { + // id_token is added as token for the app updateDataFromCache(_adal.config.loginResource); $rootScope.userInfo = _oauthData; - // redirect to login requested page - var loginStartPage = _adal._getItem(_adal.CONSTANTS.STORAGE.START_PAGE); - if (loginStartPage) { - // Check to see if any params were stored - var paramsJSON = _adal._getItem(_adal.CONSTANTS.STORAGE.START_PAGE_PARAMS); - if (paramsJSON) { - // If params were stored redirect to the page and then - // initialize the params - var loginStartPageParams = JSON.parse(paramsJSON); - $location.url(loginStartPage).search(loginStartPageParams); - } else { - $location.url(loginStartPage); - } - } }, 1); + $rootScope.$broadcast('adal:loginSuccess'); } else { $rootScope.$broadcast('adal:loginFailure', _adal._getItem(_adal.CONSTANTS.STORAGE.ERROR_DESCRIPTION)); } + + // redirect to login start page + var loginStartPage = _adal._getItem(_adal.CONSTANTS.STORAGE.LOGIN_REQUEST); + if (loginStartPage) { + // prevent the current location change and redirect the user back to the login start page + _adal.verbose('Redirecting to start page: ' + loginStartPage); + event.preventDefault(); + if (!$location.$$html5 && loginStartPage.indexOf('#') > -1) { + $location.url(loginStartPage.substring(loginStartPage.indexOf('#') + 1)); + } + $window.location = loginStartPage; + } } } else { @@ -150,8 +147,8 @@ if (typeof module !== 'undefined' && module.exports) { // No callback. App resumes after closing or moving to new page. // Check token and username updateDataFromCache(_adal.config.loginResource); - if (!_oauthData.isAuthenticated && _oauthData.userName && !_adal._renewActive && !_adal._renewFailed) { - // Idtoken is expired or not present + if (!_oauthData.isAuthenticated && _oauthData.userName && !_adal._renewActive) { + // id_token is expired or not present _adal._renewActive = true; _adal.acquireToken(_adal.config.loginResource, function (error, tokenOut) { _adal._renewActive = false; @@ -176,9 +173,9 @@ if (typeof module !== 'undefined' && module.exports) { _adal.info('Login event for:' + $location.$$url); if (_adal.config && _adal.config.localLoginUrl) { $location.path(_adal.config.localLoginUrl); - } else { + } + else { // directly start login flow - _adal._saveItem(_adal.CONSTANTS.STORAGE.START_PAGE, $location.$$url); _adal.info('Start login at:' + window.location.href); $rootScope.$broadcast('adal:loginRedirect'); _adal.login(); @@ -203,9 +200,11 @@ if (typeof module !== 'undefined' && module.exports) { var routeChangeHandler = function (e, nextRoute) { if (nextRoute && nextRoute.$$route) { if (isADLoginRequired(nextRoute.$$route, _adal.config)) { - if (!_oauthData.isAuthenticated && !_adal._renewActive) { - _adal.info('Route change event for:' + $location.$$url); - loginHandler(); + if (!_oauthData.isAuthenticated) { + if (!_adal._renewActive && !_adal.loginInProgress()) { + _adal.info('Route change event for:' + $location.$$url); + loginHandler(); + } } } else { @@ -219,18 +218,11 @@ if (typeof module !== 'undefined' && module.exports) { var stateChangeHandler = function (e, toState, toParams, fromState, fromParams) { if (toState) { if (isADLoginRequired(toState, _adal.config)) { - if (!_oauthData.isAuthenticated && !_adal._renewActive) { - // $location.$$url is set as the page we are coming from - // Update it so we can store the actual location we want to - // redirect to upon returning - $location.$$url = toState.url; - - // Parameters are not stored in the url on stateChange so - // we store them - _adal._saveItem(_adal.CONSTANTS.STORAGE.START_PAGE_PARAMS, JSON.stringify(toParams)); - - _adal.info('State change event for:' + $location.$$url); - loginHandler(); + if (!_oauthData.isAuthenticated) { + if (!_adal._renewActive && !_adal.loginInProgress()) { + _adal.info('State change event for:' + $location.$$url); + loginHandler(); + } } } else { @@ -241,6 +233,17 @@ if (typeof module !== 'undefined' && module.exports) { } }; + var stateChangeErrorHandler = function (event, toState, toParams, fromState, fromParams, error) { + _adal.verbose("State change error occured. Error: " + error); + + // adal interceptor sets the error on config.data property. If it is set, it means state change is rejected by adal, + // in which case set the defaultPrevented to true to avoid url update as that sometimesleads to infinte loop. + if (error && error.data) { + _adal.info("Setting defaultPrevented to true if state change error occured because adal rejected a request. Error: " + error.data); + event.preventDefault(); + } + }; + // Route change event tracking to receive fragment and also auto renew tokens $rootScope.$on('$routeChangeStart', routeChangeHandler); @@ -248,6 +251,8 @@ if (typeof module !== 'undefined' && module.exports) { $rootScope.$on('$locationChangeStart', locationChangeHandler); + $rootScope.$on('$stateChangeError', stateChangeErrorHandler); + updateDataFromCache(_adal.config.loginResource); $rootScope.userInfo = _oauthData; @@ -255,7 +260,7 @@ if (typeof module !== 'undefined' && module.exports) { // public methods will be here that are accessible from Controller config: _adal.config, login: function () { - _adal.login(); + loginHandler(); }, loginInProgress: function () { return _adal.loginInProgress(); @@ -323,37 +328,36 @@ if (typeof module !== 'undefined' && module.exports) { request: function (config) { if (config) { - // This interceptor needs to load service, but dependeny definition causes circular reference error. - // Loading with injector is suggested at github. https://github.com/angular/angular.js/issues/2367 - config.headers = config.headers || {}; - var resource = authService.getResourceForEndpoint(config.url); authService.verbose('Url: ' + config.url + ' maps to resource: ' + resource); if (resource === null) { return config; } - var tokenStored = authService.getCachedToken(resource); if (tokenStored) { - authService.info('Token is avaliable for this url ' + config.url); + authService.info('Token is available for this url ' + config.url); // check endpoint mapping if provided config.headers.Authorization = 'Bearer ' + tokenStored; return config; - } else { + } + else { // Cancel request if login is starting if (authService.loginInProgress()) { - authService.info('login already start.'); - return $q.reject('login in progress, cancelling the request'); - } else { + authService.info('login is in progress.'); + config.data = 'login in progress, cancelling the request for ' + config.url; + return $q.reject(config); + } + else { // delayed request to return after iframe completes var delayedRequest = $q.defer(); authService.acquireToken(resource).then(function (token) { - authService.verbose('Token is avaliable'); + authService.verbose('Token is available'); config.headers.Authorization = 'Bearer ' + token; delayedRequest.resolve(config); }, function (err) { - delayedRequest.reject(err); + config.data = err; + delayedRequest.reject(config); }); return delayedRequest.promise; @@ -364,7 +368,7 @@ if (typeof module !== 'undefined' && module.exports) { } }, responseError: function (rejection) { - authService.info('Getting error in the response'); + authService.info('Getting error in the response.'); if (rejection) { if (rejection.status === 401) { var resource = authService.getResourceForEndpoint(rejection.config.url); diff --git a/lib/adal.js b/lib/adal.js index 1051ce1b..f9db922a 100644 --- a/lib/adal.js +++ b/lib/adal.js @@ -1,5 +1,5 @@ -//---------------------------------------------------------------------- -// AdalJS v1.0.10 +//---------------------------------------------------------------------- +// AdalJS v1.0.11 // @preserve Copyright (c) Microsoft Open Technologies, Inc. // All Rights Reserved // Apache License 2.0 @@ -16,1173 +16,1182 @@ // See the License for the specific language governing permissions and // limitations under the License. //---------------------------------------------------------------------- -'use strict'; - -// node.js usage for tests -var Logging = { - level: 0, - log: function () { } -}; - -var AuthenticationContext; -if (typeof module !== 'undefined' && module.exports) { - module.exports.inject = function (conf) { - return new AuthenticationContext(conf); - }; -} - -/** - * Config information - * @public - * @class Config - * @property {tenant} Your target tenant - * @property {clientId} Identifier assigned to your app by Azure Active Directory - * @property {redirectUri} Endpoint at which you expect to receive tokens - * @property {instance} Azure Active Directory Instance(default:https://login.microsoftonline.com/) - * @property {endpoints} Collection of {Endpoint-ResourceId} used for autmatically attaching tokens in webApi calls - */ - -/** - * User information from idtoken. - * @class User - * @property {string} userName - username assigned from upn or email. - * @property {object} profile - properties parsed from idtoken. - */ - -/** - * Creates a new AuthenticationContext object. - * @constructor - * @param {object} config Configuration options for AuthenticationContext - * - **/ -AuthenticationContext = function (config) { + +var AuthenticationContext = (function () { + + 'use strict'; + /** - * Enum for request type - * @enum {string} + * Config information + * @public + * @class Config + * @property {tenant} Your target tenant + * @property {clientId} Identifier assigned to your app by Azure Active Directory + * @property {redirectUri} Endpoint at which you expect to receive tokens + * @property {instance} Azure Active Directory Instance(default:https://login.microsoftonline.com/) + * @property {endpoints} Collection of {Endpoint-ResourceId} used for autmatically attaching tokens in webApi calls */ - this.REQUEST_TYPE = { - LOGIN: 'LOGIN', - RENEW_TOKEN: 'RENEW_TOKEN', - UNKNOWN: 'UNKNOWN' - }; /** - * Enum for storage constants - * @enum {string} + * User information from idtoken. + * @class User + * @property {string} userName - username assigned from upn or email. + * @property {object} profile - properties parsed from idtoken. */ - this.CONSTANTS = { - ACCESS_TOKEN: 'access_token', - EXPIRES_IN: 'expires_in', - ID_TOKEN: 'id_token', - ERROR_DESCRIPTION: 'error_description', - SESSION_STATE: 'session_state', - STORAGE: { - TOKEN_KEYS: 'adal.token.keys', - ACCESS_TOKEN_KEY: 'adal.access.token.key', - EXPIRATION_KEY: 'adal.expiration.key', - START_PAGE: 'adal.start.page', - START_PAGE_PARAMS: 'adal.start.page.params', - STATE_LOGIN: 'adal.state.login', - STATE_RENEW: 'adal.state.renew', - STATE_RENEW_RESOURCE: 'adal.state.renew.resource', - NONCE_IDTOKEN: 'adal.nonce.idtoken', - SESSION_STATE: 'adal.session.state', - USERNAME: 'adal.username', - IDTOKEN: 'adal.idtoken', - ERROR: 'adal.error', - ERROR_DESCRIPTION: 'adal.error.description', - LOGIN_REQUEST: 'adal.login.request', - LOGIN_ERROR: 'adal.login.error' - }, - RESOURCE_DELIMETER: '|', - ERR_MESSAGES: { - NO_TOKEN: 'User is not authorized' - }, - LOGGING_LEVEL: { - ERROR: 0, - WARN: 1, - INFO: 2, - VERBOSE: 3 - }, - LEVEL_STRING_MAP: { - 0: 'ERROR:', - 1: 'WARNING:', - 2: 'INFO:', - 3: 'VERBOSE:' + + /** + * Creates a new AuthenticationContext object. + * @constructor + * @param {object} config Configuration options for AuthenticationContext + * + **/ + AuthenticationContext = function (config) { + /** + * Enum for request type + * @enum {string} + */ + this.REQUEST_TYPE = { + LOGIN: 'LOGIN', + RENEW_TOKEN: 'RENEW_TOKEN', + UNKNOWN: 'UNKNOWN' + }; + + /** + * Enum for storage constants + * @enum {string} + */ + this.CONSTANTS = { + ACCESS_TOKEN: 'access_token', + EXPIRES_IN: 'expires_in', + ID_TOKEN: 'id_token', + ERROR_DESCRIPTION: 'error_description', + SESSION_STATE: 'session_state', + STORAGE: { + TOKEN_KEYS: 'adal.token.keys', + ACCESS_TOKEN_KEY: 'adal.access.token.key', + EXPIRATION_KEY: 'adal.expiration.key', + STATE_LOGIN: 'adal.state.login', + STATE_RENEW: 'adal.state.renew', + STATE_RENEW_RESOURCE: 'adal.state.renew.resource', + NONCE_IDTOKEN: 'adal.nonce.idtoken', + SESSION_STATE: 'adal.session.state', + USERNAME: 'adal.username', + IDTOKEN: 'adal.idtoken', + ERROR: 'adal.error', + ERROR_DESCRIPTION: 'adal.error.description', + LOGIN_REQUEST: 'adal.login.request', + LOGIN_ERROR: 'adal.login.error', + RENEW_STATUS: 'adal.token.renew.status' + }, + RESOURCE_DELIMETER: '|', + ERR_MESSAGES: { + NO_TOKEN: 'User is not authorized' + }, + LOADFRAME_TIMEOUT: '6000', + TOKEN_RENEW_STATUS_CANCELED: 'Canceled', + TOKEN_RENEW_STATUS_COMPLETED: 'Completed', + TOKEN_RENEW_STATUS_IN_PROGRESS: 'In Progress', + LOGGING_LEVEL: { + ERROR: 0, + WARN: 1, + INFO: 2, + VERBOSE: 3 + }, + LEVEL_STRING_MAP: { + 0: 'ERROR:', + 1: 'WARNING:', + 2: 'INFO:', + 3: 'VERBOSE:' + } + }; + + if (AuthenticationContext.prototype._singletonInstance) { + return AuthenticationContext.prototype._singletonInstance; + } + AuthenticationContext.prototype._singletonInstance = this; + + // public + this.instance = 'https://login.microsoftonline.com/'; + this.config = {}; + this.callback = null; + this.popUp = false; + + // private + this._user = null; + this._activeRenewals = {}; + this._loginInProgress = false; + this._renewStates = []; + + window.callBackMappedToRenewStates = {}; + window.callBacksMappedToRenewStates = {}; + + // validate before constructor assignments + if (config.displayCall && typeof config.displayCall !== 'function') { + throw new Error('displayCall is not a function'); + } + + if (!config.clientId) { + throw new Error('clientId is required'); + } + + this.config = this._cloneConfig(config); + + if (this.config.instance) { + this.instance = this.config.instance; + } + + // App can request idtoken for itself using clientid as resource + if (!this.config.loginResource) { + this.config.loginResource = this.config.clientId; + } + + if (!this.config.redirectUri) { + this.config.redirectUri = window.location.href; + } + + if (!this.config.anonymousEndpoints) { + this.config.anonymousEndpoints = []; } }; - if (AuthenticationContext.prototype._singletonInstance) { - return AuthenticationContext.prototype._singletonInstance; - } - AuthenticationContext.prototype._singletonInstance = this; - - // public - this.instance = 'https://login.microsoftonline.com/'; - this.config = {}; - this.callback = null; - this.popUp = false; - - // private - this._user = null; - this._activeRenewals = {}; - this._loginInProgress = false; - this._renewStates = []; - - window.callBackMappedToRenewStates = {}; - window.callBacksMappedToRenewStates = {}; - - // validate before constructor assignments - if (config.displayCall && typeof config.displayCall !== 'function') { - throw new Error('displayCall is not a function'); - } + window.Logging = { + level: 0, + log: function (message) { } + }; - if (!config.clientId) { - throw new Error('clientId is required'); - } + /** + * Gets initial Idtoken for the app backend + * Saves the resulting Idtoken in localStorage. + */ + AuthenticationContext.prototype.login = function () { + // Token is not present and user needs to login + var expectedState = this._guid(); + this.config.state = expectedState; + this._idTokenNonce = this._guid(); + this.verbose('Expected state: ' + expectedState + ' startPage:' + window.location); + this._saveItem(this.CONSTANTS.STORAGE.LOGIN_REQUEST, window.location); + this._saveItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, ''); + this._saveItem(this.CONSTANTS.STORAGE.STATE_LOGIN, expectedState); + this._saveItem(this.CONSTANTS.STORAGE.NONCE_IDTOKEN, this._idTokenNonce); + this._saveItem(this.CONSTANTS.STORAGE.ERROR, ''); + this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, ''); + + + var urlNavigate = this._getNavigateUrl('id_token', null) + '&nonce=' + encodeURIComponent(this._idTokenNonce); + this.frameCallInProgress = false; + this._loginInProgress = true; + if (this.config.displayCall) { + // User defined way of handling the navigation + this.config.displayCall(urlNavigate); + } else { + this.promptUser(urlNavigate); + } + // callback from redirected page will receive fragment. It needs to call oauth2Callback + }; - if (!config.correlationId) { - config.correlationId = this._guid(); - } + AuthenticationContext.prototype.loginInProgress = function () { + return this._loginInProgress; + }; - this.config = this._cloneConfig(config); + AuthenticationContext.prototype._hasResource = function (key) { + var keys = this._getItem(this.CONSTANTS.STORAGE.TOKEN_KEYS); + return keys && !this._isEmpty(keys) && (keys.indexOf(key + this.CONSTANTS.RESOURCE_DELIMETER) > -1); + }; - // App can request idtoken for itself using clientid as resource - if (!this.config.loginResource) { - this.config.loginResource = this.config.clientId; - } + /** + * Gets token for the specified resource from local storage cache + * @param {string} resource A URI that identifies the resource for which the token is valid. + * @returns {string} token if exists and not expired or null + */ + AuthenticationContext.prototype.getCachedToken = function (resource) { + if (!this._hasResource(resource)) { + return null; + } - if (!this.config.redirectUri) { - this.config.redirectUri = window.location.href; - } -}; - -/** - * Gets initial Idtoken for the app backend - * Saves the resulting Idtoken in localStorage. - */ -AuthenticationContext.prototype.login = function () { - // Token is not present and user needs to login - var expectedState = this._guid(); - this.config.state = expectedState; - this._idTokenNonce = this._guid(); - this.verbose('Expected state: ' + expectedState + ' startPage:' + window.location); - this._saveItem(this.CONSTANTS.STORAGE.LOGIN_REQUEST, window.location); - this._saveItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, ''); - this._saveItem(this.CONSTANTS.STORAGE.STATE_LOGIN, expectedState); - this._saveItem(this.CONSTANTS.STORAGE.NONCE_IDTOKEN, this._idTokenNonce); - this._saveItem(this.CONSTANTS.STORAGE.ERROR, ''); - this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, ''); - - - var urlNavigate = this._getNavigateUrl('id_token', null) + '&nonce=' + encodeURIComponent(this._idTokenNonce); - this.frameCallInProgress = false; - this._loginInProgress = true; - if (this.config.displayCall) { - // User defined way of handling the navigation - this.config.displayCall(urlNavigate); - } else { - this.promptUser(urlNavigate); - } - // callback from redirected page will receive fragment. It needs to call oauth2Callback -}; - -AuthenticationContext.prototype.loginInProgress = function () { - return this._loginInProgress; -}; - -AuthenticationContext.prototype._hasResource = function (key) { - var keys = this._getItem(this.CONSTANTS.STORAGE.TOKEN_KEYS); - return keys && !this._isEmpty(keys) && (keys.indexOf(key + this.CONSTANTS.RESOURCE_DELIMETER) > -1); -}; - -/** - * Gets token for the specified resource from local storage cache - * @param {string} resource A URI that identifies the resource for which the token is valid. - * @returns {string} token if exists and not expired or null - */ -AuthenticationContext.prototype.getCachedToken = function (resource) { - if (!this._hasResource(resource)) { - return null; - } + var token = this._getItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + resource); + var expired = this._getItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + resource); - var token = this._getItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + resource); - var expired = this._getItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + resource); + // If expiration is within offset, it will force renew + var offset = this.config.expireOffsetSeconds || 120; - // If expiration is within offset, it will force renew - var offset = this.config.expireOffsetSeconds || 120; + if (expired && (expired > this._now() + offset)) { + return token; + } else { + this._saveItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + resource, ''); + this._saveItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + resource, 0); + return null; + } + }; + /** + * Retrieves and parse idToken from localstorage + * @returns {User} user object + */ + AuthenticationContext.prototype.getCachedUser = function () { + if (this._user) { + return this._user; + } - if (expired && (expired > this._now() + offset)) { - return token; - } else { - this._saveItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + resource, ''); - this._saveItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + resource, 0); - return null; - } -}; - -/** - * Retrieves and parse idToken from localstorage - * @returns {User} user object - */ -AuthenticationContext.prototype.getCachedUser = function () { - if (this._user) { + var idtoken = this._getItem(this.CONSTANTS.STORAGE.IDTOKEN); + this._user = this._createUser(idtoken); return this._user; - } + }; - var idtoken = this._getItem(this.CONSTANTS.STORAGE.IDTOKEN); - this._user = this._createUser(idtoken); - return this._user; -}; + AuthenticationContext.prototype.registerCallback = function (expectedState, resource, callback) { + this._activeRenewals[resource] = expectedState; + if (!window.callBacksMappedToRenewStates[expectedState]) { + window.callBacksMappedToRenewStates[expectedState] = []; + } + var self = this; + window.callBacksMappedToRenewStates[expectedState].push(callback); + if (!window.callBackMappedToRenewStates[expectedState]) { + window.callBackMappedToRenewStates[expectedState] = function (message, token) { + for (var i = 0; i < window.callBacksMappedToRenewStates[expectedState].length; ++i) { + window.callBacksMappedToRenewStates[expectedState][i](message, token); + } + self._activeRenewals[resource] = null; + window.callBacksMappedToRenewStates[expectedState] = null; + window.callBackMappedToRenewStates[expectedState] = null; + }; + } + }; + + // var errorResponse = {error:'', error_description:''}; + // var token = 'string token'; + // callback(errorResponse, token) + // with callback + /** + * Acquires access token with hidden iframe + * @param {string} resource ResourceUri identifying the target resource + * @returns {string} access token if request is successful + */ + AuthenticationContext.prototype._renewToken = function (resource, callback) { + // use iframe to try refresh token + // use given resource to create new authz url + this.info('renewToken is called for resource:' + resource); + var frameHandle = this._addAdalFrame('adalRenewFrame' + resource); + var expectedState = this._guid() + '|' + resource; + this.config.state = expectedState; + // renew happens in iframe, so it keeps javascript context + this._renewStates.push(expectedState); + + this.verbose('Renew token Expected state: ' + expectedState); + var urlNavigate = this._getNavigateUrl('token', resource) + '&prompt=none'; + urlNavigate = this._addHintParameters(urlNavigate); + + this.callback = callback; + this.registerCallback(expectedState, resource, callback); + this.verbose('Navigate to:' + urlNavigate); + this._saveItem(this.CONSTANTS.STORAGE.LOGIN_REQUEST, ''); + frameHandle.src = 'about:blank'; + this._loadFrameTimeout(urlNavigate, 'adalRenewFrame' + resource, resource); -AuthenticationContext.prototype.registerCallback = function (expectedState, resource, callback) { - this._activeRenewals[resource] = expectedState; - if (!window.callBacksMappedToRenewStates[expectedState]) { - window.callBacksMappedToRenewStates[expectedState] = []; + }; + + AuthenticationContext.prototype._renewIdToken = function (callback) { + // use iframe to try refresh token + this.info('renewIdToken is called'); + var frameHandle = this._addAdalFrame('adalIdTokenFrame'); + var expectedState = this._guid() + '|' + this.config.clientId; + this._idTokenNonce = this._guid(); + this._saveItem(this.CONSTANTS.STORAGE.NONCE_IDTOKEN, this._idTokenNonce); + this.config.state = expectedState; + // renew happens in iframe, so it keeps javascript context + this._renewStates.push(expectedState); + + this.verbose('Renew Idtoken Expected state: ' + expectedState); + var urlNavigate = this._getNavigateUrl('id_token', null) + '&prompt=none'; + urlNavigate = this._addHintParameters(urlNavigate); + + urlNavigate += '&nonce=' + encodeURIComponent(this._idTokenNonce); + this.registerCallback(expectedState, this.config.clientId, callback); + this.idTokenNonce = null; + this.verbose('Navigate to:' + urlNavigate); + this._saveItem(this.CONSTANTS.STORAGE.LOGIN_REQUEST, ''); + frameHandle.src = 'about:blank'; + this._loadFrameTimeout(urlNavigate, 'adalIdTokenFrame', this.config.clientId); + }; + + AuthenticationContext.prototype._urlContainsQueryStringParameter = function (name, url) { + // regex to detect pattern of a ? or & followed by the name parameter and an equals character + var regex = new RegExp("[\\?&]" + name + "="); + return regex.test(url); } - var self = this; - window.callBacksMappedToRenewStates[expectedState].push(callback); - if (!window.callBackMappedToRenewStates[expectedState]) { - window.callBackMappedToRenewStates[expectedState] = function (message, token) { - for (var i = 0; i < window.callBacksMappedToRenewStates[expectedState].length; ++i) { - window.callBacksMappedToRenewStates[expectedState][i](message, token); + + // Calling _loadFrame but with a timeout to signal failure in loadframeStatus. Callbacks are left + // registered when network errors occur and subsequent token requests for same resource are registered to the pending request + AuthenticationContext.prototype._loadFrameTimeout = function (urlNavigation, frameName, resource) { + //set iframe session to pending + this.verbose('Set loading state to pending for: ' + resource); + this._saveItem(this.CONSTANTS.STORAGE.RENEW_STATUS + resource, this.CONSTANTS.TOKEN_RENEW_STATUS_IN_PROGRESS); + this._loadFrame(urlNavigation, frameName); + var self = this; + setTimeout(function () { + if (self._getItem(self.CONSTANTS.STORAGE.RENEW_STATUS + resource) === self.CONSTANTS.TOKEN_RENEW_STATUS_IN_PROGRESS) { + // fail the iframe session if it's in pending state + self.verbose('Loading frame has timed out after: ' + (self.CONSTANTS.LOADFRAME_TIMEOUT / 1000) + ' seconds for resource ' + resource); + var expectedState = self._activeRenewals[resource]; + if (expectedState && window.callBackMappedToRenewStates[expectedState]) { + window.callBackMappedToRenewStates[expectedState]('Token renewal operation failed due to timeout', null); + } + + self._saveItem(self.CONSTANTS.STORAGE.RENEW_STATUS + resource, self.CONSTANTS.TOKEN_RENEW_STATUS_CANCELED); } - self._activeRenewals[resource] = null; - window.callBacksMappedToRenewStates[expectedState] = null; - window.callBackMappedToRenewStates[expectedState] = null; - }; - } -}; - -// var errorResponse = {error:'', errorDescription:''}; -// var token = 'string token'; -// callback(errorResponse, token) -// with callback -/** - * Acquires access token with hidden iframe - * @param {string} resource ResourceUri identifying the target resource - * @returns {string} access token if request is successfull - */ -AuthenticationContext.prototype._renewToken = function (resource, callback) { - // use iframe to try refresh token - // use given resource to create new authz url - this.info('renewToken is called for resource:' + resource); - var frameHandle = this._addAdalFrame('adalRenewFrame' + resource); - var expectedState = this._guid() + '|' + resource; - this._idTokenNonce = this._guid(); - this.config.state = expectedState; - // renew happens in iframe, so it keeps javascript context - this._renewStates.push(expectedState); - - this.verbose('Renew token Expected state: ' + expectedState); - var urlNavigate = this._getNavigateUrl('token', resource) + '&prompt=none&login_hint=' + encodeURIComponent(this._user.userName); - - // don't add domain_hint twice if user provided it in the extraQueryParameter value - if (!this._urlContainsQueryStringParameter("domain_hint", urlNavigate)) { - urlNavigate += '&domain_hint=' + encodeURIComponent(this._getDomainHint()); + }, self.CONSTANTS.LOADFRAME_TIMEOUT); } - urlNavigate += '&nonce=' + encodeURIComponent(this._idTokenNonce); - this.callback = callback; - this.registerCallback(expectedState, resource, callback); - this.idTokenNonce = null; - this.verbose('Navigate to:' + urlNavigate); - this._saveItem(this.CONSTANTS.STORAGE.LOGIN_REQUEST, ''); - frameHandle.src = 'about:blank'; - this._loadFrame(urlNavigate, 'adalRenewFrame' + resource); -}; - -AuthenticationContext.prototype._renewIdToken = function (callback) { - // use iframe to try refresh token - this.info('renewIdToken is called'); - var frameHandle = this._addAdalFrame('adalIdTokenFrame'); - var expectedState = this._guid() + '|' + this.config.clientId; - this._idTokenNonce = this._guid(); - this._saveItem(this.CONSTANTS.STORAGE.NONCE_IDTOKEN, this._idTokenNonce); - this.config.state = expectedState; - // renew happens in iframe, so it keeps javascript context - this._renewStates.push(expectedState); - - this.verbose('Renew Idtoken Expected state: ' + expectedState); - var urlNavigate = this._getNavigateUrl('id_token', null) + '&prompt=none&login_hint=' + encodeURIComponent(this._user.userName); - - // don't add domain_hint twice if user provided it in the extraQueryParameter value - if (!this._urlContainsQueryStringParameter("domain_hint", urlNavigate)) { - urlNavigate += '&domain_hint=' + encodeURIComponent(this._getDomainHint()); - } + AuthenticationContext.prototype._loadFrame = function (urlNavigate, frameName) { + // This trick overcomes iframe navigation in IE + // IE does not load the page consistently in iframe + var self = this; + self.info('LoadFrame: ' + frameName); + var frameCheck = frameName; + setTimeout(function () { + var frameHandle = self._addAdalFrame(frameCheck); + if (frameHandle.src === '' || frameHandle.src === 'about:blank') { + frameHandle.src = urlNavigate; + self._loadFrame(urlNavigate, frameCheck); + } + }, 500); + }; + + /** + * Acquire token from cache if not expired and available. Acquires token from iframe if expired. + * @param {string} resource ResourceUri identifying the target resource + * @param {requestCallback} callback + */ + AuthenticationContext.prototype.acquireToken = function (resource, callback) { + if (this._isEmpty(resource)) { + this.warn('resource is required'); + callback('resource is required', null); + return; + } - urlNavigate += '&nonce=' + encodeURIComponent(this._idTokenNonce); - this.registerCallback(expectedState, this.config.clientId, callback); - this.idTokenNonce = null; - this.verbose('Navigate to:' + urlNavigate); - this._saveItem(this.CONSTANTS.STORAGE.LOGIN_REQUEST, ''); - frameHandle.src = 'about:blank'; - this._loadFrame(urlNavigate, 'adalIdTokenFrame'); -}; - -AuthenticationContext.prototype._urlContainsQueryStringParameter = function (name, url) { - // regex to detect pattern of a ? or & followed by the name parameter and an equals character - var regex = new RegExp("[\\?&]" + name + "="); - return regex.test(url); -} - -AuthenticationContext.prototype._loadFrame = function (urlNavigate, frameName) { - // This trick overcomes iframe navigation in IE - // IE does not load the page consistently in iframe - var self = this; - self.info('LoadFrame: ' + frameName); - var frameCheck = frameName; - setTimeout(function () { - var frameHandle = self._addAdalFrame(frameCheck); - if (frameHandle.src === '' || frameHandle.src === 'about:blank') { - frameHandle.src = urlNavigate; - self._loadFrame(urlNavigate, frameCheck); + var token = this.getCachedToken(resource); + if (token) { + this.info('Token is already in cache for resource:' + resource); + callback(null, token); + return; } - }, 500); -}; - -/** - * Acquire token from cache if not expired and available. Acquires token from iframe if expired. - * @param {string} resource ResourceUri identifying the target resource - * @param {requestCallback} callback - */ -AuthenticationContext.prototype.acquireToken = function (resource, callback) { - if (this._isEmpty(resource)) { - this.warn('resource is required'); - callback('resource is required', null); - return; - } - var token = this.getCachedToken(resource); - if (token) { - this.info('Token is already in cache for resource:' + resource); - callback(null, token); - return; - } + if (!this._user) { + this.warn('User login is required'); + callback('User login is required', null); + return; + } - if (!this._user) { - this.warn('User login is required'); - callback('User login is required', null); - return; - } + // refresh attept with iframe + //Already renewing for this resource, callback when we get the token. + if (this._activeRenewals[resource]) { + //Active renewals contains the state for each renewal. + this.registerCallback(this._activeRenewals[resource], resource, callback); + } + else { + if (resource === this.config.clientId) { + // App uses idtoken to send to api endpoints + // Default resource is tracked as clientid to store this token + this.verbose('renewing idtoken'); + this._renewIdToken(callback); + } else { + this._renewToken(resource, callback); + } + } + }; - // refresh attept with iframe - //Already renewing for this resource, callback when we get the token. - if (this._activeRenewals[resource]) { - //Active renewals contains the state for each renewal. - this.registerCallback(this._activeRenewals[resource], resource, callback); - } - else { - if (resource === this.config.clientId) { - // App uses idtoken to send to api endpoints - // Default resource is tracked as clientid to store this token - this.verbose('renewing idtoken'); - this._renewIdToken(callback); + /** + * Redirect the Browser to Azure AD Authorization endpoint + * @param {string} urlNavigate The authorization request url + */ + AuthenticationContext.prototype.promptUser = function (urlNavigate) { + if (urlNavigate) { + this.info('Navigate to:' + urlNavigate); + window.location.replace(urlNavigate); } else { - this._renewToken(resource, callback); + this.info('Navigate url is empty'); } - } -}; - -/** - * Redirect the Browser to Azure AD Authorization endpoint - * @param {string} urlNavigate The authorization request url - */ -AuthenticationContext.prototype.promptUser = function (urlNavigate) { - if (urlNavigate) { - this.info('Navigate to:' + urlNavigate); - window.location.replace(urlNavigate); - } else { - this.info('Navigate url is empty'); - } -}; - -/** - * Clear cache items. - */ -AuthenticationContext.prototype.clearCache = function () { - this._saveItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY, ''); - this._saveItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY, 0); - this._saveItem(this.CONSTANTS.STORAGE.SESSION_STATE, ''); - this._saveItem(this.CONSTANTS.STORAGE.STATE_LOGIN, ''); - this._renewStates = []; - this._saveItem(this.CONSTANTS.STORAGE.START_PAGE, ''); - this._saveItem(this.CONSTANTS.STORAGE.START_PAGE_PARAMS, ''); - this._saveItem(this.CONSTANTS.STORAGE.USERNAME, ''); - this._saveItem(this.CONSTANTS.STORAGE.IDTOKEN, ''); - this._saveItem(this.CONSTANTS.STORAGE.ERROR, ''); - this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, ''); - var keys = this._getItem(this.CONSTANTS.STORAGE.TOKEN_KEYS); - - if (!this._isEmpty(keys)) { - keys = keys.split(this.CONSTANTS.RESOURCE_DELIMETER); - for (var i = 0; i < keys.length; i++) { - this._saveItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + keys[i], ''); - this._saveItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + keys[i], 0); + }; + + /** + * Clear cache items. + */ + AuthenticationContext.prototype.clearCache = function () { + this._saveItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY, ''); + this._saveItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY, 0); + this._saveItem(this.CONSTANTS.STORAGE.SESSION_STATE, ''); + this._saveItem(this.CONSTANTS.STORAGE.STATE_LOGIN, ''); + this._renewStates = []; + this._saveItem(this.CONSTANTS.STORAGE.USERNAME, ''); + this._saveItem(this.CONSTANTS.STORAGE.IDTOKEN, ''); + this._saveItem(this.CONSTANTS.STORAGE.ERROR, ''); + this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, ''); + var keys = this._getItem(this.CONSTANTS.STORAGE.TOKEN_KEYS); + + if (!this._isEmpty(keys)) { + keys = keys.split(this.CONSTANTS.RESOURCE_DELIMETER); + for (var i = 0; i < keys.length; i++) { + this._saveItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + keys[i], ''); + this._saveItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + keys[i], 0); + } } - } - this._saveItem(this.CONSTANTS.STORAGE.TOKEN_KEYS, ''); -}; - -/** - * Clear cache items for a resource. - */ -AuthenticationContext.prototype.clearCacheForResource = function (resource) { - this._saveItem(this.CONSTANTS.STORAGE.STATE_RENEW, ''); - this._saveItem(this.CONSTANTS.STORAGE.ERROR, ''); - this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, ''); - if (this._hasResource(resource)) { - this._saveItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + resource, ''); - this._saveItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + resource, 0); - } -}; - -/** - * Logout user will redirect page to logout endpoint. - * After logout, it will redirect to post_logout page if provided. - */ -AuthenticationContext.prototype.logOut = function () { - this.clearCache(); - var tenant = 'common'; - var logout = ''; - this._user = null; - if (this.config.tenant) { - tenant = this.config.tenant; - } + this._saveItem(this.CONSTANTS.STORAGE.TOKEN_KEYS, ''); + }; - if (this.config.instance) { - this.instance = this.config.instance; - } + /** + * Clear cache items for a resource. + */ + AuthenticationContext.prototype.clearCacheForResource = function (resource) { + this._saveItem(this.CONSTANTS.STORAGE.STATE_RENEW, ''); + this._saveItem(this.CONSTANTS.STORAGE.ERROR, ''); + this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, ''); + if (this._hasResource(resource)) { + this._saveItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + resource, ''); + this._saveItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + resource, 0); + } + }; - if (this.config.postLogoutRedirectUri) { - logout = 'post_logout_redirect_uri=' + encodeURIComponent(this.config.postLogoutRedirectUri); - } + /** + * Logout user will redirect page to logout endpoint. + * After logout, it will redirect to post_logout page if provided. + */ + AuthenticationContext.prototype.logOut = function () { + this.clearCache(); + var tenant = 'common'; + var logout = ''; + this._user = null; + if (this.config.tenant) { + tenant = this.config.tenant; + } - var urlNavigate = this.instance + tenant + '/oauth2/logout?' + logout; - this.info('Logout navigate to: ' + urlNavigate); - this.promptUser(urlNavigate); -}; - -AuthenticationContext.prototype._isEmpty = function (str) { - return (typeof str === 'undefined' || !str || 0 === str.length); -}; - -/** - * This callback is displayed as part of the Requester class. - * @callback requestCallback - * @param {string} error - * @param {User} user - */ - -/** - * Gets a user profile - * @param {requestCallback} cb - The callback that handles the response. - */ -AuthenticationContext.prototype.getUser = function (callback) { - // IDToken is first call - if (typeof callback !== 'function') { - throw new Error('callback is not a function'); - } + if (this.config.postLogoutRedirectUri) { + logout = 'post_logout_redirect_uri=' + encodeURIComponent(this.config.postLogoutRedirectUri); + } - this.callback = callback; + var urlNavigate = this.instance + tenant + '/oauth2/logout?' + logout; + this.info('Logout navigate to: ' + urlNavigate); + this.promptUser(urlNavigate); + }; - // user in memory - if (this._user) { - this.callback(null, this._user); - return; - } + AuthenticationContext.prototype._isEmpty = function (str) { + return (typeof str === 'undefined' || !str || 0 === str.length); + }; - // frame is used to get idtoken - var idtoken = this._getItem(this.CONSTANTS.STORAGE.IDTOKEN); - if (!this._isEmpty(idtoken)) { - this.info('User exists in cache: '); - this._user = this._createUser(idtoken); - this.callback(null, this._user); - } else { - this.warn('User information is not available'); - this.callback('User information is not available'); - } -}; + /** + * This callback is displayed as part of the Requester class. + * @callback requestCallback + * @param {string} error + * @param {User} user + */ -AuthenticationContext.prototype._getDomainHint = function () { - if (this._user && this._user.userName && this._user.userName.indexOf('@') > -1) { - var parts = this._user.userName.split('@'); - // local part can include @ in quotes. Sending last part handles that. - return parts[parts.length - 1]; - } + /** + * Gets a user profile + * @param {requestCallback} cb - The callback that handles the response. + */ + AuthenticationContext.prototype.getUser = function (callback) { + // IDToken is first call + if (typeof callback !== 'function') { + throw new Error('callback is not a function'); + } - return ''; -}; + this.callback = callback; -AuthenticationContext.prototype._createUser = function (idToken) { - var user = null; - var parsedJson = this._extractIdToken(idToken); - if (parsedJson && parsedJson.hasOwnProperty('aud')) { + // user in memory + if (this._user) { + this.callback(null, this._user); + return; + } - if (parsedJson.aud.toLowerCase() === this.config.clientId.toLowerCase()) { + // frame is used to get idtoken + var idtoken = this._getItem(this.CONSTANTS.STORAGE.IDTOKEN); + if (!this._isEmpty(idtoken)) { + this.info('User exists in cache: '); + this._user = this._createUser(idtoken); + this.callback(null, this._user); + } else { + this.warn('User information is not available'); + this.callback('User information is not available'); + } + }; - user = { - userName: '', - profile: parsedJson - }; + AuthenticationContext.prototype._addHintParameters = function (urlNavigate) { + // include hint params only if upn is present + if (this._user && this._user.profile && this._user.profile.hasOwnProperty('upn')) { + + // add login_hint + urlNavigate += '&login_hint=' + encodeURIComponent(this._user.profile.upn); - if (parsedJson.hasOwnProperty('upn')) { - user.userName = parsedJson.upn; - } else if (parsedJson.hasOwnProperty('email')) { - user.userName = parsedJson.email; + // don't add domain_hint twice if user provided it in the extraQueryParameter value + if (!this._urlContainsQueryStringParameter("domain_hint", urlNavigate) && this._user.profile.upn.indexOf('@') > -1) { + var parts = this._user.profile.upn.split('@'); + // local part can include @ in quotes. Sending last part handles that. + urlNavigate += '&domain_hint=' + encodeURIComponent(parts[parts.length - 1]); } - } else { - this.warn('IdToken has invalid aud field'); } + return urlNavigate; } - return user; -}; + AuthenticationContext.prototype._createUser = function (idToken) { + var user = null; + var parsedJson = this._extractIdToken(idToken); + if (parsedJson && parsedJson.hasOwnProperty('aud')) { + if (parsedJson.aud.toLowerCase() === this.config.clientId.toLowerCase()) { + + user = { + userName: '', + profile: parsedJson + }; + + if (parsedJson.hasOwnProperty('upn')) { + user.userName = parsedJson.upn; + } else if (parsedJson.hasOwnProperty('email')) { + user.userName = parsedJson.email; + } + } else { + this.warn('IdToken has invalid aud field'); + } -AuthenticationContext.prototype._getHash = function (hash) { - if (hash.indexOf('#/') > -1) { - hash = hash.substring(hash.indexOf('#/') + 2); - } else if (hash.indexOf('#') > -1) { - hash = hash.substring(1); - } + } + + return user; + }; + + AuthenticationContext.prototype._getHash = function (hash) { + if (hash.indexOf('#/') > -1) { + hash = hash.substring(hash.indexOf('#/') + 2); + } else if (hash.indexOf('#') > -1) { + hash = hash.substring(1); + } - return hash; -}; - -/** - * Checks if hash contains access token or id token or error_description - * @param {string} hash - Hash passed from redirect page - * @returns {Boolean} - */ -AuthenticationContext.prototype.isCallback = function (hash) { - hash = this._getHash(hash); - var parameters = this._deserialize(hash); - return ( - parameters.hasOwnProperty(this.CONSTANTS.ERROR_DESCRIPTION) || - parameters.hasOwnProperty(this.CONSTANTS.ACCESS_TOKEN) || - parameters.hasOwnProperty(this.CONSTANTS.ID_TOKEN) - ); -}; - -/** - * Gets login error - * @returns {string} error message related to login - */ -AuthenticationContext.prototype.getLoginError = function () { - return this._getItem(this.CONSTANTS.STORAGE.LOGIN_ERROR); -}; - -/** - * Gets requestInfo from given hash. - * @returns {string} error message related to login - */ -AuthenticationContext.prototype.getRequestInfo = function (hash) { - hash = this._getHash(hash); - var parameters = this._deserialize(hash); - var requestInfo = { - valid: false, - parameters: {}, - stateMatch: false, - stateResponse: '', - requestType: this.REQUEST_TYPE.UNKNOWN + return hash; }; - if (parameters) { - requestInfo.parameters = parameters; - if (parameters.hasOwnProperty(this.CONSTANTS.ERROR_DESCRIPTION) || + + /** + * Checks if hash contains access token or id token or error_description + * @param {string} hash - Hash passed from redirect page + * @returns {Boolean} + */ + AuthenticationContext.prototype.isCallback = function (hash) { + hash = this._getHash(hash); + var parameters = this._deserialize(hash); + return ( + parameters.hasOwnProperty(this.CONSTANTS.ERROR_DESCRIPTION) || parameters.hasOwnProperty(this.CONSTANTS.ACCESS_TOKEN) || - parameters.hasOwnProperty(this.CONSTANTS.ID_TOKEN)) { + parameters.hasOwnProperty(this.CONSTANTS.ID_TOKEN) + ); + }; - requestInfo.valid = true; + /** + * Gets login error + * @returns {string} error message related to login + */ + AuthenticationContext.prototype.getLoginError = function () { + return this._getItem(this.CONSTANTS.STORAGE.LOGIN_ERROR); + }; - // which call - var stateResponse = ''; - if (parameters.hasOwnProperty('state')) { - this.verbose('State: ' + parameters.state); - stateResponse = parameters.state; - } else { - this.warn('No state returned'); - return requestInfo; - } + /** + * Gets requestInfo from given hash. + * @returns {string} error message related to login + */ + AuthenticationContext.prototype.getRequestInfo = function (hash) { + hash = this._getHash(hash); + var parameters = this._deserialize(hash); + var requestInfo = { + valid: false, + parameters: {}, + stateMatch: false, + stateResponse: '', + requestType: this.REQUEST_TYPE.UNKNOWN + }; + if (parameters) { + requestInfo.parameters = parameters; + if (parameters.hasOwnProperty(this.CONSTANTS.ERROR_DESCRIPTION) || + parameters.hasOwnProperty(this.CONSTANTS.ACCESS_TOKEN) || + parameters.hasOwnProperty(this.CONSTANTS.ID_TOKEN)) { + + requestInfo.valid = true; + + // which call + var stateResponse = ''; + if (parameters.hasOwnProperty('state')) { + this.verbose('State: ' + parameters.state); + stateResponse = parameters.state; + } else { + this.warn('No state returned'); + return requestInfo; + } - requestInfo.stateResponse = stateResponse; + requestInfo.stateResponse = stateResponse; - // async calls can fire iframe and login request at the same time if developer does not use the API as expected - // incoming callback needs to be looked up to find the request type - if (stateResponse === this._getItem(this.CONSTANTS.STORAGE.STATE_LOGIN)) { - requestInfo.requestType = this.REQUEST_TYPE.LOGIN; - requestInfo.stateMatch = true; - return requestInfo; - } + // async calls can fire iframe and login request at the same time if developer does not use the API as expected + // incoming callback needs to be looked up to find the request type + if (stateResponse === this._getItem(this.CONSTANTS.STORAGE.STATE_LOGIN)) { + requestInfo.requestType = this.REQUEST_TYPE.LOGIN; + requestInfo.stateMatch = true; + return requestInfo; + } - // external api requests may have many renewtoken requests for different resource - if (!requestInfo.stateMatch && window.parent && window.parent.AuthenticationContext()) { - var statesInParentContext = window.parent.AuthenticationContext()._renewStates; - for (var i = 0; i < statesInParentContext.length; i++) { - if (statesInParentContext[i] === requestInfo.stateResponse) { - requestInfo.requestType = this.REQUEST_TYPE.RENEW_TOKEN; - requestInfo.stateMatch = true; - break; + // external api requests may have many renewtoken requests for different resource + if (!requestInfo.stateMatch && window.parent && window.parent.AuthenticationContext()) { + var statesInParentContext = window.parent.AuthenticationContext()._renewStates; + for (var i = 0; i < statesInParentContext.length; i++) { + if (statesInParentContext[i] === requestInfo.stateResponse) { + requestInfo.requestType = this.REQUEST_TYPE.RENEW_TOKEN; + requestInfo.stateMatch = true; + break; + } } } } } - } - return requestInfo; -}; + return requestInfo; + }; -AuthenticationContext.prototype._getResourceFromState = function (state) { - if (state) { - var splitIndex = state.indexOf('|'); - if (splitIndex > -1 && splitIndex + 1 < state.length) { - return state.substring(splitIndex + 1); + AuthenticationContext.prototype._getResourceFromState = function (state) { + if (state) { + var splitIndex = state.indexOf('|'); + if (splitIndex > -1 && splitIndex + 1 < state.length) { + return state.substring(splitIndex + 1); + } } - } - return ''; -}; - -/** - * Saves token from hash that is received from redirect. - * @param {string} hash - Hash passed from redirect page - * @returns {string} error message related to login - */ -AuthenticationContext.prototype.saveTokenFromHash = function (requestInfo) { - this.info('State status:' + requestInfo.stateMatch + '; Request type:' + requestInfo.requestType); - this._saveItem(this.CONSTANTS.STORAGE.ERROR, ''); - this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, ''); - - // Record error - if (requestInfo.parameters.hasOwnProperty(this.CONSTANTS.ERROR_DESCRIPTION)) { - this.info('Error :' + requestInfo.parameters.error + '; Error description:' + requestInfo.parameters[this.CONSTANTS.ERROR_DESCRIPTION]); - this._saveItem(this.CONSTANTS.STORAGE.ERROR, requestInfo.parameters.error); - this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, requestInfo.parameters[this.CONSTANTS.ERROR_DESCRIPTION]); - - if (requestInfo.requestType === this.REQUEST_TYPE.LOGIN) { - this._loginInProgress = false; - this._saveItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, requestInfo.parameters.errorDescription); - } - } else { - - // It must verify the state from redirect - if (requestInfo.stateMatch) { - // record tokens to storage if exists - this.info('State is right'); - if (requestInfo.parameters.hasOwnProperty(this.CONSTANTS.SESSION_STATE)) { - this._saveItem(this.CONSTANTS.STORAGE.SESSION_STATE, requestInfo.parameters[this.CONSTANTS.SESSION_STATE]); + return ''; + }; + + /** + * Saves token from hash that is received from redirect. + * @param {string} hash - Hash passed from redirect page + * @returns {string} error message related to login + */ + AuthenticationContext.prototype.saveTokenFromHash = function (requestInfo) { + this.info('State status:' + requestInfo.stateMatch + '; Request type:' + requestInfo.requestType); + this._saveItem(this.CONSTANTS.STORAGE.ERROR, ''); + this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, ''); + + var resource = this._getResourceFromState(requestInfo.stateResponse); + + // Record error + if (requestInfo.parameters.hasOwnProperty(this.CONSTANTS.ERROR_DESCRIPTION)) { + this.info('Error :' + requestInfo.parameters.error + '; Error description:' + requestInfo.parameters[this.CONSTANTS.ERROR_DESCRIPTION]); + this._saveItem(this.CONSTANTS.STORAGE.ERROR, requestInfo.parameters.error); + this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, requestInfo.parameters[this.CONSTANTS.ERROR_DESCRIPTION]); + + if (requestInfo.requestType === this.REQUEST_TYPE.LOGIN) { + this._loginInProgress = false; + this._saveItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, requestInfo.parameters.error_description); } + } else { + // It must verify the state from redirect + if (requestInfo.stateMatch) { + // record tokens to storage if exists + this.info('State is right'); + if (requestInfo.parameters.hasOwnProperty(this.CONSTANTS.SESSION_STATE)) { + this._saveItem(this.CONSTANTS.STORAGE.SESSION_STATE, requestInfo.parameters[this.CONSTANTS.SESSION_STATE]); + } - var keys, resource; + var keys; - if (requestInfo.parameters.hasOwnProperty(this.CONSTANTS.ACCESS_TOKEN)) { - this.info('Fragment has access token'); - resource = this._getResourceFromState(requestInfo.stateResponse); + if (requestInfo.parameters.hasOwnProperty(this.CONSTANTS.ACCESS_TOKEN)) { + this.info('Fragment has access token'); - if (!this._hasResource(resource)) { - keys = this._getItem(this.CONSTANTS.STORAGE.TOKEN_KEYS) || ''; - this._saveItem(this.CONSTANTS.STORAGE.TOKEN_KEYS, keys + resource + this.CONSTANTS.RESOURCE_DELIMETER); + if (!this._hasResource(resource)) { + keys = this._getItem(this.CONSTANTS.STORAGE.TOKEN_KEYS) || ''; + this._saveItem(this.CONSTANTS.STORAGE.TOKEN_KEYS, keys + resource + this.CONSTANTS.RESOURCE_DELIMETER); + } + // save token with related resource + this._saveItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + resource, requestInfo.parameters[this.CONSTANTS.ACCESS_TOKEN]); + this._saveItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + resource, this._expiresIn(requestInfo.parameters[this.CONSTANTS.EXPIRES_IN])); } - // save token with related resource - this._saveItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + resource, requestInfo.parameters[this.CONSTANTS.ACCESS_TOKEN]); - this._saveItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + resource, this._expiresIn(requestInfo.parameters[this.CONSTANTS.EXPIRES_IN])); - } - if (requestInfo.parameters.hasOwnProperty(this.CONSTANTS.ID_TOKEN)) { - this.info('Fragment has id token'); - this._loginInProgress = false; - this._user = this._createUser(requestInfo.parameters[this.CONSTANTS.ID_TOKEN]); - if (this._user && this._user.profile) { - if (this._user.profile.nonce !== this._getItem(this.CONSTANTS.STORAGE.NONCE_IDTOKEN)) { - this._user = null; - this._saveItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, 'Nonce is not same as ' + this._idTokenNonce); - } else { - this._saveItem(this.CONSTANTS.STORAGE.IDTOKEN, requestInfo.parameters[this.CONSTANTS.ID_TOKEN]); - - // Save idtoken as access token for app itself - resource = this.config.loginResource ? this.config.loginResource : this.config.clientId; - if (!this._hasResource(resource)) { - keys = this._getItem(this.CONSTANTS.STORAGE.TOKEN_KEYS) || ''; - this._saveItem(this.CONSTANTS.STORAGE.TOKEN_KEYS, keys + resource + this.CONSTANTS.RESOURCE_DELIMETER); + if (requestInfo.parameters.hasOwnProperty(this.CONSTANTS.ID_TOKEN)) { + this.info('Fragment has id token'); + this._loginInProgress = false; + + this._user = this._createUser(requestInfo.parameters[this.CONSTANTS.ID_TOKEN]); + + if (this._user && this._user.profile) { + if (this._user.profile.nonce !== this._getItem(this.CONSTANTS.STORAGE.NONCE_IDTOKEN)) { + this._user = null; + this._saveItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, 'Nonce is not same as ' + this._idTokenNonce); + } else { + this._saveItem(this.CONSTANTS.STORAGE.IDTOKEN, requestInfo.parameters[this.CONSTANTS.ID_TOKEN]); + + // Save idtoken as access token for app itself + resource = this.config.loginResource ? this.config.loginResource : this.config.clientId; + + if (!this._hasResource(resource)) { + keys = this._getItem(this.CONSTANTS.STORAGE.TOKEN_KEYS) || ''; + this._saveItem(this.CONSTANTS.STORAGE.TOKEN_KEYS, keys + resource + this.CONSTANTS.RESOURCE_DELIMETER); + } + this._saveItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + resource, requestInfo.parameters[this.CONSTANTS.ID_TOKEN]); + this._saveItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + resource, this._user.profile.exp); } - this._saveItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + resource, requestInfo.parameters[this.CONSTANTS.ID_TOKEN]); - this._saveItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + resource, this._user.profile.exp); + } + else { + this._saveItem(this.CONSTANTS.STORAGE.ERROR, 'invalid id_token'); + this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, 'Invalid id_token. id_token: ' + requestInfo.parameters[this.CONSTANTS.ID_TOKEN]); } } + } else { + this._saveItem(this.CONSTANTS.STORAGE.ERROR, 'Invalid_state'); + this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, 'Invalid_state. state: ' + requestInfo.stateResponse); } - } else { - this._saveItem(this.CONSTANTS.STORAGE.ERROR, 'Invalid_state'); - this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, 'Invalid_state. state: ' + requestInfo.stateResponse); } - } -}; - -/** - * Gets resource for given endpoint if mapping is provided with config. - * @param {string} endpoint - API endoibt - * @returns {string} resource for this API endpoint - */ -AuthenticationContext.prototype.getResourceForEndpoint = function (endpoint) { - if (this.config && this.config.endpoints) { - for (var configEndpoint in this.config.endpoints) { - // configEndpoint is like /api/Todo requested endpoint can be /api/Todo/1 - if (endpoint.indexOf(configEndpoint) > -1) { - return this.config.endpoints[configEndpoint]; + this._saveItem(this.CONSTANTS.STORAGE.RENEW_STATUS + resource, this.CONSTANTS.TOKEN_RENEW_STATUS_COMPLETED); + }; + + /** + * Gets resource for given endpoint if mapping is provided with config. + * @param {string} endpoint - API endoibt + * @returns {string} resource for this API endpoint + */ + AuthenticationContext.prototype.getResourceForEndpoint = function (endpoint) { + if (this.config && this.config.endpoints) { + for (var configEndpoint in this.config.endpoints) { + // configEndpoint is like /api/Todo requested endpoint can be /api/Todo/1 + if (endpoint.indexOf(configEndpoint) > -1) { + return this.config.endpoints[configEndpoint]; + } } } - } - // default resource will be clientid if nothing specified - // App will use idtoken for calls to itself - // check if it's staring from http or https, needs to match with app host - if (endpoint.indexOf('http://') > -1 || endpoint.indexOf('https://') > -1) { - if (this._getHostFromUri(endpoint) === this._getHostFromUri(this.config.redirectUri)) { - return this.config.loginResource; + // default resource will be clientid if nothing specified + // App will use idtoken for calls to itself + // check if it's staring from http or https, needs to match with app host + if (endpoint.indexOf('http://') > -1 || endpoint.indexOf('https://') > -1) { + if (this._getHostFromUri(endpoint) === this._getHostFromUri(this.config.redirectUri)) { + return this.config.loginResource; + } } - } - // in angular level, the url for $http interceptor call could be relative url, - // if it's relative call, we'll treat it as app backend call. - else { - // if user specified list of anonymous endpoints, no need to send token to these endpoints, return null. - if (this.config && this.config.anonymousEndpoints) { - for (var i = 0; i < this.config.anonymousEndpoints.length; i++) { - if (endpoint.indexOf(this.config.anonymousEndpoints[i]) > -1) { - return null; + // in angular level, the url for $http interceptor call could be relative url, + // if it's relative call, we'll treat it as app backend call. + else { + // if user specified list of anonymous endpoints, no need to send token to these endpoints, return null. + if (this.config && this.config.anonymousEndpoints) { + for (var i = 0; i < this.config.anonymousEndpoints.length; i++) { + if (endpoint.indexOf(this.config.anonymousEndpoints[i]) > -1) { + return null; + } } } + // all other app's backend calls are secured. + return this.config.loginResource; } - // all other app's backend calls are secured. - return this.config.loginResource; - } - // if not the app's own backend or not a domain listed in the endpoints structure - return null; -}; - -AuthenticationContext.prototype._getHostFromUri = function (uri) { - // remove http:// or https:// from uri - var extractedUri = String(uri).replace(/^(https?:)\/\//, ''); - - extractedUri = extractedUri.split('/')[0]; - return extractedUri; -}; - -/*exported oauth2Callback */ -AuthenticationContext.prototype.handleWindowCallback = function () { - // This is for regular javascript usage for redirect handling - // need to make sure this is for callback - var hash = window.location.hash; - if (this.isCallback(hash)) { - var requestInfo = this.getRequestInfo(hash); - this.info('Returned from redirect url'); - this.saveTokenFromHash(requestInfo); - var callback = null; - if ((requestInfo.requestType === this.REQUEST_TYPE.RENEW_TOKEN && window.parent)) { - // iframe call but same single page - this.verbose('Window is in iframe'); - callback = window.parent.callBackMappedToRenewStates[requestInfo.stateResponse]; - window.src = ''; - } else if (window && window.oauth2Callback) { - this.verbose('Window is redirecting'); - callback = this.callback; - } + // if not the app's own backend or not a domain listed in the endpoints structure + return null; + }; - window.location.hash = ''; - window.location = this._getItem(this.CONSTANTS.STORAGE.LOGIN_REQUEST); - if (requestInfo.requestType === this.REQUEST_TYPE.RENEW_TOKEN) { - callback(this._getItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION), requestInfo.parameters[this.CONSTANTS.ACCESS_TOKEN] || requestInfo.parameters[this.CONSTANTS.ID_TOKEN]); - return; - } - } -}; + AuthenticationContext.prototype._getHostFromUri = function (uri) { + // remove http:// or https:// from uri + var extractedUri = String(uri).replace(/^(https?:)\/\//, ''); -AuthenticationContext.prototype._getNavigateUrl = function (responseType, resource) { - var tenant = 'common'; - if (this.config.tenant) { - tenant = this.config.tenant; - } + extractedUri = extractedUri.split('/')[0]; + return extractedUri; + }; - if (this.config.instance) { - this.instance = this.config.instance; - } + /*exported oauth2Callback */ + AuthenticationContext.prototype.handleWindowCallback = function () { + // This is for regular javascript usage for redirect handling + // need to make sure this is for callback + var hash = window.location.hash; + if (this.isCallback(hash)) { + var requestInfo = this.getRequestInfo(hash); + this.info('Returned from redirect url'); + this.saveTokenFromHash(requestInfo); + var callback = null; + if ((requestInfo.requestType === this.REQUEST_TYPE.RENEW_TOKEN) && window.parent && (window.parent !== window)) { + // iframe call but same single page + this.verbose('Window is in iframe'); + callback = window.parent.callBackMappedToRenewStates[requestInfo.stateResponse]; + if (callback) + callback(this._getItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION), requestInfo.parameters[this.CONSTANTS.ACCESS_TOKEN] || requestInfo.parameters[this.CONSTANTS.ID_TOKEN]); + return; + } else if (window && window.oauth2Callback) { + this.verbose('Window is redirecting'); + callback = this.callback; + } + window.location = this._getItem(this.CONSTANTS.STORAGE.LOGIN_REQUEST); + } + }; - var urlNavigate = this.instance + tenant + '/oauth2/authorize' + this._serialize(responseType, this.config, resource) + this._addLibMetadata(); - this.info('Navigate url:' + urlNavigate); - return urlNavigate; -}; + AuthenticationContext.prototype._getNavigateUrl = function (responseType, resource) { + var tenant = 'common'; + if (this.config.tenant) { + tenant = this.config.tenant; + } -AuthenticationContext.prototype._extractIdToken = function (encodedIdToken) { - // id token will be decoded to get the username - var decodedToken = this._decodeJwt(encodedIdToken); - if (!decodedToken) { - return null; - } + var urlNavigate = this.instance + tenant + '/oauth2/authorize' + this._serialize(responseType, this.config, resource) + this._addLibMetadata(); + this.info('Navigate url:' + urlNavigate); + return urlNavigate; + }; - try { - var base64IdToken = decodedToken.JWSPayload; - var base64Decoded = this._base64DecodeStringUrlSafe(base64IdToken); - if (!base64Decoded) { - this.info('The returned id_token could not be base64 url safe decoded.'); + AuthenticationContext.prototype._extractIdToken = function (encodedIdToken) { + // id token will be decoded to get the username + var decodedToken = this._decodeJwt(encodedIdToken); + if (!decodedToken) { return null; } - // ECMA script has JSON built-in support - return JSON.parse(base64Decoded); - } catch (err) { - this.error('The returned id_token could not be decoded', err); - } - - return null; -}; - -AuthenticationContext.prototype._extractUserName = function (encodedIdToken) { - // id token will be decoded to get the username - try { - var parsed = this._extractIdToken(encodedIdToken); - if (parsed) { - if (parsed.hasOwnProperty('upn')) { - return parsed.upn; - } else if (parsed.hasOwnProperty('email')) { - return parsed.email; + try { + var base64IdToken = decodedToken.JWSPayload; + var base64Decoded = this._base64DecodeStringUrlSafe(base64IdToken); + if (!base64Decoded) { + this.info('The returned id_token could not be base64 url safe decoded.'); + return null; } + + // ECMA script has JSON built-in support + return JSON.parse(base64Decoded); + } catch (err) { + this.error('The returned id_token could not be decoded', err); } - } catch (err) { - this.error('The returned id_token could not be decoded', err); - } - return null; -}; + return null; + }; -AuthenticationContext.prototype._base64DecodeStringUrlSafe = function (base64IdToken) { - // html5 should support atob function for decoding - base64IdToken = base64IdToken.replace(/-/g, '+').replace(/_/g, '/'); - if (window.atob) { - return decodeURIComponent(escape(window.atob(base64IdToken))); // jshint ignore:line - } - else { - return decodeURIComponent(escape(this._decode(base64IdToken))); - } -}; + AuthenticationContext.prototype._base64DecodeStringUrlSafe = function (base64IdToken) { + // html5 should support atob function for decoding + base64IdToken = base64IdToken.replace(/-/g, '+').replace(/_/g, '/'); + if (window.atob) { + return decodeURIComponent(escape(window.atob(base64IdToken))); // jshint ignore:line + } + else { + return decodeURIComponent(escape(this._decode(base64IdToken))); + } + }; -//Take https://cdnjs.cloudflare.com/ajax/libs/Base64/0.3.0/base64.js and https://en.wikipedia.org/wiki/Base64 as reference. -AuthenticationContext.prototype._decode = function (base64IdToken) { - var codes = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; - base64IdToken = String(base64IdToken).replace(/=+$/, ''); + //Take https://cdnjs.cloudflare.com/ajax/libs/Base64/0.3.0/base64.js and https://en.wikipedia.org/wiki/Base64 as reference. + AuthenticationContext.prototype._decode = function (base64IdToken) { + var codes = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + base64IdToken = String(base64IdToken).replace(/=+$/, ''); - var length = base64IdToken.length; - if (length % 4 === 1) { - throw new Error('The token to be decoded is not correctly encoded.'); - } + var length = base64IdToken.length; + if (length % 4 === 1) { + throw new Error('The token to be decoded is not correctly encoded.'); + } + + var h1, h2, h3, h4, bits, c1, c2, c3, decoded = ''; + for (var i = 0; i < length; i += 4) { + //Every 4 base64 encoded character will be converted to 3 byte string, which is 24 bits + // then 6 bits per base64 encoded character + h1 = codes.indexOf(base64IdToken.charAt(i)); + h2 = codes.indexOf(base64IdToken.charAt(i + 1)); + h3 = codes.indexOf(base64IdToken.charAt(i + 2)); + h4 = codes.indexOf(base64IdToken.charAt(i + 3)); + + // For padding, if last two are '=' + if (i + 2 === length - 1) { + bits = h1 << 18 | h2 << 12 | h3 << 6; + c1 = bits >> 16 & 255; + c2 = bits >> 8 & 255; + decoded += String.fromCharCode(c1, c2); + break; + } + // if last one is '=' + else if (i + 1 === length - 1) { + bits = h1 << 18 | h2 << 12 + c1 = bits >> 16 & 255; + decoded += String.fromCharCode(c1); + break; + } + + bits = h1 << 18 | h2 << 12 | h3 << 6 | h4; - var h1, h2, h3, h4, bits, c1, c2, c3, decoded = ''; - for (var i = 0; i < length; i += 4) { - //Every 4 base64 encoded character will be converted to 3 byte string, which is 24 bits - // then 6 bits per base64 encoded character - h1 = codes.indexOf(base64IdToken.charAt(i)); - h2 = codes.indexOf(base64IdToken.charAt(i + 1)); - h3 = codes.indexOf(base64IdToken.charAt(i + 2)); - h4 = codes.indexOf(base64IdToken.charAt(i + 3)); - - // For padding, if last two are '=' - if (i + 2 === length - 1) { - bits = h1 << 18 | h2 << 12 | h3 << 6; + // then convert to 3 byte chars c1 = bits >> 16 & 255; c2 = bits >> 8 & 255; - decoded += String.fromCharCode(c1, c2); - break; - } - // if last one is '=' - else if (i + 1 === length - 1) { - bits = h1 << 18 | h2 << 12 - c1 = bits >> 16 & 255; - decoded += String.fromCharCode(c1); - break; - } + c3 = bits & 255; - bits = h1 << 18 | h2 << 12 | h3 << 6 | h4; + decoded += String.fromCharCode(c1, c2, c3); + } - // then convert to 3 byte chars - c1 = bits >> 16 & 255; - c2 = bits >> 8 & 255; - c3 = bits & 255; + return decoded; + }; - decoded += String.fromCharCode(c1, c2, c3); - } + // Adal.node js crack function + AuthenticationContext.prototype._decodeJwt = function (jwtToken) { + if (this._isEmpty(jwtToken)) { + return null; + }; - return decoded; -}; + var idTokenPartsRegex = /^([^\.\s]*)\.([^\.\s]+)\.([^\.\s]*)$/; -// Adal.node js crack function -AuthenticationContext.prototype._decodeJwt = function (jwtToken) { - if (this._isEmpty(jwtToken)) { - return null; - }; + var matches = idTokenPartsRegex.exec(jwtToken); + if (!matches || matches.length < 4) { + this.warn('The returned id_token is not parseable.'); + return null; + } - var idTokenPartsRegex = /^([^\.\s]*)\.([^\.\s]+)\.([^\.\s]*)$/; + var crackedToken = { + header: matches[1], + JWSPayload: matches[2], + JWSSig: matches[3] + }; - var matches = idTokenPartsRegex.exec(jwtToken); - if (!matches || matches.length < 4) { - this.warn('The returned id_token is not parseable.'); - return null; - } + return crackedToken; + }; - var crackedToken = { - header: matches[1], - JWSPayload: matches[2], - JWSSig: matches[3] + AuthenticationContext.prototype._convertUrlSafeToRegularBase64EncodedString = function (str) { + return str.replace('-', '+').replace('_', '/'); }; - return crackedToken; -}; + AuthenticationContext.prototype._serialize = function (responseType, obj, resource) { + var str = []; + if (obj !== null) { + str.push('?response_type=' + responseType); + str.push('client_id=' + encodeURIComponent(obj.clientId)); + if (resource) { + str.push('resource=' + encodeURIComponent(resource)); + } -AuthenticationContext.prototype._convertUrlSafeToRegularBase64EncodedString = function (str) { - return str.replace('-', '+').replace('_', '/'); -}; + str.push('redirect_uri=' + encodeURIComponent(obj.redirectUri)); + str.push('state=' + encodeURIComponent(obj.state)); -AuthenticationContext.prototype._serialize = function (responseType, obj, resource) { - var str = []; - if (obj !== null) { - str.push('?response_type=' + responseType); - str.push('client_id=' + encodeURIComponent(obj.clientId)); - if (resource) { - str.push('resource=' + encodeURIComponent(resource)); - } + if (obj.hasOwnProperty('slice')) { + str.push('slice=' + encodeURIComponent(obj.slice)); + } - str.push('redirect_uri=' + encodeURIComponent(obj.redirectUri)); - str.push('state=' + encodeURIComponent(obj.state)); + if (obj.hasOwnProperty('extraQueryParameter')) { + str.push(obj.extraQueryParameter); + } - if (obj.hasOwnProperty('slice')) { - str.push('slice=' + encodeURIComponent(obj.slice)); + var correlationId = obj.correlationId ? obj.correlationId : this._guid(); + str.push('client-request-id=' + encodeURIComponent(correlationId)); } - if (obj.hasOwnProperty('extraQueryParameter')) { - str.push(obj.extraQueryParameter); - } + return str.join('&'); + }; - if (obj.correlationId) { - str.push('client-request-id=' + encodeURIComponent(obj.correlationId)); + AuthenticationContext.prototype._deserialize = function (query) { + var match, + pl = /\+/g, // Regex for replacing addition symbol with a space + search = /([^&=]+)=([^&]*)/g, + decode = function (s) { + return decodeURIComponent(s.replace(pl, ' ')); + }, + obj = {}; + match = search.exec(query); + while (match) { + obj[decode(match[1])] = decode(match[2]); + match = search.exec(query); } - } - return str.join('&'); -}; - -AuthenticationContext.prototype._deserialize = function (query) { - var match, - pl = /\+/g, // Regex for replacing addition symbol with a space - search = /([^&=]+)=?([^&]*)/g, - decode = function (s) { - return decodeURIComponent(s.replace(pl, ' ')); - }, - obj = {}; - match = search.exec(query); - while (match) { - obj[decode(match[1])] = decode(match[2]); - match = search.exec(query); - } + return obj; + }; - return obj; -}; - -/* jshint ignore:start */ -AuthenticationContext.prototype._guid = function () { - // RFC4122: The version 4 UUID is meant for generating UUIDs from truly-random or - // pseudo-random numbers. - // The algorithm is as follows: - // Set the two most significant bits (bits 6 and 7) of the - // clock_seq_hi_and_reserved to zero and one, respectively. - // Set the four most significant bits (bits 12 through 15) of the - // time_hi_and_version field to the 4-bit version number from - // Section 4.1.3. Version4 - // Set all the other bits to randomly (or pseudo-randomly) chosen - // values. - // UUID = time-low "-" time-mid "-"time-high-and-version "-"clock-seq-reserved and low(2hexOctet)"-" node - // time-low = 4hexOctet - // time-mid = 2hexOctet - // time-high-and-version = 2hexOctet - // clock-seq-and-reserved = hexOctet: - // clock-seq-low = hexOctet - // node = 6hexOctet - // Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx - // y could be 1000, 1001, 1010, 1011 since most significant two bits needs to be 10 - // y values are 8, 9, A, B - var guidHolder = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'; - var hex = '0123456789abcdef'; - var r = 0; - var guidResponse = ""; - for (var i = 0; i < 36; i++) { - if (guidHolder[i] !== '-' && guidHolder[i] !== '4') { - // each x and y needs to be random - r = Math.random() * 16 | 0; - } + /* jshint ignore:start */ + AuthenticationContext.prototype._guid = function () { + // RFC4122: The version 4 UUID is meant for generating UUIDs from truly-random or + // pseudo-random numbers. + // The algorithm is as follows: + // Set the two most significant bits (bits 6 and 7) of the + // clock_seq_hi_and_reserved to zero and one, respectively. + // Set the four most significant bits (bits 12 through 15) of the + // time_hi_and_version field to the 4-bit version number from + // Section 4.1.3. Version4 + // Set all the other bits to randomly (or pseudo-randomly) chosen + // values. + // UUID = time-low "-" time-mid "-"time-high-and-version "-"clock-seq-reserved and low(2hexOctet)"-" node + // time-low = 4hexOctet + // time-mid = 2hexOctet + // time-high-and-version = 2hexOctet + // clock-seq-and-reserved = hexOctet: + // clock-seq-low = hexOctet + // node = 6hexOctet + // Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx + // y could be 1000, 1001, 1010, 1011 since most significant two bits needs to be 10 + // y values are 8, 9, A, B + var guidHolder = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'; + var hex = '0123456789abcdef'; + var r = 0; + var guidResponse = ""; + for (var i = 0; i < 36; i++) { + if (guidHolder[i] !== '-' && guidHolder[i] !== '4') { + // each x and y needs to be random + r = Math.random() * 16 | 0; + } - if (guidHolder[i] === 'x') { - guidResponse += hex[r]; - } else if (guidHolder[i] === 'y') { - // clock-seq-and-reserved first hex is filtered and remaining hex values are random - r &= 0x3; // bit and with 0011 to set pos 2 to zero ?0?? - r |= 0x8; // set pos 3 to 1 as 1??? - guidResponse += hex[r]; - } else { - guidResponse += guidHolder[i]; + if (guidHolder[i] === 'x') { + guidResponse += hex[r]; + } else if (guidHolder[i] === 'y') { + // clock-seq-and-reserved first hex is filtered and remaining hex values are random + r &= 0x3; // bit and with 0011 to set pos 2 to zero ?0?? + r |= 0x8; // set pos 3 to 1 as 1??? + guidResponse += hex[r]; + } else { + guidResponse += guidHolder[i]; + } } - } - return guidResponse; -}; -/* jshint ignore:end */ + return guidResponse; + }; + /* jshint ignore:end */ -AuthenticationContext.prototype._expiresIn = function (expires) { - return this._now() + parseInt(expires, 10); -}; + AuthenticationContext.prototype._expiresIn = function (expires) { + return this._now() + parseInt(expires, 10); + }; -AuthenticationContext.prototype._now = function () { - return Math.round(new Date().getTime() / 1000.0); -}; + AuthenticationContext.prototype._now = function () { + return Math.round(new Date().getTime() / 1000.0); + }; -AuthenticationContext.prototype._addAdalFrame = function (iframeId) { - if (typeof iframeId === 'undefined') { - return; - } + AuthenticationContext.prototype._addAdalFrame = function (iframeId) { + if (typeof iframeId === 'undefined') { + return; + } - this.info('Add adal frame to document:' + iframeId); - var adalFrame = document.getElementById(iframeId); + this.info('Add adal frame to document:' + iframeId); + var adalFrame = document.getElementById(iframeId); - if (!adalFrame) { - if (document.createElement && document.documentElement && - (window.opera || window.navigator.userAgent.indexOf('MSIE 5.0') === -1)) { - var ifr = document.createElement('iframe'); - ifr.setAttribute('id', iframeId); - ifr.style.visibility = 'hidden'; - ifr.style.position = 'absolute'; - ifr.style.width = ifr.style.height = ifr.borderWidth = '0px'; + if (!adalFrame) { + if (document.createElement && document.documentElement && + (window.opera || window.navigator.userAgent.indexOf('MSIE 5.0') === -1)) { + var ifr = document.createElement('iframe'); + ifr.setAttribute('id', iframeId); + ifr.style.visibility = 'hidden'; + ifr.style.position = 'absolute'; + ifr.style.width = ifr.style.height = ifr.borderWidth = '0px'; - adalFrame = document.getElementsByTagName('body')[0].appendChild(ifr); - } - else if (document.body && document.body.insertAdjacentHTML) { - document.body.insertAdjacentHTML('beforeEnd', ''); - } - if (window.frames && window.frames[iframeId]) { - adalFrame = window.frames[iframeId]; + adalFrame = document.getElementsByTagName('body')[0].appendChild(ifr); + } + else if (document.body && document.body.insertAdjacentHTML) { + document.body.insertAdjacentHTML('beforeEnd', ''); + } + if (window.frames && window.frames[iframeId]) { + adalFrame = window.frames[iframeId]; + } } - } - return adalFrame; -}; + return adalFrame; + }; -AuthenticationContext.prototype._saveItem = function (key, obj) { + AuthenticationContext.prototype._saveItem = function (key, obj) { - if (this.config && this.config.cacheLocation && this.config.cacheLocation === 'localStorage') { + if (this.config && this.config.cacheLocation && this.config.cacheLocation === 'localStorage') { - if (!this._supportsLocalStorage()) { - this.info('Local storage is not supported'); - return false; + if (!this._supportsLocalStorage()) { + this.info('Local storage is not supported'); + return false; + } + + localStorage.setItem(key, obj); + + return true; } - localStorage.setItem(key, obj); + // Default as session storage + if (!this._supportsSessionStorage()) { + this.info('Session storage is not supported'); + return false; + } + sessionStorage.setItem(key, obj); return true; - } + }; - // Default as session storage - if (!this._supportsSessionStorage()) { - this.info('Session storage is not supported'); - return false; - } + AuthenticationContext.prototype._getItem = function (key) { - sessionStorage.setItem(key, obj); - return true; -}; + if (this.config && this.config.cacheLocation && this.config.cacheLocation === 'localStorage') { -AuthenticationContext.prototype._getItem = function (key) { + if (!this._supportsLocalStorage()) { + this.info('Local storage is not supported'); + return null; + } - if (this.config && this.config.cacheLocation && this.config.cacheLocation === 'localStorage') { + return localStorage.getItem(key); + } - if (!this._supportsLocalStorage()) { - this.info('Local storage is not supported'); + // Default as session storage + if (!this._supportsSessionStorage()) { + this.info('Session storage is not supported'); return null; } - return localStorage.getItem(key); - } - - // Default as session storage - if (!this._supportsSessionStorage()) { - this.info('Session storage is not supported'); - return null; - } - - return sessionStorage.getItem(key); -}; + return sessionStorage.getItem(key); + }; -AuthenticationContext.prototype._supportsLocalStorage = function () { - try { - return 'localStorage' in window && window['localStorage']; - } catch (e) { - return false; - } -}; + AuthenticationContext.prototype._supportsLocalStorage = function () { + try { + return 'localStorage' in window && window['localStorage']; + } catch (e) { + return false; + } + }; -AuthenticationContext.prototype._supportsSessionStorage = function () { - try { - return 'sessionStorage' in window && window['sessionStorage']; - } catch (e) { - return false; - } -}; + AuthenticationContext.prototype._supportsSessionStorage = function () { + try { + return 'sessionStorage' in window && window['sessionStorage']; + } catch (e) { + return false; + } + }; -AuthenticationContext.prototype._cloneConfig = function (obj) { - if (null === obj || 'object' !== typeof obj) { - return obj; - } + AuthenticationContext.prototype._cloneConfig = function (obj) { + if (null === obj || 'object' !== typeof obj) { + return obj; + } - var copy = {}; - for (var attr in obj) { - if (obj.hasOwnProperty(attr)) { - copy[attr] = obj[attr]; + var copy = {}; + for (var attr in obj) { + if (obj.hasOwnProperty(attr)) { + copy[attr] = obj[attr]; + } } - } - return copy; -}; + return copy; + }; -AuthenticationContext.prototype._addLibMetadata = function () { - // x-client-SKU - // x-client-Ver - return '&x-client-SKU=Js&x-client-Ver=' + this._libVersion(); -}; + AuthenticationContext.prototype._addLibMetadata = function () { + // x-client-SKU + // x-client-Ver + return '&x-client-SKU=Js&x-client-Ver=' + this._libVersion(); + }; -AuthenticationContext.prototype.log = function (level, message, error) { - if (level <= Logging.level) { - var correlationId = this.config.correlationId; - var timestamp = new Date().toUTCString(); + AuthenticationContext.prototype.log = function (level, message, error) { + if (level <= Logging.level) { + var timestamp = new Date().toUTCString(); + var formattedMessage = ''; - var formattedMessage = timestamp + ':' + correlationId + '-' + this._libVersion() + '-' + this.CONSTANTS.LEVEL_STRING_MAP[level] + ' ' + message; + if (this.config.correlationId) + formattedMessage = timestamp + ':' + this.config.correlationId + '-' + this._libVersion() + '-' + this.CONSTANTS.LEVEL_STRING_MAP[level] + ' ' + message; + else + formattedMessage = timestamp + ':' + this._libVersion() + '-' + this.CONSTANTS.LEVEL_STRING_MAP[level] + ' ' + message; - if (error) { - formattedMessage += '\nstack:\n' + error.stack; + if (error) { + formattedMessage += '\nstack:\n' + error.stack; + } + + Logging.log(formattedMessage); } + }; - Logging.log(formattedMessage); - } -}; + AuthenticationContext.prototype.error = function (message, error) { + this.log(this.CONSTANTS.LOGGING_LEVEL.ERROR, message, error); + }; + + AuthenticationContext.prototype.warn = function (message) { + this.log(this.CONSTANTS.LOGGING_LEVEL.WARN, message, null); + }; + + AuthenticationContext.prototype.info = function (message) { + this.log(this.CONSTANTS.LOGGING_LEVEL.INFO, message, null); + }; -AuthenticationContext.prototype.error = function (message, error) { - this.log(this.CONSTANTS.LOGGING_LEVEL.ERROR, message, error); -}; + AuthenticationContext.prototype.verbose = function (message) { + this.log(this.CONSTANTS.LOGGING_LEVEL.VERBOSE, message, null); + }; -AuthenticationContext.prototype.warn = function (message) { - this.log(this.CONSTANTS.LOGGING_LEVEL.WARN, message, null); -}; + AuthenticationContext.prototype._libVersion = function () { + return '1.0.11'; + }; -AuthenticationContext.prototype.info = function (message) { - this.log(this.CONSTANTS.LOGGING_LEVEL.INFO, message, null); -}; + if (typeof module !== 'undefined' && module.exports) { + module.exports = AuthenticationContext; + module.exports.inject = function (conf) { + return new AuthenticationContext(conf); + }; + } -AuthenticationContext.prototype.verbose = function (message) { - this.log(this.CONSTANTS.LOGGING_LEVEL.VERBOSE, message, null); -}; + return AuthenticationContext; -AuthenticationContext.prototype._libVersion = function () { - return '1.0.10'; -}; +}()); diff --git a/package.json b/package.json index 1ed39013..d8b615e3 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "type": "git", "url": "https://github.com/AzureAD/azure-activedirectory-library-for-js.git" }, - "version": "1.0.10", + "version": "1.0.11", "description": "Windows Azure Active Directory Client Library for js", "keywords": [ "implicit", diff --git a/tests/angularModuleSpec.js b/tests/angularModuleSpec.js index b7e3c11b..9204ac17 100644 --- a/tests/angularModuleSpec.js +++ b/tests/angularModuleSpec.js @@ -21,26 +21,27 @@ /* global describe */ 'use strict'; - + describe('TaskCtl', function () { - var scope, $httpBackend, adalServiceProvider, rootScope, controller, q, window; + var scope, $httpBackend, adalServiceProvider, rootScope, controller, q, window, route, location; //mock Application to allow us to inject our own dependencies beforeEach(angular.mock.module('TestApplication')); - //mock the controller for the same reason and include $rootScope and $controller - beforeEach(angular.mock.inject(function (_adalAuthenticationService_, _$rootScope_, _$controller_, _$httpBackend_, _$q_, _$window_) { + //mock the controller for the same reason and include $scope and $controller + beforeEach(angular.mock.inject(function (_adalAuthenticationService_, _$rootScope_, _$controller_, _$httpBackend_, _$q_, _$window_, _$route_, _$location_) { adalServiceProvider = _adalAuthenticationService_; rootScope = _$rootScope_; controller = _$controller_; $httpBackend = _$httpBackend_; q = _$q_; window = _$window_; + route = _$route_; + location = _$location_; //create an empty scope scope = rootScope.$new(); - adalServiceProvider.userInfo = { userName: 'UserVerify', isAuthenticated: true }; - + adalServiceProvider.getCachedToken = function (resource) { console.log('Requesting token for resource:' + resource); if (resource === 'resource1') { @@ -52,7 +53,7 @@ describe('TaskCtl', function () { } if (resource === adalServiceProvider.config.loginResource) { - return 'Token456'; + return 'Token456'; } return ''; @@ -79,8 +80,8 @@ describe('TaskCtl', function () { })); it('assigns user', function () { - expect(scope.user.userName).toBe('UserVerify'); - expect(scope.user.isAuthenticated).toBe(true); + expect(scope.user.userName).toBe(''); + expect(scope.user.isAuthenticated).toBe(false); }); it('send tokens for webapi call in endpoints list', function () { @@ -115,11 +116,11 @@ describe('TaskCtl', function () { }); it('does not send tokens for webapi(https) call not in endpoints list', function () { - $httpBackend.expectGET('https://test.com/', function (headers) { - return headers.hasOwnProperty('Authorization') === false; - }).respond(200); - scope.taskCall2(); - $httpBackend.flush(); + $httpBackend.expectGET('https://test.com/', function (headers) { + return headers.hasOwnProperty('Authorization') === false; + }).respond(200); + scope.taskCall2(); + $httpBackend.flush(); }); it('does not send tokens for webapi(http) call not in endpoint list', function () { @@ -127,10 +128,10 @@ describe('TaskCtl', function () { return headers.hasOwnProperty('Authorization') === false; }).respond(200); scope.taskCall6(); - $httpBackend.flush(); + $httpBackend.flush(); }); - it ('send tokens for app backend call not in endpoints list', function () { + it('send tokens for app backend call not in endpoints list', function () { $httpBackend.expectGET('/someapi/item', function (headers) { return headers.Authorization === 'Bearer Token456' }).respond(200); @@ -199,29 +200,29 @@ describe('TaskCtl', function () { return headers.Authorization === 'Bearer Token456' }).respond(200); - rootScope.$on('adal:errorResponse', function (event, message) { + scope.$on('adal:errorResponse', function (event, message) { expect(event.name).toBe('adal:errorResponse'); - expect(message).toBe('login in progress, cancelling the request'); + expect(message.data).toBe('login in progress, cancelling the request for https://myapp.com/someapi/item'); }); scope.taskCall5(); - rootScope.$apply(); - expect(rootScope.$broadcast).toHaveBeenCalledWith('adal:errorResponse', 'login in progress, cancelling the request'); + scope.$apply(); + expect(rootScope.$broadcast).toHaveBeenCalled(); }); it('tests stateMismatch broadcast when state does not match', function () { window.parent.AuthenticationContext = function () { return { callback: function () { }, - _renewStates: { } + _renewStates: {} }; }; window.location.hash = 'id_token=sample&state=4343'; spyOn(rootScope, '$broadcast').andCallThrough(); - rootScope.$on('adal:stateMismatch', function (event, message) { + scope.$on('adal:stateMismatch', function (event, message) { expect(event.name).toBe('adal:stateMismatch'); expect(message).toBe('Invalid_state. state: 4343'); }); - rootScope.$apply(); + scope.$apply(); expect(rootScope.$broadcast).toHaveBeenCalled(); }); @@ -229,7 +230,7 @@ describe('TaskCtl', function () { window.parent.AuthenticationContext = function () { return { callback: function () { }, - _renewStates: [ '4343' ] + _renewStates: ['4343'] }; }; window.parent.callBackMappedToRenewStates = {}; @@ -237,6 +238,162 @@ describe('TaskCtl', function () { expect(error).toBe('renewfailed'); }; window.location.hash = 'error=sample&error_description=renewfailed&state=4343'; - rootScope.$apply(); + scope.$apply(); + }); + + it('tests callback is called when response contains access token', function () { + window.parent.AuthenticationContext = function () { + return { + callback: function () { }, + _renewStates: ['4343'] + }; + }; + window.parent.callBackMappedToRenewStates = {}; + window.parent.callBackMappedToRenewStates['4343'] = function (error, token) { + expect(error).toBe(''); + expect(token).toBe('newAccessToken123'); + }; + window.location.hash = 'access_token=newAccessToken123&state=4343'; + scope.$apply(); + }); + + + it('tests callback is called when response contains id token', function () { + window.parent.AuthenticationContext = function () { + return { + callback: function () { }, + _renewStates: ['4343'] + }; + }; + window.parent.callBackMappedToRenewStates = {}; + window.parent.callBackMappedToRenewStates['4343'] = function (error, token) { + expect(error).toBe('Invalid id_token. id_token: newIdToken123'); + expect(token).toBe('newIdToken123'); + }; + window.location.hash = 'id_token=newIdToken123&state=4343'; + scope.$apply(); + }); + + + it('tests login failure after users logs in', function () { + window.parent.AuthenticationContext = function () { + return { + callback: 'callback', + _renewStates: ['1234'] + }; + }; + window.parent.callBackMappedToRenewStates = {}; + window.parent.callBackMappedToRenewStates['1234'] = 'callback'; + var mockInvalidClientIdToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJjbGllbnQxMjMiLCJuYW1lIjoiSm9obiBEb2UiLCJ1cG4iOiJqb2huQGVtYWlsLmNvbSJ9.zNX4vfLzlbFeKHZ9BMN3sYLtEEE-0o1RoL4NUhXz-l8'; + window.location.hash = 'id_token=' + mockInvalidClientIdToken + '&state=1234'; + spyOn(rootScope, '$broadcast').andCallThrough(); + scope.$on('adal:loginFailure', function (event, message) { + expect(event.name).toBe('adal:loginFailure'); + expect(message).toBe('Invalid id_token. id_token: ' + mockInvalidClientIdToken); + }); + scope.$apply(); + expect(rootScope.$broadcast).toHaveBeenCalled(); + + }); + + it('tests login success after users logs in', function () { + window.parent.AuthenticationContext = function () { + return { + callback: 'callback', + _renewStates: ['1234'] + }; + }; + window.parent.callBackMappedToRenewStates = {}; + window.parent.callBackMappedToRenewStates['1234'] = 'callback'; + var mockIdToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJjbGllbnRpZDEyMyIsIm5hbWUiOiJKb2huIERvZSIsInVwbiI6ImpvaG5AZW1haWwuY29tIiwibm9uY2UiOm51bGx9.DLCO6yIWhnNBYfHH8qFPswcH4M2Alpjn6AZy7K6HENY'; + window.location.hash = 'id_token=' + mockIdToken + '&state=1234'; + spyOn(rootScope, '$broadcast').andCallThrough(); + scope.$on('adal:loginSuccess', function (event, message) { + expect(event.name).toBe('adal:loginSuccess'); + expect(adalServiceProvider.userInfo.userName).toBe('john@email.com'); + expect(adalServiceProvider.userInfo.profile.upn).toBe('john@email.com'); + expect(adalServiceProvider.userInfo.profile.aud).toBe('clientid123'); + }); + scope.$apply(); + expect(rootScope.$broadcast).toHaveBeenCalled(); + }); + + it('tests auto id token renew when id token expires', function () { + window.parent.AuthenticationContext = function () { + return { + callback: 'callback', + _renewStates: ['1234'] + }; + }; + window.parent.callBackMappedToRenewStates = {}; + window.parent.callBackMappedToRenewStates['1234'] = 'callback'; + var loginResourceOldValue = adalServiceProvider.config.loginResource; + adalServiceProvider.config.loginResource = null; + window.location.hash = 'hash'; + spyOn(rootScope, '$broadcast').andCallThrough(); + scope.$on('adal:loginFailure', function (event, message) { + expect(event.name).toBe('adal:loginFailure'); + expect(message).toBe('auto renew failure'); + }); + scope.$apply(); + adalServiceProvider.config.loginResource = loginResourceOldValue; + expect(rootScope.$broadcast).toHaveBeenCalled(); + }); + + it('tests login handler', function () { + spyOn(rootScope, '$broadcast').andCallThrough(); + + adalServiceProvider.config.localLoginUrl = 'localLoginUrl'; + adalServiceProvider.login(); + scope.$on('$locationChangeStart', function (event, newUrl, oldUrl) { + expect(newUrl).toContain('localLoginUrl'); + console.log('location event called'); + event.preventDefault(); + }) + expect(adalServiceProvider.loginInProgress()).toBe(false); + scope.$apply(); + expect(rootScope.$broadcast).toHaveBeenCalled(); + + adalServiceProvider.config.localLoginUrl = null + adalServiceProvider.login(); + scope.$on('adal:loginRedirect', function (event, message) { + expect(event.name).toBe('adal:loginRedirect'); + }); + expect(adalServiceProvider.loginInProgress()).toBe(true); + expect(rootScope.$broadcast).toHaveBeenCalled(); + }); + + it('tests route change handler', function () { + var todoRoute = route.routes['/todoList']; + var homeRoute = route.routes['/home']; + var aboutRoute = route.routes['/about']; + + location.url('/todoList'); + scope.$apply(); + expect(route.current.controller).toBe(todoRoute.controller); + expect(route.current.template).toBe(todoRoute.template); + + location.url('/home'); + scope.$apply(); + expect(route.current.controller).toBe(homeRoute.controller); + expect(route.current.template).toBe(homeRoute.template); + + $httpBackend.expectGET('about.html').respond(200); + location.url('/about'); + scope.$apply(); + expect(route.current.controller).toBe(aboutRoute.controller); + expect(route.current.templateUrl).toBe(aboutRoute.templateUrl); + expect(adalServiceProvider.config.anonymousEndpoints).toContain(aboutRoute.templateUrl); + $httpBackend.flush(); + }); + + it('checks if Logging is defined', function () { + Logging.level = 2; + Logging.log = function (message) { + window.logMessage = message; + } + adalServiceProvider.info("test message"); + expect(window.logMessage).toContain("test message"); + expect(Logging.level).toEqual(2); }); }); diff --git a/tests/testApp.js b/tests/testApp.js index ca6ada6c..0917072c 100644 --- a/tests/testApp.js +++ b/tests/testApp.js @@ -23,9 +23,17 @@ var app = angular.module('TestApplication', ['ngResource', 'ngRoute', 'AdalAngul app.config(['$httpProvider', '$routeProvider', 'adalAuthenticationServiceProvider', function ($httpProvider, $routeProvider, adalAuthenticationServiceProvider) { $routeProvider. + when('/home', { + controller: 'homeController', + template: '
home
' + }). + when('/about', { + controller: 'aboutController', + templateUrl: 'about.html' + }). when('/todoList', { controller: 'todoListController', - templateUrl: '/App/Views/todoList.html', + template: '
todoList
', requireADLogin: true }). otherwise({ redirectTo: '/home' }); diff --git a/tests/unit/spec/AdalSpec.js b/tests/unit/spec/AdalSpec.js index 92029eb0..fefdcd89 100644 --- a/tests/unit/spec/AdalSpec.js +++ b/tests/unit/spec/AdalSpec.js @@ -21,10 +21,12 @@ /* global describe */ var atobHelper = require('atob'); var confighash = { hash: '#' }; +global.window = {}; var AdalModule = require('../../../lib/adal.js'); describe('Adal', function () { var adal; + global.Logging = global.window.Logging; var window = { location: { hash: '#hash', @@ -44,17 +46,20 @@ describe('Adal', function () { return 1000; } }; - var frameMock = { - src: 'start' - }; + + var mockFrames = {}; var documentMock = { - getElementById: function () { - return frameMock; + getElementById: function (frameId) { + if (!mockFrames[frameId]) { + mockFrames[frameId] = { src: 'start' }; + } + return mockFrames[frameId]; } }; + var angularMock = {}; - var conf = { loginResource: 'default resource', tenant: 'testtenant', clientId: 'e9a5a8b6-8af7-4719-9821-0deef255f68e' }; + var conf = { loginResource: 'defaultResource', tenant: 'testtenant', clientId: 'e9a5a8b6-8af7-4719-9821-0deef255f68e' }; var testPage = 'this is a song'; var STORAGE_PREFIX = 'adal'; var STORAGE_ACCESS_TOKEN_KEY = STORAGE_PREFIX + '.access.token.key'; @@ -64,6 +69,10 @@ describe('Adal', function () { var SECONDS_TO_EXPIRE = 3600; var DEFAULT_INSTANCE = "https://login.microsoftonline.com/"; var IDTOKEN_MOCK = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjVUa0d0S1JrZ2FpZXpFWTJFc0xDMmdPTGpBNCJ9.eyJhdWQiOiJlOWE1YThiNi04YWY3LTQ3MTktOTgyMS0wZGVlZjI1NWY2OGUiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLXBwZS5uZXQvNTJkNGIwNzItOTQ3MC00OWZiLTg3MjEtYmMzYTFjOTkxMmExLyIsImlhdCI6MTQxMTk1OTAwMCwibmJmIjoxNDExOTU5MDAwLCJleHAiOjE0MTE5NjI5MDAsInZlciI6IjEuMCIsInRpZCI6IjUyZDRiMDcyLTk0NzAtNDlmYi04NzIxLWJjM2ExYzk5MTJhMSIsImFtciI6WyJwd2QiXSwib2lkIjoiZmEzYzVmYTctN2Q5OC00Zjk3LWJmYzQtZGJkM2E0YTAyNDMxIiwidXBuIjoidXNlckBvYXV0aGltcGxpY2l0LmNjc2N0cC5uZXQiLCJ1bmlxdWVfbmFtZSI6InVzZXJAb2F1dGhpbXBsaWNpdC5jY3NjdHAubmV0Iiwic3ViIjoiWTdUbXhFY09IUzI0NGFHa3RjbWpicnNrdk5tU1I4WHo5XzZmbVc2NXloZyIsImZhbWlseV9uYW1lIjoiYSIsImdpdmVuX25hbWUiOiJ1c2VyIiwibm9uY2UiOiI4MGZmYTkwYS1jYjc0LTRkMGYtYTRhYy1hZTFmOTNlMzJmZTAiLCJwd2RfZXhwIjoiNTc3OTkxMCIsInB3ZF91cmwiOiJodHRwczovL3BvcnRhbC5taWNyb3NvZnRvbmxpbmUuY29tL0NoYW5nZVBhc3N3b3JkLmFzcHgifQ.WHsl8TH1rQ3dQbRkV0TS6GBVAxzNOpG3nGG6mpEBCwAOCbyW6qRsSoo4qq8I5IGyerDf2cvcS-zzatHEROpRC9dcpwkRm6ta5dFZuouFyZ_QiYVKSMwfzEC_FI-6p7eT8gY6FbV51bp-Ah_WKJqEmaXv-lqjIpgsMGeWDgZRlB9cPODXosBq-PEk0q27Be-_A-KefQacJuWTX2eEhECLyuAu-ETVJb7s19jQrs_LJXz_ISib4DdTKPa7XTBDJlVGdCI18ctB67XwGmGi8MevkeKqFI8dkykTxeJ0MXMmEQbE6Fw-gxmP7uJYbZ61Jqwsw24zMDMeXatk2VWMBPCuhA'; + var STATE = '585bd348-4d52-4689-b9c7-d8480564f368'; + var SESSION_STATE = '451c6916-27cf-4eae-81cd-accf96126398'; + var VALID_URLFRAGMENT = 'id_token=' + IDTOKEN_MOCK + '' + '&state=' + STATE + '&session_state=' + SESSION_STATE; + var INVALID_URLFRAGMENT = 'id_token' + IDTOKEN_MOCK + '' + '&state=' + STATE + '&session_state=' + SESSION_STATE; var storageFake = function () { var store = {}; return { @@ -72,7 +81,7 @@ describe('Adal', function () { }, setItem: function (key, value) { if (typeof value != 'undefined') { - store[key] = value + ''; + store[key] = value; } }, clear: function () { @@ -99,6 +108,7 @@ describe('Adal', function () { window.sessionStorage = storageFake; // Init adal + global.window = window; global.localStorage = storageFake; global.sessionStorage = storageFake; @@ -110,8 +120,9 @@ describe('Adal', function () { adal._user = null; adal._renewStates = []; adal._activeRenewals = {}; + adal.CONSTANTS.LOADFRAME_TIMEOUT = 800; }); - + it('gets specific resource for defined endpoint mapping', function () { adal.config.endpoints = { 'a': 'resource for a' }; expect(adal.getResourceForEndpoint('a')).toBe('resource for a'); @@ -120,8 +131,8 @@ describe('Adal', function () { it('gets default resource for empty endpoint mapping', function () { adal.config.endpoints = null; - expect(adal.getResourceForEndpoint('a')).toBe('default resource'); - expect(adal.getResourceForEndpoint('b')).toBe('default resource'); + expect(adal.getResourceForEndpoint('a')).toBe('defaultResource'); + expect(adal.getResourceForEndpoint('b')).toBe('defaultResource'); }); it('gets null resource for annonymous endpoints', function () { @@ -129,7 +140,7 @@ describe('Adal', function () { expect(adal.getResourceForEndpoint('app/views')).toBe(null); expect(adal.getResourceForEndpoint('app/views/abc')).toBe(null); expect(adal.getResourceForEndpoint('default/app/views/abc')).toBe(null); - expect(adal.getResourceForEndpoint('app/home')).toBe('default resource'); + expect(adal.getResourceForEndpoint('app/home')).toBe('defaultResource'); }); it('says token expired', function () { @@ -154,7 +165,6 @@ describe('Adal', function () { adal.config.clientId = 'client'; adal.config.redirectUri = 'contoso_site'; spyOn(adal, 'promptUser'); - console.log('instance:' + adal.instance); adal.login(); expect(adal.promptUser).toHaveBeenCalledWith(DEFAULT_INSTANCE + conf.tenant + '/oauth2/authorize?response_type=id_token&client_id=client&redirect_uri=contoso_site&state=33333333-3333-4333-b333-333333333333' + '&client-request-id=33333333-3333-4333-b333-333333333333' + adal._addLibMetadata() + '&nonce=33333333-3333-4333-b333-333333333333'); @@ -182,10 +192,10 @@ describe('Adal', function () { adal.config.displayCall = displayCallback; spyOn(adal.config, 'displayCall'); adal.login(); - expect(adal.config.displayCall).toHaveBeenCalledWith(DEFAULT_INSTANCE + conf.tenant + '/oauth2/authorize?response_type=id_token&client_id=client&redirect_uri=contoso_site&state=33333333-3333-4333-b333-333333333333' + expect(adal.config.displayCall).toHaveBeenCalledWith(DEFAULT_INSTANCE + conf.tenant + '/oauth2/authorize?response_type=id_token&client_id=client&redirect_uri=contoso_site&state=33333333-3333-4333-b333-333333333333' + '&client-request-id=33333333-3333-4333-b333-333333333333' + adal._addLibMetadata() - + '&nonce=33333333-3333-4333-b333-333333333333' + + '&nonce=33333333-3333-4333-b333-333333333333' ); expect(adal.config.state).toBe('33333333-3333-4333-b333-333333333333'); }); @@ -225,23 +235,24 @@ describe('Adal', function () { token = valToken; }; adal._renewStates = []; - adal._user = { userName: 'test@testuser.com' }; + adal._user = { profile: { 'upn': 'test@testuser.com' }, userName: 'test@domain.com'}; adal.acquireToken(RESOURCE1, callback); expect(adal.callback).toBe(callback); expect(storageFake.getItem(adal.CONSTANTS.STORAGE.LOGIN_REQUEST)).toBe(''); expect(adal._renewStates.length).toBe(1); // Wait for initial timeout load console.log('Waiting for initial timeout'); - waits(2000); + waitsFor(function () { + return mockFrames['adalRenewFrame' + RESOURCE1].src !== 'about:blank'; + }, 'iframe src not updated', 2000); runs(function () { - console.log('Frame src:' + frameMock.src); - expect(frameMock.src).toBe(DEFAULT_INSTANCE + conf.tenant + '/oauth2/authorize?response_type=token&client_id=client&resource=' + RESOURCE1 + '&redirect_uri=contoso_site&state=33333333-3333-4333-b333-333333333333%7Ctoken.resource1' - + '&client-request-id=33333333-3333-4333-b333-333333333333' + adal._addLibMetadata() + '&prompt=none&login_hint=test%40testuser.com&domain_hint=testuser.com&nonce=33333333-3333-4333-b333-333333333333'); + expect(mockFrames['adalRenewFrame' + RESOURCE1].src).toBe(DEFAULT_INSTANCE + conf.tenant + '/oauth2/authorize?response_type=token&client_id=client&resource=' + RESOURCE1 + '&redirect_uri=contoso_site&state=33333333-3333-4333-b333-333333333333%7Ctoken.resource1' + + '&client-request-id=33333333-3333-4333-b333-333333333333' + adal._addLibMetadata() + '&prompt=none&login_hint=test%40testuser.com&domain_hint=testuser.com'); }); - + }); - + //Necessary for integration with Angular when multiple http calls are queued. it('allows multiple callers to be notified when the token is renewed', function () { adal.config.redirectUri = 'contoso_site'; @@ -255,13 +266,13 @@ describe('Adal', function () { err = valErr; token = valToken; }; - var callback2 = function(valErr, valToken){ + var callback2 = function (valErr, valToken) { err2 = valErr; token2 = valToken; }; - + adal._renewStates = []; - adal._user = { userName: 'test@testuser.com' }; + adal._user = { profile: { 'upn': 'test@testuser.com' }, userName: 'test@domain.com' }; adal.acquireToken(RESOURCE1, callback); //Simulate second acquire i.e. second service call from Angular. adal.acquireToken(RESOURCE1, callback2); @@ -269,21 +280,22 @@ describe('Adal', function () { expect(adal._renewStates.length).toBe(1); // Wait for initial timeout load console.log('Waiting for initial timeout'); - waits(2000); - + waitsFor(function () { + return mockFrames['adalRenewFrame' + RESOURCE1].src !== 'about:blank'; + }, 'iframe src not updated', 2000); + runs(function () { - console.log('Frame src:' + frameMock.src); - expect(frameMock.src).toBe(DEFAULT_INSTANCE + conf.tenant + '/oauth2/authorize?response_type=token&client_id=client&resource=' + RESOURCE1 + '&redirect_uri=contoso_site&state=33333333-3333-4333-b333-333333333333%7Ctoken.resource1' - + '&client-request-id=33333333-3333-4333-b333-333333333333' + adal._addLibMetadata() + '&prompt=none&login_hint=test%40testuser.com&domain_hint=testuser.com&nonce=33333333-3333-4333-b333-333333333333'); + expect(mockFrames['adalRenewFrame' + RESOURCE1].src).toBe(DEFAULT_INSTANCE + conf.tenant + '/oauth2/authorize?response_type=token&client_id=client&resource=' + RESOURCE1 + '&redirect_uri=contoso_site&state=33333333-3333-4333-b333-333333333333%7Ctoken.resource1' + + '&client-request-id=33333333-3333-4333-b333-333333333333' + adal._addLibMetadata() + '&prompt=none&login_hint=test%40testuser.com&domain_hint=testuser.com'); }); - + //Simulate callback from the frame. //adal.callback(null, '33333333-3333-4333-b333-333333333333'); window.callBackMappedToRenewStates[adal.config.state](null, '33333333-3333-4333-b333-333333333333'); //Both callbacks should have been provided with the token. expect(token).toBe('33333333-3333-4333-b333-333333333333', 'First callback should be called'); expect(token2).toBe('33333333-3333-4333-b333-333333333333', 'Second callback should be called'); - + }); it('check guid masking', function () { @@ -304,7 +316,7 @@ describe('Adal', function () { }; // 15->1111 after masked with & 0011 | 1000 1011 expect(adal._guid()).toBe('ffffffff-ffff-4fff-bfff-ffffffffffff'); - + mathMock.random = function () { return 0.9; }; @@ -335,7 +347,6 @@ describe('Adal', function () { storageFake.setItem(adal.CONSTANTS.STORAGE.EXPIRATION_KEY, 3); storageFake.setItem(adal.CONSTANTS.STORAGE.SESSION_STATE, 'session_state'); storageFake.setItem(adal.CONSTANTS.STORAGE.STATE_LOGIN, 'state login'); - storageFake.setItem(adal.CONSTANTS.STORAGE.START_PAGE, 'start page'); storageFake.setItem(adal.CONSTANTS.STORAGE.USERNAME, 'username'); storageFake.setItem(adal.CONSTANTS.STORAGE.ERROR, 'error'); storageFake.setItem(adal.CONSTANTS.STORAGE.ERROR_DESCRIPTION, 'error description'); @@ -492,7 +503,7 @@ describe('Adal', function () { requestType: adal.REQUEST_TYPE.RENEW_TOKEN }; adal.saveTokenFromHash(requestInfo); - expect(storageFake.getItem(adal.CONSTANTS.STORAGE.EXPIRATION_KEY + 'loginResource1')).toBe(mathMock.round(1) + 3589 + ''); + expect(storageFake.getItem(adal.CONSTANTS.STORAGE.EXPIRATION_KEY + 'loginResource1')).toBe(mathMock.round(1) + 3589); }); it('saves username after extracting idtoken', function () { @@ -505,7 +516,7 @@ describe('Adal', function () { stateMatch: true, stateResponse: '123', requestType: adal.REQUEST_TYPE.ID_TOKEN - }; + }; storageFake.setItem(adal.CONSTANTS.STORAGE.NONCE_IDTOKEN, '19e67b24-cd99-45b6-a588-840e3f8f2a70'); adal.config.clientId = conf.clientId; adal._user = null; @@ -513,7 +524,6 @@ describe('Adal', function () { var cachedUser = adal.getCachedUser(); expect(cachedUser.userName).toBe('user@oauthimplicit.ccsctp.net'); expect(cachedUser.profile.upn).toBe('user@oauthimplicit.ccsctp.net'); - console.log('test extract idtoken done'); }); it('does not save user for invalid nonce in idtoken', function () { @@ -568,27 +578,27 @@ describe('Adal', function () { expect(storageFake.getItem(adal.CONSTANTS.STORAGE.USERNAME)).toBeUndefined(); }); - it ('test decode with no padding', function () { + it('test decode with no padding', function () { expect(adal._decode('ZGVjb2RlIHRlc3Rz')).toBe('decode tests'); }); - it ('test decode with one = padding', function () { - expect(adal._decode('ZWNvZGUgdGVzdHM=')).toBe('ecode tests'); + it('test decode with one = padding', function () { + expect(adal._decode('ZWNvZGUgdGVzdHM=')).toBe('ecode tests'); }); - it ('test decode with two == padding', function () { - expect(adal._decode('Y29kZSB0ZXN0cw==')).toBe('code tests'); + it('test decode with two == padding', function () { + expect(adal._decode('Y29kZSB0ZXN0cw==')).toBe('code tests'); }) - it ('test decode throw error', function () { - try{ - adal._decode('YW55I'); - } catch(e) { + it('test decode throw error', function () { + try { + adal._decode('YW55I'); + } catch (e) { expect(e.message).toBe('The token to be decoded is not correctly encoded.'); } }); - it ('test get resource for endpoint from app backend', function () { + it('test get resource for endpoint from app backend', function () { adal.config.redirectUri = 'https://host.com/page'; expect(adal.getResourceForEndpoint('https://host.com')).toBe(adal.config.loginResource); expect(adal.getResourceForEndpoint('https://host.com/a/b')).toBe(adal.config.loginResource); @@ -597,7 +607,7 @@ describe('Adal', function () { expect(adal.getResourceForEndpoint('/api/todo')).toBe(adal.config.loginResource); }); - it ('test host extraction', function () { + it('test host extraction', function () { expect(adal._getHostFromUri('https://a.com/b/c')).toBe('a.com'); expect(adal._getHostFromUri('http://a.com')).toBe('a.com'); expect(adal._getHostFromUri('a.com/b/c')).toBe('a.com'); @@ -624,7 +634,224 @@ describe('Adal', function () { expect(storageFake.getItem(adal.CONSTANTS.STORAGE.ERROR_DESCRIPTION)).toBe('Invalid_state. state: ' + requestInfo.stateResponse); }); + it('checks if Logging is defined on window', function () { + Logging.level = 2; + Logging.log = function (message) { + window.logMessage = message; + } + adal.promptUser(); + expect(window.logMessage).toContain("Navigate url is empty"); + expect(Logging.level).toEqual(2); + }); + + it('tests the load frame timeout method', function () { + adal._loadFrameTimeout('urlnavigation', 'frameName', RESOURCE1); + expect(storageFake.getItem(adal.CONSTANTS.STORAGE.RENEW_STATUS + RESOURCE1)).toBe(adal.CONSTANTS.TOKEN_RENEW_STATUS_IN_PROGRESS); + + // timeout interval passed + waitsFor(function () { + return storageFake.getItem(adal.CONSTANTS.STORAGE.RENEW_STATUS + RESOURCE1) === adal.CONSTANTS.TOKEN_RENEW_STATUS_CANCELED; + }, 'token renew status not updated', 1000); + + runs(function () { + expect(storageFake.getItem(adal.CONSTANTS.STORAGE.RENEW_STATUS + RESOURCE1)).toBe(adal.CONSTANTS.TOKEN_RENEW_STATUS_CANCELED); + + adal._loadFrameTimeout('urlnavigation', 'frameName', RESOURCE1); + expect(storageFake.getItem(adal.CONSTANTS.STORAGE.RENEW_STATUS + RESOURCE1)).toBe(adal.CONSTANTS.TOKEN_RENEW_STATUS_IN_PROGRESS); + var requestInfo = { + valid: true, + parameters: { 'access_token': 'token123', 'state': '123', 'expires_in': '23' }, + stateMatch: true, + stateResponse: '64532|' + RESOURCE1, + requestType: adal.REQUEST_TYPE.RENEW_TOKEN + }; + adal.saveTokenFromHash(requestInfo); + expect(storageFake.getItem(adal.CONSTANTS.STORAGE.RENEW_STATUS + RESOURCE1)).toBe(adal.CONSTANTS.TOKEN_RENEW_STATUS_COMPLETED); + }); + }); + + it('tests that callbacks are called when renewal token request was canceled', function () { + adal.config.expireOffsetSeconds = SECONDS_TO_EXPIRE + 100; + var err = ''; + var token = ''; + var callback = function (valErr, valToken) { + err = valErr; + token = valToken; + }; + adal._renewStates = []; + adal._user = { userName: 'test@testuser.com' }; + adal.acquireToken(RESOURCE1, callback); + waitsFor(function () { + return storageFake.getItem(adal.CONSTANTS.STORAGE.RENEW_STATUS + RESOURCE1) === adal.CONSTANTS.TOKEN_RENEW_STATUS_CANCELED; + }, 'token renew status not updated', 1000); + runs(function () { + expect(storageFake.getItem(adal.CONSTANTS.STORAGE.RENEW_STATUS + RESOURCE1)).toBe(adal.CONSTANTS.TOKEN_RENEW_STATUS_CANCELED); + expect(err).toBe('Token renewal operation failed due to timeout'); + expect(token).toBe(null); + + }); + }); + + it('attempts to renewidToken if token expired and renew is allowed', function () { + adal.config.redirectUri = 'contoso_site'; + adal.config.clientId = 'client'; + adal.config.expireOffsetSeconds = SECONDS_TO_EXPIRE + 100; + adal.config.tenant = 'testtenant'; + var err = ''; + var token = ''; + var callback = function (valErr, valToken) { + err = valErr; + token = valToken; + }; + adal._renewStates = []; + adal._user = { profile: { 'upn': 'test@testuser.com' }, userName: 'test@domain.com' }; + adal.acquireToken(adal.config.clientId, callback); + expect(storageFake.getItem(adal.CONSTANTS.STORAGE.NONCE_IDTOKEN)).toBe('33333333-3333-4333-b333-333333333333'); + expect(adal.config.state).toBe('33333333-3333-4333-b333-333333333333' + '|' + 'client'); + expect(adal._renewStates.length).toBe(1); + expect(storageFake.getItem(adal.CONSTANTS.STORAGE.LOGIN_REQUEST)).toBe(''); + // Wait for initial timeout load + console.log('Waiting for initial timeout'); + waitsFor(function () { + return mockFrames['adalIdTokenFrame'].src !== 'about:blank'; + }, 'iframe src not updated', 2000); + + runs(function () { + expect(mockFrames['adalIdTokenFrame'].src).toBe(DEFAULT_INSTANCE + conf.tenant + '/oauth2/authorize?response_type=id_token&client_id=' + adal.config.clientId + '&redirect_uri=contoso_site&state=33333333-3333-4333-b333-333333333333%7Cclient' + + '&client-request-id=33333333-3333-4333-b333-333333333333' + adal._addLibMetadata() + '&prompt=none&login_hint=test%40testuser.com&domain_hint=testuser.com' + '&nonce=33333333-3333-4333-b333-333333333333'); + }); + }); + + it('tests handleWindowCallback function for RENEW_TOKEN', function () { + window.location.hash = '#/id_token=' + IDTOKEN_MOCK; + adal.getRequestInfo = function (hash) { + return { + valid: true, + parameters: { 'error_description': 'error description', 'error': 'invalid', 'id_token': IDTOKEN_MOCK, 'session_state': '61ae5247-eaf8-4496-a667-32b0acbad7a0', 'state': '19537a2a-e9e7-489d-ae7d-3eefab9e4137' }, + stateMatch: true, + stateResponse: '19537a2a-e9e7-489d-ae7d-3eefab9e4137', + requestType: adal.REQUEST_TYPE.RENEW_TOKEN + }; + }; + var err = ''; + var token = ''; + var callback = function (valErr, valToken) { + err = valErr; + token = valToken; + }; + window.parent = {}; + window.parent.callBackMappedToRenewStates = {}; + window.parent.callBackMappedToRenewStates[adal.getRequestInfo().stateResponse] = callback; + adal.handleWindowCallback(); + expect(err).toBe('error description'); + expect(token).toBe(IDTOKEN_MOCK); + + }); + + it('tests handleWindowCallback function for LOGIN_REQUEST', function () { + window.location = {}; + window.location.hash = '#/id_token=' + IDTOKEN_MOCK; + adal.getRequestInfo = function () { + return { + valid: true, + parameters: { 'error_description': 'error description', 'error': 'invalid', 'id_token': IDTOKEN_MOCK, 'session_state': '61ae5247-eaf8-4496-a667-32b0acbad7a0', 'state': '19537a2a-e9e7-489d-ae7d-3eefab9e4137' }, + stateMatch: true, + stateResponse: '19537a2a-e9e7-489d-ae7d-3eefab9e4137', + requestType: adal.REQUEST_TYPE.LOGIN_REQUEST + }; + }; + storageFake.setItem(adal.CONSTANTS.STORAGE.LOGIN_REQUEST, "www.test.com"); + window.oauth2Callback = {}; + adal.handleWindowCallback(); + expect(window.location).toBe('www.test.com'); + + }); + + it('use the same correlationId for each request sent to AAD if set by user', function () { + adal.config.correlationId = '33333333-3333-4333-b333-333333333333'; + adal.config.redirectUri = 'contoso_site'; + adal.config.clientId = 'client'; + adal.config.expireOffsetSeconds = SECONDS_TO_EXPIRE + 100; + var callback = function () { + }; + adal._renewStates = []; + adal._user = { profile: { 'upn': 'test@testuser.com' }, userName: 'test@domain.com' }; + spyOn(adal, '_loadFrameTimeout'); + adal.acquireToken(RESOURCE1, callback); + expect(adal._loadFrameTimeout).toHaveBeenCalledWith(DEFAULT_INSTANCE + conf.tenant + '/oauth2/authorize?response_type=token&client_id=client&resource=' + RESOURCE1 + '&redirect_uri=contoso_site&state=33333333-3333-4333-b333-333333333333%7Ctoken.resource1' + + '&client-request-id=33333333-3333-4333-b333-333333333333' + adal._addLibMetadata() + '&prompt=none&login_hint=test%40testuser.com&domain_hint=testuser.com', 'adalRenewFrametoken.resource1', 'token.resource1'); + + adal._activeRenewals = {}; + adal._user = { profile: { 'sub': 'test@testuser.com' }, userName: 'test@domain.com' }; + adal.acquireToken(RESOURCE1, callback); + expect(adal._loadFrameTimeout).toHaveBeenCalledWith(DEFAULT_INSTANCE + conf.tenant + '/oauth2/authorize?response_type=token&client_id=client&resource=' + RESOURCE1 + '&redirect_uri=contoso_site&state=33333333-3333-4333-b333-333333333333%7Ctoken.resource1' + + '&client-request-id=33333333-3333-4333-b333-333333333333' + adal._addLibMetadata() + '&prompt=none', 'adalRenewFrametoken.resource1', 'token.resource1'); + }); + + it('generates new correlationId for each request sent to AAD if not set by user', function () { + adal.config.correlationId = null; + adal.config.redirectUri = 'contoso_site'; + adal.config.clientId = 'client'; + adal.config.expireOffsetSeconds = SECONDS_TO_EXPIRE + 100; + var callback = function () { + }; + adal._renewStates = []; + adal._user = { profile: { 'upn': 'test@testuser.com' }, userName: 'test@domain.com' }; + mathMock.random = function () { + return 0.1; + }; + spyOn(adal, '_loadFrameTimeout'); + adal.acquireToken(RESOURCE1, callback); + expect(adal._loadFrameTimeout).toHaveBeenCalledWith(DEFAULT_INSTANCE + conf.tenant + '/oauth2/authorize?response_type=token&client_id=client&resource=' + RESOURCE1 + '&redirect_uri=contoso_site&state=11111111-1111-4111-9111-111111111111%7Ctoken.resource1' + + '&client-request-id=11111111-1111-4111-9111-111111111111' + adal._addLibMetadata() + '&prompt=none&login_hint=test%40testuser.com&domain_hint=testuser.com', 'adalRenewFrametoken.resource1', 'token.resource1'); + + mathMock.random = function () { + return 0.3; + }; + adal._activeRenewals = {}; + adal._user = { profile: { 'sub': 'test@testuser.com' }, userName: 'test@domain.com' }; + adal.acquireToken(RESOURCE1, callback); + expect(adal._loadFrameTimeout).toHaveBeenCalledWith(DEFAULT_INSTANCE + conf.tenant + '/oauth2/authorize?response_type=token&client_id=client&resource=' + RESOURCE1 + '&redirect_uri=contoso_site&state=44444444-4444-4444-8444-444444444444%7Ctoken.resource1' + + '&client-request-id=44444444-4444-4444-8444-444444444444' + adal._addLibMetadata() + '&prompt=none', 'adalRenewFrametoken.resource1', 'token.resource1'); + + }); + + it('checks the deserialize method for extracting idToken', function () { + var obj = adal._deserialize(VALID_URLFRAGMENT); + expect(obj.id_token).toBe(IDTOKEN_MOCK); + expect(obj.state).toBe(STATE); + expect(obj.session_state).toBe(SESSION_STATE); + + obj = adal._deserialize(INVALID_URLFRAGMENT); + expect(obj.id_token).toBeUndefined; + expect(obj.state).toBe(STATE); + expect(obj.session_state).toBe(SESSION_STATE); + expect(obj['id_token' + IDTOKEN_MOCK]).toBeUndefined; + var deserialize = adal._deserialize;//save initial state of function + + adal._deserialize = function (query) { + var match, + pl = /\+/g, // Regex for replacing addition symbol with a space + search = /([^&=]+)=?([^&]*)/g, + decode = function (s) { + return decodeURIComponent(s.replace(pl, ' ')); + }, + obj = {}; + match = search.exec(query); + while (match) { + obj[decode(match[1])] = decode(match[2]); + match = search.exec(query); + } + + return obj; + } + obj = adal._deserialize(INVALID_URLFRAGMENT); + expect(obj['id_token' + IDTOKEN_MOCK]).toBe('');//This additional property is parsed because of ? operator in regex + expect(obj.id_token).toBeUndefined; + expect(obj.state).toBe(STATE); + expect(obj.session_state).toBe(SESSION_STATE); + adal._deserialize = deserialize;//reassign state to original function + }); // TODO angular intercepptor - - // TODO angular authenticaitonService -}); \ No newline at end of file + // TODO angular authenticationService +});