diff --git a/README.md b/README.md index 40c4ad91..a4425eab 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Active Directory Authentication Library for JavaScript (ADAL JS) helps you to us This library is optimized for working together with AngularJS. ## Versions -Current version - 1.0.12 +Current version - 1.0.13 Minimum recommended version - 1.0.11 You can find the changes for each version in the [change log](https://github.com/AzureAD/azure-activedirectory-library-for-js/blob/master/changelog.txt). @@ -26,7 +26,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.12**. +This is a GA released version. The current version is **1.0.13**. You have multiple ways of getting ADAL JS: @@ -37,10 +37,10 @@ Via NPM: Via CDN: - - + + -CDN will be updated to latest version 1.0.12. +CDN will be updated to latest version 1.0.13. Via Bower: diff --git a/bower.json b/bower.json index 3263b857..874f4bd7 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "adal-angular", - "version": "1.0.12", + "version": "1.0.13", "homepage": "https://github.com/AzureAD/azure-activedirectory-library-for-js", "authors": [ "MSOpentech" diff --git a/changelog.txt b/changelog.txt index 454fc9f4..95392ffd 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,10 @@ +Version 1.0.13 +========================= +* Pass 'error' parameter to the callback besides 'error_description': #424 +* Adding API documentation of adal.js +* Adding 'acquireTokenSuccess' and 'acquireTokenFailure' events in adal-angular.js +* Other bug fixes and updates + Version 1.0.12 ========================== * Adding support for Login using a pop-up instead of a full redirect. Please see this: https://github.com/AzureAD/azure-activedirectory-library-for-js/issues/60 diff --git a/dist/adal-angular.min.js b/dist/adal-angular.min.js index 0886f8a8..66cf4c52 100644 --- a/dist/adal-angular.min.js +++ b/dist/adal-angular.min.js @@ -1,2 +1,2 @@ -/*! adal-angular v1.0.12 2016-08-31 */ -!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,b.isAngular=!0,d&&d.interceptors&&d.interceptors.push("ProtectedResourceInterceptor"),a=new AuthenticationContext(b),c(a.config.loginResource)},this.$get=["$rootScope","$window","$q","$location","$timeout","$injector",function(d,e,f,g,h,i){function j(a,b){return b.requireADLogin?a.requireADLogin!==!1:!!a.requireADLogin}function k(b){if(a.config&&a.config.anonymousEndpoints)for(var c=0;c-1)return!0;return!1}function l(a){var b=null,c=[];if(a.hasOwnProperty("parent"))for(b=a;b;)c.unshift(b),b=i.get("$state").get(b.parent);else for(var d=a.name.split("."),e=0,f=d[0];e-1&&g.url(n.substring(n.indexOf("#")+1)),e.location=n)}}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)},n=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())},o=function(c,d){if(d&&d.$$route)if(j(d.$$route,a.config))b.isAuthenticated||a._renewActive||a.loginInProgress()||(a.info("Route change event for:"+g.$$url),n());else{var e;e="function"==typeof d.$$route.templateUrl?d.$$route.templateUrl(d.params):d.$$route.templateUrl,e&&!k(e)&&a.config.anonymousEndpoints.push(e)}},p=function(c,d,e,f,h){if(d)for(var i=l(d),m=null,o=0;o0;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,b.isAngular=!0,d&&d.interceptors&&d.interceptors.push("ProtectedResourceInterceptor"),a=new AuthenticationContext(b),c(a.config.loginResource)},this.$get=["$rootScope","$window","$q","$location","$timeout","$injector",function(d,e,f,g,h,i){function j(a,b){return b.requireADLogin?a.requireADLogin!==!1:!!a.requireADLogin}function k(b){if(a.config&&a.config.anonymousEndpoints)for(var c=0;c-1)return!0;return!1}function l(a){var b=null,c=[];if(a.hasOwnProperty("parent"))for(b=a;b;)c.unshift(b),b=i.get("$state").get(b.parent);else for(var d=a.name.split("."),e=0,f=d[0];e-1&&g.url(o.substring(o.indexOf("#")+1)),e.location=o)}}else d.$broadcast("adal:stateMismatch",a._getItem(a.CONSTANTS.STORAGE.ERROR_DESCRIPTION),a._getItem(a.CONSTANTS.STORAGE.ERROR))}else if(c(a.config.loginResource),!b.isAuthenticated&&b.userName&&!a._renewActive){var p=i.get("adalAuthenticationService");p.acquireToken(a.config.loginResource).then(function(a){a&&(b.isAuthenticated=!0)},function(a){var b=a.split("|");d.$broadcast("adal:loginFailure",b[0],b[1])})}h(function(){c(a.config.loginResource),d.userInfo=b},1)},n=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())},o=function(c,d){if(d&&d.$$route)if(j(d.$$route,a.config))b.isAuthenticated||a._renewActive||a.loginInProgress()||(a.info("Route change event for:"+g.$$url),n());else{var e;e="function"==typeof d.$$route.templateUrl?d.$$route.templateUrl(d.params):d.$$route.templateUrl,e&&!k(e)&&a.config.anonymousEndpoints.push(e)}},p=function(c,d,e,f,h){if(d)for(var i=l(d),m=null,o=0;o-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(a){if(null==a&&(a=window.location.hash),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]));b.requestType===this.REQUEST_TYPE.LOGIN&&(c=this.callback,c&&c(this._getItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION),b.parameters[this.CONSTANTS.ID_TOKEN])),this.popUp||(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(a){this.error("The returned id_token could not be decoded",a)}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;m>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(){function a(a){for(var b=a.toString(16);b.length<2;)b="0"+b;return b}var b=window.crypto||window.msCrypto;if(b&&b.getRandomValues){var c=new Uint8Array(16);return b.getRandomValues(c),c[6]|=64,c[6]&=79,c[8]|=128,c[8]&=191,a(c[0])+a(c[1])+a(c[2])+a(c[3])+"-"+a(c[4])+a(c[5])+"-"+a(c[6])+a(c[7])+"-"+a(c[8])+a(c[9])+"-"+a(c[10])+a(c[11])+a(c[12])+a(c[13])+a(c[14])+a(c[15])}for(var d="xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx",e="0123456789abcdef",f=0,g="",h=0;h<36;h++)"-"!==d[h]&&"4"!==d[h]&&(f=16*Math.random()|0),"x"===d[h]?g+=e[f]:"y"===d[h]?(f&=3,f|=8,g+=e[f]):g+=d[h];return g},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||window.navigator.userAgent.indexOf("MSIE 5.0")===-1)){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.12"},"undefined"!=typeof module&&module.exports&&(module.exports=AuthenticationContext,module.exports.inject=function(a){return new AuthenticationContext(a)}),AuthenticationContext}(); \ No newline at end of file +/*! adal-angular v1.0.13 2016-11-08 */ +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",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:"|",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:"},POPUP_WIDTH:483,POPUP_HEIGHT:600},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.isAngular=!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.popUp&&(this.popUp=!0),this.config.callback&&"function"==typeof this.config.callback&&(this.callback=this.config.callback),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=[]),this.config.isAngular&&(this.isAngular=this.config.isAngular)},window.Logging={level:0,log:function(a){}},AuthenticationContext.prototype.login=function(){if(this._loginInProgress)return void this.info("Login in progress");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);return this._loginInProgress=!0,this.popUp?void this._loginPopup(b):void(this.config.displayCall?this.config.displayCall(b):this.promptUser(b))},AuthenticationContext.prototype._openPopup=function(a,b,c,d){try{var e=window.screenLeft?window.screenLeft:window.screenX,f=window.screenTop?window.screenTop:window.screenY,g=window.innerWidth||document.documentElement.clientWidth||document.body.clientWidth,h=window.innerHeight||document.documentElement.clientHeight||document.body.clientHeight,i=g/2-c/2+e,j=h/2-d/2+f,k=window.open(a,b,"width="+c+", height="+d+", top="+j+", left="+i);return k.focus&&k.focus(),k}catch(l){return this.warn("Error opening popup, "+l.message),this._loginInProgress=!1,null}},AuthenticationContext.prototype._loginPopup=function(a){var b=this._openPopup(a,"login",this.CONSTANTS.POPUP_WIDTH,this.CONSTANTS.POPUP_HEIGHT);if(null==b)return this.warn("Popup Window is null. This can happen if you are using IE"),this._saveItem(this.CONSTANTS.STORAGE.ERROR,"Error opening popup"),this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION,"Popup Window is null. This can happen if you are using IE"),this._saveItem(this.CONSTANTS.STORAGE.LOGIN_ERROR,"Popup Window is null. This can happen if you are using IE"),void(this.callback&&this.callback(this._getItem(this.CONSTANTS.STORAGE.LOGIN_ERROR),null,this._getItem(this.CONSTANTS.STORAGE.ERROR)));if(this.config.redirectUri.indexOf("#")!=-1)var c=this.config.redirectUri.split("#")[0];else var c=this.config.redirectUri;var d=this,e=window.setInterval(function(){b&&!b.closed&&void 0!==b.closed||(d._loginInProgress=!1,window.clearInterval(e));try{b.location.href.indexOf(c)!=-1&&(d.isAngular?window.location.hash=b.location.hash:d.handleWindowCallback(b.location.hash),window.clearInterval(e),d._loginInProgress=!1,d.info("Closing popup window"),b.close())}catch(a){}},20)},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,f){for(var g=0;g-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(a){if(null==a&&(a=window.location.hash),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],this._getItem(this.CONSTANTS.STORAGE.ERROR)));b.requestType===this.REQUEST_TYPE.LOGIN&&(c=this.callback,c&&c(this._getItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION),b.parameters[this.CONSTANTS.ID_TOKEN],this._getItem(this.CONSTANTS.STORAGE.ERROR))),this.popUp||(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;m>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._decimalToHex=function(a){for(var b=a.toString(16);b.length<2;)b="0"+b;return b},AuthenticationContext.prototype._guid=function(){var a=window.crypto||window.msCrypto;if(a&&a.getRandomValues){var b=new Uint8Array(16);return a.getRandomValues(b),b[6]|=64,b[6]&=79,b[8]|=128,b[8]&=191,this._decimalToHex(b[0])+this._decimalToHex(b[1])+this._decimalToHex(b[2])+this._decimalToHex(b[3])+"-"+this._decimalToHex(b[4])+this._decimalToHex(b[5])+"-"+this._decimalToHex(b[6])+this._decimalToHex(b[7])+"-"+this._decimalToHex(b[8])+this._decimalToHex(b[9])+"-"+this._decimalToHex(b[10])+this._decimalToHex(b[11])+this._decimalToHex(b[12])+this._decimalToHex(b[13])+this._decimalToHex(b[14])+this._decimalToHex(b[15])}for(var c="xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx",d="0123456789abcdef",e=0,f="",g=0;g<36;g++)"-"!==c[g]&&"4"!==c[g]&&(e=16*Math.random()|0),"x"===c[g]?f+=d[e]:"y"===c[g]?(e&=3,e|=8,f+=d[e]):f+=c[g];return f},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||window.navigator.userAgent.indexOf("MSIE 5.0")===-1)){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{var a="localStorage"in window&&window.localStorage;return a&&(window.localStorage.setItem("storageTest",""),window.localStorage.removeItem("storageTest")),a}catch(b){return!1}},AuthenticationContext.prototype._supportsSessionStorage=function(){try{var a="sessionStorage"in window&&window.sessionStorage;return a&&(window.sessionStorage.setItem("storageTest",""),window.sessionStorage.removeItem("storageTest")),a}catch(b){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.13"},"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 a23f7f62..956941ce 100644 --- a/lib/adal-angular.js +++ b/lib/adal-angular.js @@ -1,5 +1,5 @@ //---------------------------------------------------------------------- -// AdalJS v1.0.12 +// AdalJS v1.0.13 // @preserve Copyright (c) Microsoft Open Technologies, Inc. // All Rights Reserved // Apache License 2.0 @@ -73,7 +73,7 @@ // special function that exposes methods in Angular controller // $rootScope, $window, $q, $location, $timeout are injected by Angular - this.$get = ['$rootScope', '$window', '$q', '$location', '$timeout','$injector', function ($rootScope, $window, $q, $location, $timeout,$injector) { + this.$get = ['$rootScope', '$window', '$q', '$location', '$timeout', '$injector', function ($rootScope, $window, $q, $location, $timeout, $injector) { var locationChangeHandler = function (event, newUrl, oldUrl) { _adal.verbose('Location change event from ' + oldUrl + ' to ' + newUrl); @@ -96,13 +96,13 @@ if (callback && typeof callback === 'function') { // id_token or access_token can be renewed if (requestInfo.parameters['access_token']) { - callback(_adal._getItem(_adal.CONSTANTS.STORAGE.ERROR_DESCRIPTION), requestInfo.parameters['access_token']); + callback(_adal._getItem(_adal.CONSTANTS.STORAGE.ERROR_DESCRIPTION), requestInfo.parameters['access_token'], _adal._getItem(_adal.CONSTANTS.STORAGE.ERROR)); return; } else if (requestInfo.parameters['id_token']) { - callback(_adal._getItem(_adal.CONSTANTS.STORAGE.ERROR_DESCRIPTION), requestInfo.parameters['id_token']); + callback(_adal._getItem(_adal.CONSTANTS.STORAGE.ERROR_DESCRIPTION), requestInfo.parameters['id_token'], _adal._getItem(_adal.CONSTANTS.STORAGE.ERROR)); return; } else if (requestInfo.parameters['error']) { - callback(_adal._getItem(_adal.CONSTANTS.STORAGE.ERROR_DESCRIPTION), null); + callback(_adal._getItem(_adal.CONSTANTS.STORAGE.ERROR_DESCRIPTION), null, _adal._getItem(_adal.CONSTANTS.STORAGE.ERROR)); return; } } @@ -118,17 +118,16 @@ $rootScope.$broadcast('adal:loginSuccess', _adal._getItem(_adal.CONSTANTS.STORAGE.IDTOKEN)); } else { - $rootScope.$broadcast('adal:loginFailure', _adal._getItem(_adal.CONSTANTS.STORAGE.ERROR_DESCRIPTION)); + $rootScope.$broadcast('adal:loginFailure', _adal._getItem(_adal.CONSTANTS.STORAGE.ERROR_DESCRIPTION), _adal._getItem(_adal.CONSTANTS.STORAGE.ERROR)); } if (_adal.callback && typeof _adal.callback === 'function') - _adal.callback(_adal._getItem(_adal.CONSTANTS.STORAGE.ERROR_DESCRIPTION), _adal._getItem(_adal.CONSTANTS.STORAGE.IDTOKEN)); + _adal.callback(_adal._getItem(_adal.CONSTANTS.STORAGE.ERROR_DESCRIPTION), _adal._getItem(_adal.CONSTANTS.STORAGE.IDTOKEN), _adal._getItem(_adal.CONSTANTS.STORAGE.ERROR)); - event.preventDefault(); // redirect to login start page if (!_adal.popUp) { var loginStartPage = _adal._getItem(_adal.CONSTANTS.STORAGE.LOGIN_REQUEST); - if (loginStartPage) { + if (typeof loginStartPage !== 'undefined' && loginStartPage && loginStartPage.length !== 0) { // prevent the current location change and redirect the user back to the login start page _adal.verbose('Redirecting to start page: ' + loginStartPage); if (!$location.$$html5 && loginStartPage.indexOf('#') > -1) { @@ -137,11 +136,13 @@ $window.location = loginStartPage; } } + else + event.preventDefault(); } } else { // state did not match, broadcast an error - $rootScope.$broadcast('adal:stateMismatch', _adal._getItem(_adal.CONSTANTS.STORAGE.ERROR_DESCRIPTION)); + $rootScope.$broadcast('adal:stateMismatch', _adal._getItem(_adal.CONSTANTS.STORAGE.ERROR_DESCRIPTION), _adal._getItem(_adal.CONSTANTS.STORAGE.ERROR)); } } else { // No callback. App resumes after closing or moving to new page. @@ -149,16 +150,14 @@ updateDataFromCache(_adal.config.loginResource); 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; - if (error) { - $rootScope.$broadcast('adal:loginFailure', 'auto renew failure'); - } else { - if (tokenOut) { - _oauthData.isAuthenticated = true; - } + var self = $injector.get('adalAuthenticationService'); + self.acquireToken(_adal.config.loginResource).then(function (token) { + if (token) { + _oauthData.isAuthenticated = true; } + }, function (error) { + var errorParts = error.split('|'); + $rootScope.$broadcast('adal:loginFailure', errorParts[0], errorParts[1]); }); } } @@ -232,12 +231,11 @@ } else { var nextRouteUrl; - if(typeof nextRoute.$$route.templateUrl === "function") { + if (typeof nextRoute.$$route.templateUrl === "function") { nextRouteUrl = nextRoute.$$route.templateUrl(nextRoute.params); } else { nextRouteUrl = nextRoute.$$route.templateUrl; } - if (nextRouteUrl && !isAnonymousEndpoint(nextRouteUrl)) { _adal.config.anonymousEndpoints.push(nextRouteUrl); } @@ -261,7 +259,7 @@ } else if (state.templateUrl) { var nextStateUrl; - if (typeof state.templateUrl === 'function'){ + if (typeof state.templateUrl === 'function') { nextStateUrl = state.templateUrl(toParams); } else { @@ -276,7 +274,7 @@ }; var stateChangeErrorHandler = function (event, toState, toParams, fromState, fromParams, error) { - _adal.verbose("State change error occured. Error: " + error); + _adal.verbose("State change error occured. Error: " + JSON.stringify(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. @@ -302,7 +300,7 @@ // public methods will be here that are accessible from Controller config: _adal.config, login: function () { - loginHandler(); + _adal.login(); }, loginInProgress: function () { return _adal.loginInProgress(); @@ -319,12 +317,14 @@ // automated token request call var deferred = $q.defer(); _adal._renewActive = true; - _adal.acquireToken(resource, function (error, tokenOut) { + _adal.acquireToken(resource, function (errorDesc, tokenOut, error) { _adal._renewActive = false; if (error) { + $rootScope.$broadcast('adal:acquireTokenFailure', errorDesc, error); _adal.error('Error when acquiring token for resource: ' + resource, error); - deferred.reject(error); + deferred.reject(errorDesc + "|" + error); } else { + $rootScope.$broadcast('adal:acquireTokenSuccess', tokenOut); deferred.resolve(tokenOut); } }); @@ -411,20 +411,18 @@ authService.verbose('Token is available'); config.headers.Authorization = 'Bearer ' + token; delayedRequest.resolve(config); - }, function (err) { - config.data = err; + }, function (errDesc, error) { + config.data = errDesc + "|" + error; delayedRequest.reject(config); }); return delayedRequest.promise; } } - - return config; } }, responseError: function (rejection) { - authService.info('Getting error in the response.'); + authService.info('Getting error in the response: ' + JSON.stringify(rejection)); if (rejection) { if (rejection.status === 401) { var resource = authService.getResourceForEndpoint(rejection.config.url); diff --git a/lib/adal.js b/lib/adal.js index 083390d6..45bc0cd1 100644 --- a/lib/adal.js +++ b/lib/adal.js @@ -1,5 +1,5 @@ //---------------------------------------------------------------------- -// AdalJS v1.0.12 +// AdalJS v1.0.13 // @preserve Copyright (c) Microsoft Open Technologies, Inc. // All Rights Reserved // Apache License 2.0 @@ -22,29 +22,29 @@ var AuthenticationContext = (function () { 'use strict'; /** - * 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. + * Configuration options for Authentication Context. + * @class config + * @property {string} tenant - Your target tenant. + * @property {string} clientID - Client ID assigned to your app by Azure Active Directory. + * @property {string} redirectUri - Endpoint at which you expect to receive tokens.Defaults to `window.location.href`. + * @property {string} instance - Azure Active Directory Instance.Defaults to `https://login.microsoftonline.com/`. + * @property {Array} endpoints - Collection of {Endpoint-ResourceId} used for automatically attaching tokens in webApi calls. + * @property {Boolean} popUp - Set this to true to enable login in a popup winodow instead of a full redirect.Defaults to `false`. + * @property {string} localLoginUrl - Set this to redirect the user to a custom login page. + * @property {function} displayCall - User defined function of handling the navigation to Azure AD authorization endpoint in case of login. Defaults to 'null'. + * @property {string} postLogoutRedirectUri - Redirects the user to postLogoutRedirectUri after logout. Defaults to 'null'. + * @property {string} cacheLocation - Sets browser storage to either 'localStorage' or sessionStorage'. Defaults to 'sessionStorage'. + * @property {Array.} anonymousEndpoints Array of keywords or URI's. Adal will not attach a token to outgoing requests that have these keywords or uri. Defaults to 'null'. + * @property {number} expireOffsetSeconds If the cached token is about to be expired in the expireOffsetSeconds (in seconds), Adal will renew the token instead of using the cached token. Defaults to 120 seconds. + * @property {string} correlationId Unique identifier used to map the request with the response. Defaults to RFC4122 version 4 guid (128 bits). */ /** * Creates a new AuthenticationContext object. * @constructor - * @param {object} config Configuration options for AuthenticationContext - * - **/ + * @param {config} config Configuration options for AuthenticationContext + */ + AuthenticationContext = function (config) { /** * Enum for request type @@ -169,8 +169,7 @@ var AuthenticationContext = (function () { }; /** - * Gets initial Idtoken for the app backend - * Saves the resulting Idtoken in localStorage. + * Initiates the login process by redirecting the user to Azure AD authorization endpoint. */ AuthenticationContext.prototype.login = function () { // Token is not present and user needs to login @@ -202,6 +201,10 @@ var AuthenticationContext = (function () { } }; + /** + * Configures popup window for login. + * @ignore + */ AuthenticationContext.prototype._openPopup = function (urlNavigate, title, popUpWidth, popUpHeight) { try { /** @@ -231,6 +234,11 @@ var AuthenticationContext = (function () { } } + /** + * After authorization, the user will be sent to your specified redirect_uri with the user's bearer token + * attached to the URI fragment as an id_token field. It closes popup window after redirection. + * @ignore + */ AuthenticationContext.prototype._loginPopup = function (urlNavigate) { var popupWindow = this._openPopup(urlNavigate, "login", this.CONSTANTS.POPUP_WIDTH, this.CONSTANTS.POPUP_HEIGHT); if (popupWindow == null) { @@ -239,7 +247,7 @@ var AuthenticationContext = (function () { this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, 'Popup Window is null. This can happen if you are using IE'); this._saveItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, 'Popup Window is null. This can happen if you are using IE'); if (this.callback) - this.callback(this._getItem(this.CONSTANTS.STORAGE.LOGIN_ERROR), null); + this.callback(this._getItem(this.CONSTANTS.STORAGE.LOGIN_ERROR), null, this._getItem(this.CONSTANTS.STORAGE.ERROR)); return; } if (this.config.redirectUri.indexOf('#') != -1) @@ -270,19 +278,28 @@ var AuthenticationContext = (function () { }, 20); }; + /** + * Checks if login is in progress. + * @returns {Boolean} true if login is in progress, false otherwise. + */ AuthenticationContext.prototype.loginInProgress = function () { return this._loginInProgress; }; + /** + * Checks for the resource in the cache. By default, cache location is Session Storage + * @ignore + * @returns {Boolean} 'true' if login is in progress, else returns 'false'. + */ 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 + * Gets token for the specified resource from the cache. + * @param {string} resource A URI that identifies the resource for which the token is requested. + * @returns {string} token if if it exists and not expired, otherwise null. */ AuthenticationContext.prototype.getCachedToken = function (resource) { if (!this._hasResource(resource)) { @@ -303,8 +320,16 @@ var AuthenticationContext = (function () { return null; } }; + + /** + * User information from idtoken. + * @class User + * @property {string} userName - username assigned from upn or email. + * @property {object} profile - properties parsed from idtoken. + */ + /** - * Retrieves and parse idToken from localstorage + * If user object exists, returns it. Else creates a new user object by decoding id_token from the cache. * @returns {User} user object */ AuthenticationContext.prototype.getCachedUser = function () { @@ -316,7 +341,13 @@ var AuthenticationContext = (function () { this._user = this._createUser(idtoken); return this._user; }; - + + /** + * Adds the passed callback to the array of callbacks for the specified resource and puts the array on the window object. + * @param {string} resource A URI that identifies the resource for which the token is requested. + * @param {string} expectedState A unique identifier (guid). + * @param {tokenCallback} callback - The callback provided by the caller. It will be called with token or error. + */ AuthenticationContext.prototype.registerCallback = function (expectedState, resource, callback) { this._activeRenewals[resource] = expectedState; if (!window.callBacksMappedToRenewStates[expectedState]) { @@ -325,9 +356,14 @@ var AuthenticationContext = (function () { var self = this; window.callBacksMappedToRenewStates[expectedState].push(callback); if (!window.callBackMappedToRenewStates[expectedState]) { - window.callBackMappedToRenewStates[expectedState] = function (message, token) { + window.callBackMappedToRenewStates[expectedState] = function (errorDesc, token, error) { for (var i = 0; i < window.callBacksMappedToRenewStates[expectedState].length; ++i) { - window.callBacksMappedToRenewStates[expectedState][i](message, token); + try { + window.callBacksMappedToRenewStates[expectedState][i](errorDesc, token, error); + } + catch (error) { + self.warn(error); + } } self._activeRenewals[resource] = null; window.callBacksMappedToRenewStates[expectedState] = null; @@ -342,8 +378,7 @@ var AuthenticationContext = (function () { // with callback /** * Acquires access token with hidden iframe - * @param {string} resource ResourceUri identifying the target resource - * @returns {string} access token if request is successful + * @ignore */ AuthenticationContext.prototype._renewToken = function (resource, callback) { // use iframe to try refresh token @@ -361,12 +396,15 @@ var AuthenticationContext = (function () { 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); }; + /** + * Renews idtoken for app's own backend when resource is clientId and calls the callback with token/error + * @ignore + */ AuthenticationContext.prototype._renewIdToken = function (callback) { // use iframe to try refresh token this.info('renewIdToken is called'); @@ -386,11 +424,14 @@ var AuthenticationContext = (function () { 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); }; + /** + * Checks if the authorization endpoint URL contains query string parameters + * @ignore + */ 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 + "="); @@ -399,6 +440,9 @@ var AuthenticationContext = (function () { // 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 + /** + * @ignore + */ AuthenticationContext.prototype._loadFrameTimeout = function (urlNavigation, frameName, resource) { //set iframe session to pending this.verbose('Set loading state to pending for: ' + resource); @@ -411,7 +455,7 @@ var AuthenticationContext = (function () { 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); + window.callBackMappedToRenewStates[expectedState]('Token renewal operation failed due to timeout', null, 'Token Renewal Failed'); } self._saveItem(self.CONSTANTS.STORAGE.RENEW_STATUS + resource, self.CONSTANTS.TOKEN_RENEW_STATUS_CANCELED); @@ -419,6 +463,10 @@ var AuthenticationContext = (function () { }, self.CONSTANTS.LOADFRAME_TIMEOUT); } + /** + * Loads iframe with authorization endpoint URL + * @ignore + */ AuthenticationContext.prototype._loadFrame = function (urlNavigate, frameName) { // This trick overcomes iframe navigation in IE // IE does not load the page consistently in iframe @@ -435,27 +483,33 @@ var AuthenticationContext = (function () { }; /** - * Acquire token from cache if not expired and available. Acquires token from iframe if expired. + * @callback tokenCallback + * @param {string} error error message returned from AAD if token request fails. + * @param {string} token token returned from AAD if token request is successful. + */ + + /** + * Acquires token from the cache if it is not expired. Otherwise sends request to AAD to obtain a new token. * @param {string} resource ResourceUri identifying the target resource - * @param {requestCallback} callback + * @param {tokenCallback} callback - The callback provided by the caller. It will be called with token or error. */ AuthenticationContext.prototype.acquireToken = function (resource, callback) { if (this._isEmpty(resource)) { this.warn('resource is required'); - callback('resource is required', null); + callback('resource is required', null, 'resource is required'); return; } var token = this.getCachedToken(resource); if (token) { this.info('Token is already in cache for resource:' + resource); - callback(null, token); + callback(null, token, null); return; } if (!this._user) { this.warn('User login is required'); - callback('User login is required', null); + callback('User login is required', null, 'login required'); return; } @@ -478,8 +532,8 @@ var AuthenticationContext = (function () { }; /** - * Redirect the Browser to Azure AD Authorization endpoint - * @param {string} urlNavigate The authorization request url + * Redirects the browser to Azure AD authorization endpoint. + * @param {string} urlNavigate Url of the authorization endpoint. */ AuthenticationContext.prototype.promptUser = function (urlNavigate) { if (urlNavigate) { @@ -491,7 +545,7 @@ var AuthenticationContext = (function () { }; /** - * Clear cache items. + * Clears cache items. */ AuthenticationContext.prototype.clearCache = function () { this._saveItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY, ''); @@ -516,7 +570,8 @@ var AuthenticationContext = (function () { }; /** - * Clear cache items for a resource. + * Clears cache items for a given resource. + * @param {string} resource a URI that identifies the resource. */ AuthenticationContext.prototype.clearCacheForResource = function (resource) { this._saveItem(this.CONSTANTS.STORAGE.STATE_RENEW, ''); @@ -529,23 +584,31 @@ var AuthenticationContext = (function () { }; /** - * Logout user will redirect page to logout endpoint. - * After logout, it will redirect to post_logout page if provided. + * Redirects user to logout endpoint. + * After logout, it will redirect to postLogoutRedirectUri if added as a property on the config object. */ AuthenticationContext.prototype.logOut = function () { this.clearCache(); - var tenant = 'common'; - var logout = ''; this._user = null; - if (this.config.tenant) { - tenant = this.config.tenant; - } + var urlNavigate; - if (this.config.postLogoutRedirectUri) { - logout = 'post_logout_redirect_uri=' + encodeURIComponent(this.config.postLogoutRedirectUri); + if (this.config.logOutUri) { + urlNavigate = this.config.logOutUri; + } else { + var tenant = 'common'; + var logout = ''; + + if (this.config.tenant) { + tenant = this.config.tenant; + } + + if (this.config.postLogoutRedirectUri) { + logout = 'post_logout_redirect_uri=' + encodeURIComponent(this.config.postLogoutRedirectUri); + } + + urlNavigate = this.instance + tenant + '/oauth2/logout?' + logout; } - var urlNavigate = this.instance + tenant + '/oauth2/logout?' + logout; this.info('Logout navigate to: ' + urlNavigate); this.promptUser(urlNavigate); }; @@ -555,15 +618,14 @@ var AuthenticationContext = (function () { }; /** - * This callback is displayed as part of the Requester class. - * @callback requestCallback - * @param {string} error - * @param {User} user + * @callback userCallback + * @param {string} error error message if user info is not available. + * @param {User} user user object retrieved from the cache. */ /** - * Gets a user profile - * @param {requestCallback} cb - The callback that handles the response. + * Calls the passed in callback with the user object or error message related to the user. + * @param {userCallback} callback - The callback provided by the caller. It will be called with user or error. */ AuthenticationContext.prototype.getUser = function (callback) { // IDToken is first call @@ -585,10 +647,15 @@ var AuthenticationContext = (function () { callback(null, this._user); } else { this.warn('User information is not available'); - callback('User information is not available'); + callback('User information is not available', null); } }; + /** + * Adds login_hint to authorization URL which is used to pre-fill the username field of sign in page for the user if known ahead of time. + * domain_hint can be one of users/organisations which when added skips the email based discovery process of the user. + * @ignore + */ AuthenticationContext.prototype._addHintParameters = function (urlNavigate) { // include hint params only if upn is present if (this._user && this._user.profile && this._user.profile.hasOwnProperty('upn')) { @@ -607,6 +674,10 @@ var AuthenticationContext = (function () { return urlNavigate; } + /** + * Creates a user object by decoding the id_token + * @ignore + */ AuthenticationContext.prototype._createUser = function (idToken) { var user = null; var parsedJson = this._extractIdToken(idToken); @@ -632,6 +703,10 @@ var AuthenticationContext = (function () { return user; }; + /** + * Returns the anchor part(#) of the URL + * @ignore + */ AuthenticationContext.prototype._getHash = function (hash) { if (hash.indexOf('#/') > -1) { hash = hash.substring(hash.indexOf('#/') + 2); @@ -643,9 +718,9 @@ var AuthenticationContext = (function () { }; /** - * Checks if hash contains access token or id token or error_description + * Checks if the URL fragment contains access token, id token or error_description. * @param {string} hash - Hash passed from redirect page - * @returns {Boolean} + * @returns {Boolean} true if response contains id_token, access_token or error, false otherwise. */ AuthenticationContext.prototype.isCallback = function (hash) { hash = this._getHash(hash); @@ -659,15 +734,25 @@ var AuthenticationContext = (function () { /** * Gets login error - * @returns {string} error message related to login + * @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 + * Request info object created from the response received from AAD. + * @class RequestInfo + * @property {object} parameters - object comprising of fields such as id_token/error, session_state, state, e.t.c. + * @property {REQUEST_TYPE} requestType - either LOGIN, RENEW_TOKEN or UNKNOWN. + * @property {boolean} stateMatch - true if state is valid, false otherwise. + * @property {string} stateResponse - unique guid used to match the response with the request. + * @property {boolean} valid - true if requestType contains id_token, access_token or error, false otherwise. + */ + + /** + * Creates a requestInfo object from the URL fragment and returns it. + * @returns {RequestInfo} an object created from the redirect response from AAD comprising of the keys - parameters, requestType, stateMatch, stateResponse and valid. */ AuthenticationContext.prototype.getRequestInfo = function (hash) { hash = this._getHash(hash); @@ -708,7 +793,7 @@ var AuthenticationContext = (function () { } // external api requests may have many renewtoken requests for different resource - if (!requestInfo.stateMatch && window.parent && window.parent.AuthenticationContext()) { + 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) { @@ -724,6 +809,10 @@ var AuthenticationContext = (function () { return requestInfo; }; + /** + * Extracts resource value from state. + * @ignore + */ AuthenticationContext.prototype._getResourceFromState = function (state) { if (state) { var splitIndex = state.indexOf('|'); @@ -736,9 +825,7 @@ var AuthenticationContext = (function () { }; /** - * Saves token from hash that is received from redirect. - * @param {string} hash - Hash passed from redirect page - * @returns {string} error message related to login + * Saves token or error received in the response from AAD in the cache. In case of id_token, it also creates the user object. */ AuthenticationContext.prototype.saveTokenFromHash = function (requestInfo) { this.info('State status:' + requestInfo.stateMatch + '; Request type:' + requestInfo.requestType); @@ -819,8 +906,8 @@ var AuthenticationContext = (function () { /** * Gets resource for given endpoint if mapping is provided with config. - * @param {string} endpoint - API endoibt - * @returns {string} resource for this API endpoint + * @param {string} endpoint - The URI for which the resource Id is requested. + * @returns {string} resource for this API endpoint. */ AuthenticationContext.prototype.getResourceForEndpoint = function (endpoint) { if (this.config && this.config.endpoints) { @@ -859,6 +946,10 @@ var AuthenticationContext = (function () { return null; }; + /** + * Strips the protocol part of the URL and returns it. + * @ignore + */ AuthenticationContext.prototype._getHostFromUri = function (uri) { // remove http:// or https:// from uri var extractedUri = String(uri).replace(/^(https?:)\/\//, ''); @@ -867,7 +958,10 @@ var AuthenticationContext = (function () { return extractedUri; }; - /*exported oauth2Callback */ + /** + * This method must be called for processing the response received from AAD. It extracts the hash, processes the token or error, saves it in the cache and calls the registered callbacks with the result. + * @param {string} [hash=window.location.hash] - Hash fragment of Url. + */ AuthenticationContext.prototype.handleWindowCallback = function (hash) { // This is for regular javascript usage for redirect handling // need to make sure this is for callback @@ -883,18 +977,22 @@ var AuthenticationContext = (function () { 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]); + callback(this._getItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION), requestInfo.parameters[this.CONSTANTS.ACCESS_TOKEN] || requestInfo.parameters[this.CONSTANTS.ID_TOKEN], this._getItem(this.CONSTANTS.STORAGE.ERROR)); return; } else if (requestInfo.requestType === this.REQUEST_TYPE.LOGIN) { callback = this.callback; if (callback) - callback(this._getItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION), requestInfo.parameters[this.CONSTANTS.ID_TOKEN]); + callback(this._getItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION), requestInfo.parameters[this.CONSTANTS.ID_TOKEN], this._getItem(this.CONSTANTS.STORAGE.ERROR)); } if (!this.popUp)// No need to redirect user in case of popup window.location = this._getItem(this.CONSTANTS.STORAGE.LOGIN_REQUEST); } }; + /** + * Constructs the authorization endpoint URL and returns it. + * @ignore + */ AuthenticationContext.prototype._getNavigateUrl = function (responseType, resource) { var tenant = 'common'; if (this.config.tenant) { @@ -906,6 +1004,10 @@ var AuthenticationContext = (function () { return urlNavigate; }; + /** + * Returns the decoded id_token. + * @ignore + */ AuthenticationContext.prototype._extractIdToken = function (encodedIdToken) { // id token will be decoded to get the username var decodedToken = this._decodeJwt(encodedIdToken); @@ -930,6 +1032,10 @@ var AuthenticationContext = (function () { return null; }; + /** + * Decodes a string of data which has been encoded using base-64 encoding. + * @ignore + */ AuthenticationContext.prototype._base64DecodeStringUrlSafe = function (base64IdToken) { // html5 should support atob function for decoding base64IdToken = base64IdToken.replace(/-/g, '+').replace(/_/g, '/'); @@ -989,6 +1095,10 @@ var AuthenticationContext = (function () { return decoded; }; + /** + * Decodes an id token into an object with header, payload and signature fields. + * @ignore + */ // Adal.node js crack function AuthenticationContext.prototype._decodeJwt = function (jwtToken) { if (this._isEmpty(jwtToken)) { @@ -1012,10 +1122,18 @@ var AuthenticationContext = (function () { return crackedToken; }; + /** + * Converts string to represent binary data in ASCII string format by translating it into a radix-64 representation and returns it + * @ignore + */ AuthenticationContext.prototype._convertUrlSafeToRegularBase64EncodedString = function (str) { return str.replace('-', '+').replace('_', '/'); }; + /** + * Serializes the parameters for the authorization endpoint URL and returns the serialized uri string. + * @ignore + */ AuthenticationContext.prototype._serialize = function (responseType, obj, resource) { var str = []; if (obj !== null) { @@ -1043,6 +1161,10 @@ var AuthenticationContext = (function () { return str.join('&'); }; + /** + * Parses the query string parameters into a key-value pair object. + * @ignore + */ AuthenticationContext.prototype._deserialize = function (query) { var match, pl = /\+/g, // Regex for replacing addition symbol with a space @@ -1060,6 +1182,22 @@ var AuthenticationContext = (function () { return obj; }; + /** + * Converts decimal value to hex equivalent + * @ignore + */ + AuthenticationContext.prototype._decimalToHex = function (number) { + var hex = number.toString(16); + while (hex.length < 2) { + hex = '0' + hex; + } + return hex; + } + + /** + * Generates RFC4122 version 4 guid (128 bits) + * @ignore + */ /* jshint ignore:start */ AuthenticationContext.prototype._guid = function () { // RFC4122: The version 4 UUID is meant for generating UUIDs from truly-random or @@ -1092,15 +1230,8 @@ var AuthenticationContext = (function () { //buffer[8] represents the clock_seq_hi_and_reserved field. We will set the two most significant bits (6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively. buffer[8] |= 0x80; //buffer[8] | 10000000 will set the 7 bit to 1. buffer[8] &= 0xbf; //buffer[8] & 10111111 will set the 6 bit to 0. - function decimalToHex(num) { - var hex = num.toString(16); - while (hex.length < 2) { - hex = '0' + hex; - } - return hex; - } - return decimalToHex(buffer[0]) + decimalToHex(buffer[1]) + decimalToHex(buffer[2]) + decimalToHex(buffer[3]) + '-' + decimalToHex(buffer[4]) + decimalToHex(buffer[5]) + '-' + decimalToHex(buffer[6]) + decimalToHex(buffer[7]) + '-' + - decimalToHex(buffer[8]) + decimalToHex(buffer[9]) + '-' + decimalToHex(buffer[10]) + decimalToHex(buffer[11]) + decimalToHex(buffer[12]) + decimalToHex(buffer[13]) + decimalToHex(buffer[14]) + decimalToHex(buffer[15]); + return this._decimalToHex(buffer[0]) + this._decimalToHex(buffer[1]) + this._decimalToHex(buffer[2]) + this._decimalToHex(buffer[3]) + '-' + this._decimalToHex(buffer[4]) + this._decimalToHex(buffer[5]) + '-' + this._decimalToHex(buffer[6]) + this._decimalToHex(buffer[7]) + '-' + + this._decimalToHex(buffer[8]) + this._decimalToHex(buffer[9]) + '-' + this._decimalToHex(buffer[10]) + this._decimalToHex(buffer[11]) + this._decimalToHex(buffer[12]) + this._decimalToHex(buffer[13]) + this._decimalToHex(buffer[14]) + this._decimalToHex(buffer[15]); } else { var guidHolder = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'; @@ -1128,15 +1259,26 @@ var AuthenticationContext = (function () { }; /* jshint ignore:end */ + /** + * Calculates the expires in value in milliseconds for the acquired token + * @ignore + */ AuthenticationContext.prototype._expiresIn = function (expires) { return this._now() + parseInt(expires, 10); }; + /** + * Return the number of milliseconds since 1970/01/01 + * @ignore + */ AuthenticationContext.prototype._now = function () { return Math.round(new Date().getTime() / 1000.0); }; - + /** + * Adds the hidden iframe for silent token renewal + * @ignore + */ AuthenticationContext.prototype._addAdalFrame = function (iframeId) { if (typeof iframeId === 'undefined') { return; @@ -1167,6 +1309,10 @@ var AuthenticationContext = (function () { return adalFrame; }; + /** + * Saves the key-value pair in the cache + * @ignore + */ AuthenticationContext.prototype._saveItem = function (key, obj) { if (this.config && this.config.cacheLocation && this.config.cacheLocation === 'localStorage') { @@ -1191,6 +1337,10 @@ var AuthenticationContext = (function () { return true; }; + /** + * Searches the value for the given key in the cache + * @ignore + */ AuthenticationContext.prototype._getItem = function (key) { if (this.config && this.config.cacheLocation && this.config.cacheLocation === 'localStorage') { @@ -1212,22 +1362,44 @@ var AuthenticationContext = (function () { return sessionStorage.getItem(key); }; + /** + * Returns true if browser supports localStorage, false otherwise. + * @ignore + */ AuthenticationContext.prototype._supportsLocalStorage = function () { try { - return 'localStorage' in window && window['localStorage']; + var supportsLocalStorage = 'localStorage' in window && window['localStorage']; + if (supportsLocalStorage) { + window.localStorage.setItem('storageTest', ''); + window.localStorage.removeItem('storageTest'); + } + return supportsLocalStorage; } catch (e) { return false; } }; + /** + * Returns true if browser supports sessionStorage, false otherwise. + * @ignore + */ AuthenticationContext.prototype._supportsSessionStorage = function () { try { - return 'sessionStorage' in window && window['sessionStorage']; + var supportsSessionStorage = 'sessionStorage' in window && window['sessionStorage']; + if (supportsSessionStorage) { + window.sessionStorage.setItem('storageTest', ''); + window.sessionStorage.removeItem('storageTest'); + } + return supportsSessionStorage; } catch (e) { return false; } }; + /** + * Returns a cloned copy of the passed object. + * @ignore + */ AuthenticationContext.prototype._cloneConfig = function (obj) { if (null === obj || 'object' !== typeof obj) { return obj; @@ -1242,12 +1414,22 @@ var AuthenticationContext = (function () { return copy; }; + /** + * Adds the library version and returns it. + * @ignore + */ AuthenticationContext.prototype._addLibMetadata = function () { // x-client-SKU // x-client-Ver return '&x-client-SKU=Js&x-client-Ver=' + this._libVersion(); }; + /** + * Checks the Logging Level, constructs the Log message and logs it. Users need to implement/override this method to turn on Logging. + * @param {number} level - Level can be set 0,1,2 and 3 which turns on 'error', 'warning', 'info' or 'verbose' level logging respectively. + * @param {string} message - Message to log. + * @param {string} error - Error to log. + */ AuthenticationContext.prototype.log = function (level, message, error) { if (level <= Logging.level) { var timestamp = new Date().toUTCString(); @@ -1266,26 +1448,51 @@ var AuthenticationContext = (function () { } }; + /** + * Logs messages when Logging Level is set to 0. + * @param {string} message - Message to log. + * @param {string} error - Error to log. + */ AuthenticationContext.prototype.error = function (message, error) { this.log(this.CONSTANTS.LOGGING_LEVEL.ERROR, message, error); }; + /** + * Logs messages when Logging Level is set to 1. + * @param {string} message - Message to log. + */ AuthenticationContext.prototype.warn = function (message) { this.log(this.CONSTANTS.LOGGING_LEVEL.WARN, message, null); }; + /** + * Logs messages when Logging Level is set to 2. + * @param {string} message - Message to log. + */ AuthenticationContext.prototype.info = function (message) { this.log(this.CONSTANTS.LOGGING_LEVEL.INFO, message, null); }; + /** + * Logs messages when Logging Level is set to 3. + * @param {string} message - Message to log. + */ AuthenticationContext.prototype.verbose = function (message) { this.log(this.CONSTANTS.LOGGING_LEVEL.VERBOSE, message, null); }; + /** + * Returns the library version. + * @ignore + */ AuthenticationContext.prototype._libVersion = function () { - return '1.0.12'; + return '1.0.13'; }; + /** + * Returns a reference of Authentication Context as a result of a require call. + * @ignore + */ if (typeof module !== 'undefined' && module.exports) { module.exports = AuthenticationContext; module.exports.inject = function (conf) { diff --git a/package.json b/package.json index 7e61824e..55aca42c 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.12", + "version": "1.0.13", "description": "Windows Azure Active Directory Client Library for js", "keywords": [ "implicit", @@ -39,9 +39,9 @@ "grunt-contrib-watch": "~0.2.0", "grunt-karma": "^0.9.x", "atob": "~1.1.2", - "karma-chrome-launcher": "~0.1.5", - "karma": "~0.12.24", - "karma-jasmine": "~0.1.5", + "karma-chrome-launcher": "^0.1.5", + "karma": "^0.12.24", + "karma-jasmine": "^0.1.5", "bower": "^1.3.3", "grunt-jasmine-node": "~0.2.1" }, diff --git a/tests/angularModuleSpec.js b/tests/angularModuleSpec.js index dd2456fb..0c4ab047 100644 --- a/tests/angularModuleSpec.js +++ b/tests/angularModuleSpec.js @@ -76,6 +76,11 @@ describe('TaskCtl', function () { return q.when(token); }; + window.parent.AuthenticationContext = window.AuthenticationContext; + window.location.hash = ''; + + // to prevent full page reload error in karma + window.onbeforeunload = function () { return }; controller('TaskCtl', { $scope: scope, adalAuthenticationService: adalServiceProvider }); })); @@ -200,179 +205,150 @@ describe('TaskCtl', function () { return headers.Authorization === 'Bearer Token456' }).respond(200); + var eventName = '', msg = ''; scope.$on('adal:errorResponse', function (event, message) { - expect(event.name).toBe('adal:errorResponse'); - expect(message.data).toBe('login in progress, cancelling the request for https://myapp.com/someapi/item'); + eventName = event.name; + msg = message; }); scope.taskCall5(); scope.$apply(); expect(rootScope.$broadcast).toHaveBeenCalled(); + expect(eventName).toBe('adal:errorResponse'); + expect(msg.data).toBe('login in progress, cancelling the request for https://myapp.com/someapi/item'); + }); it('tests stateMismatch broadcast when state does not match', function () { - window.parent.AuthenticationContext = function () { - return { - callback: function () { }, - _renewStates: {} - }; + window.parent = { + AuthenticationContext: function () { + return { + _renewStates: {} + } + }, }; window.location.hash = 'id_token=sample&state=4343'; spyOn(rootScope, '$broadcast').andCallThrough(); + + var eventName = '', msg = ''; scope.$on('adal:stateMismatch', function (event, message) { - expect(event.name).toBe('adal:stateMismatch'); - expect(message).toBe('Invalid_state. state: 4343'); + eventName = event.name; + msg = message; }); + scope.$apply(); expect(rootScope.$broadcast).toHaveBeenCalled(); + expect(eventName).toBe('adal:stateMismatch'); + expect(msg).toBe('Invalid_state. state: 4343'); }); it('tests callback is called when response contains error', function () { - window.parent.AuthenticationContext = function () { - return { - callback: function () { }, - _renewStates: ['4343'] - }; + var error = '', errorDesc = ''; + var callback = function (valErrorDesc, valToken, valError) { + error = valError; + errorDesc = valErrorDesc; }; - window.parent.callBackMappedToRenewStates = {}; - window.parent.callBackMappedToRenewStates['4343'] = function (error, token) { - expect(error).toBe('renewfailed'); + window.parent = { + AuthenticationContext: function () { + return { + _renewStates: ['4343'] + } + }, + callBackMappedToRenewStates: { "4343": callback } }; window.location.hash = 'error=sample&error_description=renewfailed&state=4343'; scope.$apply(); + expect(error).toBe('sample'); + expect(errorDesc).toBe('renewfailed'); }); it('tests callback is called when response contains access token', function () { - window.parent.AuthenticationContext = function () { - return { - callback: function () { }, - _renewStates: ['4343'] - }; + var error = null, errorDesc = null, token = ''; + var callback = function (valErrorDesc, valToken, valError) { + error = valError; + errorDesc = valErrorDesc; + token = valToken; }; - window.parent.callBackMappedToRenewStates = {}; - window.parent.callBackMappedToRenewStates['4343'] = function (error, token) { - expect(error).toBe(''); - expect(token).toBe('newAccessToken123'); + window.parent = { + AuthenticationContext: function () { + return { + _renewStates: ['4343'] + } + }, + callBackMappedToRenewStates: { "4343": callback } }; window.location.hash = 'access_token=newAccessToken123&state=4343'; scope.$apply(); + expect(error).toBe(''); + expect(errorDesc).toBe(''); + expect(token).toBe('newAccessToken123'); }); - it('tests callback is called when response contains id token', function () { - window.parent.AuthenticationContext = function () { - return { - callback: function () { }, - _renewStates: ['4343'] - }; + var error = '', errorDesc = '', token = ''; + var callback = function (valErrorDesc, valToken, valError) { + error = valError; + errorDesc = valErrorDesc; + token = valToken; }; - window.parent.callBackMappedToRenewStates = {}; - window.parent.callBackMappedToRenewStates['4343'] = function (error, token) { - expect(error).toBe('Invalid id_token. id_token: newIdToken123'); - expect(token).toBe('newIdToken123'); + window.parent = { + AuthenticationContext: function () { + return { + _renewStates: ['4343'] + } + }, + callBackMappedToRenewStates: { "4343": callback } }; window.location.hash = 'id_token=newIdToken123&state=4343'; scope.$apply(); + expect(errorDesc).toBe('Invalid id_token. id_token: newIdToken123'); + expect(error).toBe('invalid id_token'); + expect(token).toBe('newIdToken123'); }); 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'; + window.sessionStorage.setItem('adal.state.login', '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); + var eventName = '', error = '', errorDesc = '', token = ''; + scope.$on('adal:loginFailure', function (event, valErrorDesc, valError) { + eventName = event.name; + errorDesc = valErrorDesc; + error = valError; }); scope.$apply(); expect(rootScope.$broadcast).toHaveBeenCalled(); - + expect(eventName).toBe('adal:loginFailure'); + expect(errorDesc).toBe('Invalid id_token. id_token: ' + mockInvalidClientIdToken); + expect(error).toBe('invalid id_token'); + window.sessionStorage.setItem('adal.state.login', ''); }); 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'; + window.sessionStorage.setItem('adal.state.login', '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'); + var eventName = '', token = ''; + scope.$on('adal:loginSuccess', function (event, valToken) { + eventName = event.name; + token = valToken; }); 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(); + expect(eventName).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'); + expect(token).toBe(mockIdToken); + adalServiceProvider.logOut(); }); 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); @@ -456,3 +432,124 @@ describe('StateCtrl', function () { } }); }); + +describe('AcquireTokenCtl', function () { + var scope, adalServiceProvider, rootScope, controller, window, $httpBackend, route, location; + var store = {}; + //mock Application to allow us to inject our own dependencies + beforeEach(angular.mock.module('TestApplication')); + + //mock the controller for the same reason and include $scope and $controller + beforeEach(angular.mock.inject(function (_adalAuthenticationService_, _$rootScope_, _$controller_, _$window_, _$httpBackend_, _$route_, _$location_) { + adalServiceProvider = _adalAuthenticationService_; + rootScope = _$rootScope_; + controller = _$controller_; + window = _$window_; + $httpBackend = _$httpBackend_; + route = _$route_; + location = _$location_; + //create an empty scope + scope = rootScope.$new(); + + spyOn(sessionStorage, 'getItem').andCallFake(function (key) { + return store[key]; + }); + spyOn(sessionStorage, 'setItem').andCallFake(function (key, value) { + store[key] = value; + }); + spyOn(window, 'Date').andCallFake(function () { + return { + getTime: function () { + return 1000; + }, + toUTCString: function () { + return ""; + } + }; + }); + })); + + afterEach(function () { + store = {}; + }); + + it('checks if acquireTokenSuccess/acquireTokenFailure events are broadcasted in case of acquireToken', function () { + var error = '', errorDesc = ''; + var tokenOut = ''; + var token = 'token123'; + spyOn(rootScope, '$broadcast').andCallThrough(); + scope.$on('adal:acquireTokenFailure', function (event, valErrorDesc, valError) { + errorDesc = valErrorDesc; + error = valError; + }); + adalServiceProvider.acquireToken(adalServiceProvider.config.loginResource); + expect(errorDesc).toBe('User login is required'); + expect(error).toBe('login required'); + store = { + 'adal.token.keys': adalServiceProvider.config.loginResource + '|', + 'adal.access.token.keyloginResource123': token, + 'adal.expiration.keyloginResource123': 122 + }; + scope.$on('adal:acquireTokenSuccess', function (event, message) { + tokenOut = message; + }); + adalServiceProvider.acquireToken(adalServiceProvider.config.loginResource); + expect(tokenOut).toBe(token); + }); + + + it('checks if user is redirected to the custom Login Page when localLoginUrl is specified', function () { + spyOn(rootScope, '$broadcast').andCallThrough(); + + adalServiceProvider.config.localLoginUrl = '/login'; + $httpBackend.expectGET('login.html').respond(200); + var loginRoute = route.routes['/login']; + location.url('/todoList'); + scope.$apply(); + expect(route.current.controller).toBe(loginRoute.controller); + expect(route.current.templateUrl).toBe(loginRoute.templateUrl); + expect(adalServiceProvider.loginInProgress()).toBe(false); + adalServiceProvider.config.localLoginUrl = null; + }); + + it('checks if loginRedirect event is fired when localLoginUrl is not specified', function () { + spyOn(rootScope, '$broadcast').andCallThrough(); + + adalServiceProvider.config.localLoginUrl = null; + location.url('/todoList'); + var eventName = ''; + scope.$on('adal:loginRedirect', function (event) { + eventName = event.name; + }); + scope.$apply(); + expect(adalServiceProvider.loginInProgress()).toBe(true); + expect(rootScope.$broadcast).toHaveBeenCalled(); + expect(eventName).toBe('adal:loginRedirect'); + }); + + it('tests auto id token renew when id token expires', function () { + spyOn(rootScope, '$broadcast').andCallThrough(); + + var loginResourceOldValue = adalServiceProvider.config.loginResource; + adalServiceProvider.config.loginResource = null; + window.location.hash = 'hash'; + var eventName = '', error = '', errorDesc = '', token = ''; + scope.$on('adal:loginFailure', function (event, valErrorDesc, valError) { + eventName = event.name; + errorDesc = valErrorDesc; + error = valError; + }); + + store = { + 'adal.idtoken': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJjbGllbnRpZDEyMyIsIm5hbWUiOiJKb2huIERvZSIsInVwbiI6ImpvaG5AZW1haWwuY29tIiwibm9uY2UiOm51bGx9.DLCO6yIWhnNBYfHH8qFPswcH4M2Alpjn6AZy7K6HENY' + } + scope.$apply(); + + adalServiceProvider.config.loginResource = loginResourceOldValue; + expect(rootScope.$broadcast).toHaveBeenCalled(); + expect(eventName).toBe('adal:loginFailure'); + expect(errorDesc).toBe('resource is required'); + expect(error).toBe('resource is required'); + adalServiceProvider.logOut(); + }); +}); diff --git a/tests/testApp.js b/tests/testApp.js index 0917072c..0180a0a9 100644 --- a/tests/testApp.js +++ b/tests/testApp.js @@ -36,6 +36,10 @@ app.config(['$httpProvider', '$routeProvider', 'adalAuthenticationServiceProvide template: '
todoList
', requireADLogin: true }). + when('/login', { + controller: 'loginController', + templateUrl: 'login.html', + }). otherwise({ redirectTo: '/home' }); var endpoints = { @@ -50,7 +54,7 @@ app.config(['$httpProvider', '$routeProvider', 'adalAuthenticationServiceProvide clientId: 'clientid123', loginResource: 'loginResource123', redirectUri: 'https://myapp.com/page', - endpoints: endpoints // optional + endpoints: endpoints, // optional }, $httpProvider // pass http provider to inject request interceptor to attach tokens ); diff --git a/tests/unit/spec/AdalSpec.js b/tests/unit/spec/AdalSpec.js index bf4e7a99..e94ea224 100644 --- a/tests/unit/spec/AdalSpec.js +++ b/tests/unit/spec/AdalSpec.js @@ -86,6 +86,11 @@ describe('Adal', function () { store[key] = value; } }, + removeItem: function (key) { + if (typeof store[key] != 'undefined') { + delete store[key]; + } + }, clear: function () { store = {}; }, @@ -203,25 +208,29 @@ describe('Adal', function () { it('returns from cache for auto renewable if not expired', function () { adal.config.expireOffsetSeconds = SECONDS_TO_EXPIRE - 100; - var err = ''; - var token = ''; - var callback = function (valErr, valToken) { - err = valErr; + var errDesc = '', token = '', err = ''; + var callback = function (valErrDesc, valToken, valErr) { + errDesc = valErrDesc; token = valToken; + err = valErr; }; adal.acquireToken(RESOURCE1, callback); expect(token).toBe('access_token_in_cache' + RESOURCE1); + expect(errDesc).toBe(null); + expect(err).toBe(null); }); it('returns error for acquireToken without resource', function () { adal.config.expireOffsetSeconds = SECONDS_TO_EXPIRE - 100; - var err = ''; - var token = ''; - var callback = function (valErr, valToken) { - err = valErr; + var errDesc = '', token = '', err = ''; + var callback = function (valErrDesc, valToken, valErr) { + errDesc = valErrDesc; token = valToken; + err = valErr; }; adal.acquireToken(null, callback); + expect(errDesc).toBe('resource is required'); + expect(token).toBe(null); expect(err).toBe('resource is required'); }); @@ -229,17 +238,16 @@ describe('Adal', function () { adal.config.redirectUri = 'contoso_site'; adal.config.clientId = 'client'; adal.config.expireOffsetSeconds = SECONDS_TO_EXPIRE + 100; - var err = ''; - var token = ''; - var callback = function (valErr, valToken) { - err = valErr; + var errDesc = '', token = '', err = ''; + var callback = function (valErrDesc, valToken, valErr) { + errDesc = valErrDesc; token = valToken; + err = valErr; }; adal._renewStates = []; adal._user = { profile: { 'upn': 'test@testuser.com' }, userName: 'test@domain.com' }; adal.acquireToken(RESOURCE1, callback); expect(adal.callback).toBe(null); - 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'); @@ -255,29 +263,27 @@ describe('Adal', function () { }); //Necessary for integration with Angular when multiple http calls are queued. - it('allows multiple callers to be notified when the token is renewed', function () { + it('allows multiple callers to be notified when the token is renewed. Also checks if all registered acquireToken callbacks are called in the case when one of the callbacks throws an error', function () { adal.config.redirectUri = 'contoso_site'; adal.config.clientId = 'client'; adal.config.expireOffsetSeconds = SECONDS_TO_EXPIRE + 100; - var err = null; - var token = null; - var err2 = null; - var token2 = null; - var callback = function (valErr, valToken) { - err = valErr; + var errDesc = '', token = '', err = ''; + var errDesc2 = '', token2 = '', err2 = ''; + var callback = function (valErrDesc, valToken, valErr) { + errDesc = valErrDesc; token = valToken; + err = valErr; }; - var callback2 = function (valErr, valToken) { - err2 = valErr; + var callback2 = function (valErrDesc, valToken, valErr) { + errDesc2 = valErrDesc; token2 = valToken; + err2 = valErr; }; - adal._renewStates = []; 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); - 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'); @@ -292,11 +298,14 @@ describe('Adal', function () { //Simulate callback from the frame. //adal.callback(null, '33333333-3333-4333-b333-333333333333'); - window.callBackMappedToRenewStates[adal.config.state](null, '33333333-3333-4333-b333-333333333333'); + window.callBackMappedToRenewStates[adal.config.state](null, '33333333-3333-4333-b333-333333333333', null); //Both callbacks should have been provided with the token. expect(token).toBe('33333333-3333-4333-b333-333333333333', 'First callback should be called'); + expect(errDesc).toBe(null); + expect(err).toBe(null); expect(token2).toBe('33333333-3333-4333-b333-333333333333', 'Second callback should be called'); - + expect(errDesc2).toBe(null); + expect(err2).toBe(null); }); it('check guid masking', function () { @@ -410,13 +419,22 @@ describe('Adal', function () { expect(adal.promptUser).toHaveBeenCalledWith(DEFAULT_INSTANCE + 'common/oauth2/logout?post_logout_redirect_uri=https%3A%2F%2Fcontoso.com%2Flogout'); }); + it('uses logout uri if given', function () { + storageFake.setItem(adal.CONSTANTS.STORAGE.USERNAME, 'test user'); + adal.config.displayCall = null; + adal.config.clientId = 'client'; + adal.config.logOutUri = 'https://login.microsoftonline.com/adfs/ls/?wa=wsignout1.0' + spyOn(adal, 'promptUser'); + adal.logOut(); + expect(adal.promptUser).toHaveBeenCalledWith('https://login.microsoftonline.com/adfs/ls/?wa=wsignout1.0'); + }) + it('gets user from cache', function () { storageFake.setItem(adal.CONSTANTS.STORAGE.IDTOKEN, IDTOKEN_MOCK); adal.config.clientId = 'e9a5a8b6-8af7-4719-9821-0deef255f68e'; adal.config.loginResource = RESOURCE1; adal.config.expireOffsetSeconds = SECONDS_TO_EXPIRE - 100; - var err = ''; - var user = {}; + var err = '', user = ''; var callback = function (valErr, valResult) { err = valErr; user = valResult; @@ -673,11 +691,11 @@ describe('Adal', function () { 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; + var errDesc = '', token = '', err = ''; + var callback = function (valErrDesc, valToken, valErr) { + errDesc = valErrDesc; token = valToken; + err = valErr; }; adal._renewStates = []; adal._user = { userName: 'test@testuser.com' }; @@ -687,8 +705,9 @@ describe('Adal', function () { }, '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(errDesc).toBe('Token renewal operation failed due to timeout'); expect(token).toBe(null); + expect(err).toBe('Token Renewal Failed'); }); }); @@ -698,11 +717,11 @@ describe('Adal', function () { 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; + var errDesc = '', token = '', err = ''; + var callback = function (valErrDesc, valToken, valErr) { + errDesc = valErrDesc; token = valToken; + err = valErr; }; adal._renewStates = []; adal._user = { profile: { 'upn': 'test@testuser.com' }, userName: 'test@domain.com' }; @@ -710,7 +729,6 @@ describe('Adal', function () { 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 () { @@ -735,17 +753,18 @@ describe('Adal', function () { requestType: adal.REQUEST_TYPE.RENEW_TOKEN }; }; - var err = ''; - var token = ''; - var callback = function (valErr, valToken) { - err = valErr; + var errDesc = '', token = '', err = ''; + var callback = function (valErrDesc, valToken, valErr) { + errDesc = valErrDesc; token = valToken; + err = valErr; }; window.parent = {}; window.parent.callBackMappedToRenewStates = {}; window.parent.callBackMappedToRenewStates[adal.getRequestInfo().stateResponse] = callback; adal.handleWindowCallback(); - expect(err).toBe('error description'); + expect(errDesc).toBe('error description'); + expect(err).toBe('invalid'); expect(token).toBe(IDTOKEN_MOCK); adal.getRequestInfo = _getRequestInfo; @@ -862,18 +881,19 @@ describe('Adal', function () { adal.popUp = true; adal.config.clientId = 'client'; adal.config.redirectUri = 'contoso_site'; - var err; - var token; - var callback = function (valErr, valToken) { - err = valErr; + var errDesc = '', token = '', err = ''; + var callback = function (valErrDesc, valToken, valErr) { + errDesc = valErrDesc; token = valToken; + err = valErr; }; window.open = function () { return null; } adal.callback = callback; adal.login(); - expect(err).toBe('Popup Window is null. This can happen if you are using IE'); + expect(errDesc).toBe('Popup Window is null. This can happen if you are using IE'); + expect(err).toBe('Error opening popup'); expect(token).toBe(null); expect(adal.loginInProgress()).toBe(false); }); @@ -902,11 +922,11 @@ describe('Adal', function () { }; return popupWindow; }; - var err; - var token; - var callback = function (valErr, valToken) { - err = valErr; + var errDesc = '', token = '', err = ''; + var callback = function (valErrDesc, valToken, valErr) { + errDesc = valErrDesc; token = valToken; + err = valErr; }; adal.callback = callback; mathMock.random = function () { @@ -922,6 +942,8 @@ describe('Adal', function () { runs(function () { expect(adal.loginInProgress()).toBe(false); expect(token).toBe(IDTOKEN_MOCK); + expect(err).toBe('invalid id_token'); + expect(errDesc).toBe('Invalid id_token. id_token: ' + IDTOKEN_MOCK); expect(window.location.href).not.toBe('home page'); }); @@ -944,7 +966,7 @@ describe('Adal', function () { }); it('tests _guid function if window.crypto is defined in the browser', function () { - var buffer = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]; + var buffer = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; window.msCrypto = null; window.crypto = { getRandomValues: function (_buffer) { @@ -956,6 +978,26 @@ describe('Adal', function () { expect(adal._guid()).toBe('00010203-0405-4607-8809-0a0b0c0d0e0f'); window.crypto = null; }); - // TODO angular intercepptor - // TODO angular authenticationService + + it('tests if error parameter is passed to acquireToken callback', function () { + var errorHash = '#error=interaction_required&error_description=some_description&state=someState'; + var errDesc = '', token = '', err = ''; + var callback = function (valErrDesc, valToken, valErr) { + errDesc = valErrDesc; + token = valToken; + err = valErr; + } + window.parent = { + AuthenticationContext: function () { + return { + _renewStates: ['someState'] + } + }, + callBackMappedToRenewStates: { "someState": callback } + }; + adal.handleWindowCallback(errorHash); + expect(err).toBe('interaction_required'); + expect(token).toBe(undefined); + expect(errDesc).toBe('some_description'); + }); });