diff --git a/app/scripts/directives/oauth.js b/app/scripts/directives/oauth.js index a5d9690..91fd5d7 100644 --- a/app/scripts/directives/oauth.js +++ b/app/scripts/directives/oauth.js @@ -21,25 +21,27 @@ directives.directive('oauth', [ restrict: 'AE', replace: true, scope: { - site: '@', // (required) set the oauth server host (e.g. http://oauth.example.com) - clientId: '@', // (required) client id - redirectUri: '@', // (required) client redirect uri - responseType: '@', // (optional) response type, defaults to token (use 'token' for implicit flow and 'code' for authorization code flow - scope: '@', // (optional) scope - profileUri: '@', // (optional) user profile uri (e.g http://example.com/me) - template: '@', // (optional) template to render (e.g views/templates/default.html) - text: '@', // (optional) login text - authorizePath: '@', // (optional) authorization url - state: '@', // (optional) An arbitrary unique string created by your app to guard against Cross-site Request Forgery - storage: '@', // (optional) Store token in 'sessionStorage' or 'localStorage', defaults to 'sessionStorage' - nonce: '@', // (optional) Send nonce on auth request - // OpenID Connect extras, more details in id-token.js: - issuer: '@', // (optional for OpenID Connect) issuer of the id_token, should match the 'iss' claim in id_token payload - subject: '@', // (optional for OpenID Connect) subject of the id_token, should match the 'sub' claim in id_token payload - pubKey: '@', // (optional for OpenID Connect) the public key(RSA public key or X509 certificate in PEM format) to verify the signature - wellKnown: '@', // (optional for OpenID Connect) whether to load public key according to .well-known/openid-configuration endpoint - logoutPath: '@', // (optional) A url to go to at logout - sessionPath: '@' // (optional) A url to use to check the validity of the current token. + site: '@', // (required) set the oauth server host (e.g. http://oauth.example.com) + clientId: '@', // (required) client id + redirectUri: '@', // (required) client redirect uri + responseType: '@', // (optional) response type, defaults to token (use 'token' for implicit flow, 'code' for authorization code flow and 'password' for resource owner password + scope: '@', // (optional) scope + profileUri: '@', // (optional) user profile uri (e.g http://example.com/me) + template: '@', // (optional) template to render (e.g views/templates/default.html) + text: '@', // (optional) login text + authorizePath: '@', // (optional) authorization url + tokenPath: '@', // (optional) token url + state: '@', // (optional) An arbitrary unique string created by your app to guard against Cross-site Request Forgery + storage: '@', // (optional) Store token in 'sessionStorage' or 'localStorage', defaults to 'sessionStorage' + nonce: '@', // (optional) Send nonce on auth request + // OpenID Connect extras, more details in id-token.js: + issuer: '@', // (optional for OpenID Connect) issuer of the id_token, should match the 'iss' claim in id_token payload + subject: '@', // (optional for OpenID Connect) subject of the id_token, should match the 'sub' claim in id_token payload + pubKey: '@', // (optional for OpenID Connect) the public key(RSA public key or X509 certificate in PEM format) to verify the signature + wellKnown: '@', // (optional for OpenID Connect) whether to load public key according to .well-known/openid-configuration endpoint + logoutPath: '@', // (optional) A url to go to at logout + sessionPath: '@', // (optional) A url to use to check the validity of the current token. + disableCheckSession:'@' // (optional) can current token be checked ? } }; @@ -58,22 +60,29 @@ directives.directive('oauth', [ OidcConfig.load(scope) // loads OIDC configuration from .well-known/openid-configuration if necessary .then(function() { IdToken.set(scope); - AccessToken.set(scope); // sets the access token object (if existing, from fragment or session) - initProfile(scope); // gets the profile resource (if existing the access token) - initView(); // sets the view (logged in or out) - checkValidity(); // ensure the validity of the current token + AccessToken.set(scope).then(function () { // sets the access token object (if existing, from fragment or session) + }) + ["finally"](function () { + initProfile(scope); // gets the profile resource (if existing the access token) + initView(); // sets the view (logged in or out) + checkValidity(); // ensure the validity of the current token + }); }); }; var initAttributes = function() { - scope.authorizePath = scope.authorizePath || '/oauth/authorize'; - scope.tokenPath = scope.tokenPath || '/oauth/token'; - scope.template = scope.template || 'views/templates/default.html'; - scope.responseType = scope.responseType || 'token'; - scope.text = scope.text || 'Sign In'; - scope.state = scope.state || undefined; - scope.scope = scope.scope || undefined; - scope.storage = scope.storage || 'sessionStorage'; + scope.authorizePath = scope.authorizePath || '/oauth/authorize'; + scope.tokenPath = scope.tokenPath || '/oauth/token'; + scope.template = scope.template || 'views/templates/default.html'; + scope.responseType = scope.responseType || 'token'; + scope.text = scope.text || 'Sign In'; + scope.state = scope.state || undefined; + scope.scope = scope.scope || undefined; + scope.storage = scope.storage || 'sessionStorage'; + scope.disableCheckSession = scope.disableCheckSession || false; + scope.typedLogin = ""; + scope.typedPassword = ""; + scope.typedKeepConnection = false; }; var compile = function() { @@ -96,8 +105,8 @@ directives.directive('oauth', [ var initView = function () { var token = AccessToken.get(); - if (!token) { - return scope.login(); + if (!token && scope.responseType !== "password") { + return expired(); } // without access token it's logged out, so we attempt to log in if (AccessToken.expired()) { return expired(); @@ -115,12 +124,43 @@ directives.directive('oauth', [ }; scope.logout = function () { + scope.typedLogin = ""; + scope.typedPassword = ""; + scope.typedKeepConnection = false; Endpoint.logout(); $rootScope.$broadcast('oauth:loggedOut'); scope.show = 'logged-out'; + AccessToken.destroy(); + }; + + scope.checkPassword = function () { + $http({ + method: "POST", + url: scope.site + scope.tokenPath, + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + transformRequest: function(obj) { + var str = []; + for(var p in obj) + str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); + return str.join("&"); + }, + data: {grant_type: "password", username: scope.typedLogin, password: scope.typedPassword, scope: scope.scope} + }).then(function (result) { + if (scope.typedKeepConnection) { + AccessToken.setTokenFromPassword(scope, result.data, scope.typedLogin, scope.typedPassword, scope.scope); + } else { + AccessToken.setTokenFromPassword(scope, result.data); + scope.typedLogin = ""; + scope.typedPassword = ""; + scope.typedKeepConnection = false; + } + scope.show = "logged-in"; + }, function () { + $rootScope.$broadcast('oauth:denied'); + }); }; - scope.$on('oauth:expired',expired); + scope.$on('oauth:expired', expired); // user is authorized var authorized = function() { @@ -129,9 +169,13 @@ directives.directive('oauth', [ }; var expired = function() { - $rootScope.$broadcast('oauth:expired'); + scope.show = 'logged-out'; scope.logout(); }; + + scope.runExpired = function() { + expired(); + }; // set the oauth directive to the denied status var denied = function() { @@ -165,6 +209,7 @@ directives.directive('oauth', [ scope.$on('$stateChangeSuccess', function () { $timeout(refreshDirective); }); + }; return definition; diff --git a/app/scripts/services/access-token.js b/app/scripts/services/access-token.js index 7e34d9b..f5365ab 100644 --- a/app/scripts/services/access-token.js +++ b/app/scripts/services/access-token.js @@ -2,203 +2,373 @@ var accessTokenService = angular.module('oauth.accessToken', []); -accessTokenService.factory('AccessToken', ['Storage', '$rootScope', '$location', '$interval', '$timeout', 'IdToken', function(Storage, $rootScope, $location, $interval, $timeout, IdToken){ - - var service = { - token: null - }, - hashFragmentKeys = [ - //Oauth2 keys per http://tools.ietf.org/html/rfc6749#section-4.2.2 - 'access_token', 'token_type', 'expires_in', 'scope', 'state', - 'error','error_description', - //Additional OpenID Connect key per http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthResponse - 'id_token' - ]; - var expiresAtEvent = null; - - /** - * Returns the access token. - */ - service.get = function(){ - return this.token; - }; - - /** - * Sets and returns the access token. It tries (in order) the following strategies: - * - takes the token from the fragment URI - * - takes the token from the sessionStorage - */ - service.set = function(){ - this.setTokenFromString($location.hash()); - - //If hash is present in URL always use it, cuz its coming from oAuth2 provider redirect - if(null === service.token){ - setTokenFromSession(); - } +accessTokenService.factory('AccessToken', ['Storage', '$rootScope', '$http', '$q', '$location', '$interval', '$timeout', 'IdToken', function(Storage, $rootScope, $http, $q, $location, $interval, $timeout, IdToken) { - return this.token; - }; - - /** - * Delete the access token and remove the session. - * @returns {null} - */ - service.destroy = function(){ - Storage.delete('token'); - this.token = null; - return this.token; - }; - - /** - * Tells if the access token is expired. - */ - service.expired = function(){ - return (this.token && this.token.expires_at && new Date(this.token.expires_at) < new Date()); - }; - - /** - * Get the access token from a string and save it - * @param hash - */ - service.setTokenFromString = function(hash){ - var params = getTokenFromString(hash); - - if(params){ - removeFragment(); - setToken(params); - setExpiresAt(); - // We have to save it again to make sure expires_at is set - // and the expiry event is set up properly - setToken(this.token); - $rootScope.$broadcast('oauth:login', service.token); - } - }; - - /** - * updates the expiration of the token - */ - service.updateExpiry = function(newExpiresIn){ - this.token.expires_in = newExpiresIn; - setExpiresAt(); - }; - - /* * * * * * * * * * - * PRIVATE METHODS * - * * * * * * * * * */ - - /** - * Set the access token from the sessionStorage. - */ - var setTokenFromSession = function(){ - var params = Storage.get('token'); - if (params) { - setToken(params); - } - }; - - /** - * Set the access token. - * - * @param params - * @returns {*|{}} - */ - var setToken = function(params){ - service.token = service.token || {}; // init the token - angular.extend(service.token, params); // set the access token params - setTokenInSession(); // save the token into the session - setExpiresAtEvent(); // event to fire when the token expires - - return service.token; - }; - - /** - * Parse the fragment URI and return an object - * @param hash - * @returns {{}} - */ - var getTokenFromString = function(hash){ - var params = {}, - regex = /([^&=]+)=([^&]*)/g, - m; - - while ((m = regex.exec(hash)) !== null) { - params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]); - } + var service = { + token: null, + typedLogin: "", + typedPassword: "", + scope: "", + runExpired: null + }, + hashFragmentKeys = [ + //Oauth2 keys per http://tools.ietf.org/html/rfc6749#section-4.2.2 + 'access_token', 'token_type', 'expires_in', 'scope', 'state', + 'error', 'error_description', + //Additional OpenID Connect key per http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthResponse + 'id_token' + ]; + var expiresAtEvent = null; + var refreshTokenUri = null; - // OpenID Connect - if (params.id_token && !params.error) { - IdToken.validateTokensAndPopulateClaims(params); - return params; - } + /** + * Returns the access token. + */ + service.get = function() { + return this.token; + }; - // Oauth2 - if(params.access_token || params.error){ - return params; - } - }; - - /** - * Save the access token into the session - */ - var setTokenInSession = function(){ - Storage.set('token', service.token); - }; - - /** - * Set the access token expiration date (useful for refresh logics) - */ - var setExpiresAt = function(){ - if (!service.token) { - return; - } - if(typeof(service.token.expires_in) !== 'undefined' && service.token.expires_in !== null) { - var expires_at = new Date(); - expires_at.setSeconds(expires_at.getSeconds() + parseInt(service.token.expires_in)-60); // 60 seconds less to secure browser and response latency - service.token.expires_at = expires_at; - } - else { - service.token.expires_at = null; - } - }; + /** + * Sets and returns the access token. It tries (in order) the following strategies: + * - Get the token using the code in the url + * - takes the token from the fragment URI + * - takes the token from the sessionStorage + */ + service.set = function(scope) { + refreshTokenUri = scope.site + scope.tokenPath; + this.runExpired = scope.runExpired; + if ($location.search().code) { + return this.setTokenFromCode($location.search(), scope); + } - /** - * Set the timeout at which the expired event is fired - */ - var setExpiresAtEvent = function(){ - // Don't bother if there's no expires token - if (typeof(service.token.expires_at) === 'undefined' || service.token.expires_at === null) { - return; - } - cancelExpiresAtEvent(); - var time = (new Date(service.token.expires_at))-(new Date()); - if(time && time > 0 && time <= 2147483647){ - expiresAtEvent = $interval(function(){ - $rootScope.$broadcast('oauth:expired', service.token); - }, time, 1); + this.setTokenFromString($location.hash()); + + //If hash is present in URL always use it, cuz its coming from oAuth2 provider redirect + + var deferred = $q.defer(); + + if (this.token) { + deferred.resolve(this.token); + } else { + deferred.reject(); + } + + if (null === service.token) { + return setTokenFromSession(); + } else { + return deferred.promise; + } + }; + + service.setTokenFromPassword = function(scope, token, typedLogin, typedPassword, oauthScope) { + this.runExpired = scope.runExpired; + if (typedLogin && typedPassword && oauthScope) { + service.typedLogin = typedLogin; + service.typedPassword = typedPassword; + service.scope = oauthScope; + } + setToken(token); + $rootScope.$broadcast('oauth:login', token); } - }; - var cancelExpiresAtEvent = function() { - if(expiresAtEvent) { - $timeout.cancel(expiresAtEvent); - expiresAtEvent = undefined; + /** + * Delete the access token and remove the session. + * @returns {null} + */ + service.destroy = function() { + cancelExpiresAtEvent(); + Storage.delete('token'); + this.token = null; + return this.token; + }; + + /** + * Tells if the access token is expired. + */ + service.expired = function() { + return (this.token && this.token.expires_at && new Date(this.token.expires_at) < new Date()); + }; + + service.setTokenFromCode = function(search, scope) { + return $http({ + method: "POST", + url: scope.site + scope.tokenPath, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + transformRequest: function(obj) { + var str = []; + for (var p in obj) + str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); + return str.join("&"); + }, + data: { + grant_type: "authorization_code", + code: search.code, + redirect_uri: scope.redirectUri, + client_id: scope.clientId + } + }).then(function(result) { + setToken(result.data); + $rootScope.$broadcast('oauth:login', service.token); + $location.url(http://github.com/$location.path()); + }); } - }; - - /** - * Remove the oAuth2 pieces from the hash fragment - */ - var removeFragment = function(){ - var curHash = $location.hash(); - angular.forEach(hashFragmentKeys,function(hashKey){ - var re = new RegExp('&'+hashKey+'(=[^&]*)?|^'+hashKey+'(=[^&]*)?&?'); - curHash = curHash.replace(re,''); - }); - - $location.hash(curHash); - }; - - return service; + + /** + * Get the access token from a string and save it + * @param hash + */ + service.setTokenFromString = function(hash) { + var params = getTokenFromString(hash); + + if (params) { + removeFragment(); + setToken(params); + // We have to save it again to make sure expires_at is set + // and the expiry event is set up properly + setToken(this.token); + $rootScope.$broadcast('oauth:login', service.token); + } + }; + + /** + * updates the expiration of the token + */ + service.updateExpiry = function(newExpiresIn) { + this.token.expires_in = newExpiresIn; + setExpiresAt(); + }; + + service.forceRefresh = function(connect) { + return refreshToken(connect); + }; + + /* * * * * * * * * * + * PRIVATE METHODS * + * * * * * * * * * */ + + /** + * Set the access token from the sessionStorage. + */ + var setTokenFromSession = function() { + var params = Storage.get('token'); + if (params) { + setToken(params); + if (!params.refresh_token) { + var deferred = $q.defer(); + deferred.resolve(params); + $rootScope.$broadcast('oauth:login', params); + return deferred.promise; + } else { + return refreshToken(true); + } + } else { + var deferred = $q.defer(); + deferred.reject(); + return deferred.promise; + } + }; + + var refreshToken = function(connect) { + if (service.token && service.token.refresh_token) { + return $http({ + method: "POST", + url: refreshTokenUri, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + transformRequest: function(obj) { + var str = []; + for (var p in obj) + str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); + return str.join("&"); + }, + data: { + grant_type: "refresh_token", + refresh_token: service.token.refresh_token + } + }).then(function(result) { + angular.extend(service.token, result.data); + setExpiresAt(); + setTokenInSession(); + if (connect) { + $rootScope.$broadcast('oauth:login', service.token); + } else { + $rootScope.$broadcast('oauth:refresh', service.token); + } + return result.data; + }, function(error) { + if (!!service.typedLogin && !!service.typedPassword) { + return reconnect(); + } else { + if (error.status === 401 || error.status === 400) { + cancelExpiresAtEvent(); + Storage.delete('token'); + $rootScope.$broadcast('oauth:expired'); + service.runExpired(); + } + } + }); + } else { + var deferred = $q.defer(); + deferred.reject(); + return deferred.promise; + } + }; + + var reconnect = function() { + return $http({ + method: "POST", + url: refreshTokenUri, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + transformRequest: function(obj) { + var str = []; + for (var p in obj) + str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); + return str.join("&"); + }, + data: { + grant_type: "password", + username: service.typedLogin, + password: service.typedPassword, + scope: service.scope + } + }).then(function(result) { + angular.extend(service.token, result.data); + setTokenInSession(); + $rootScope.$broadcast('oauth:refresh', service.token); + }, function(error) { + if (!!service.typedLogin && !!service.typedPassword) { + return reconnect(); + } else { + if (error.status === 401 || error.status === 400) { + cancelExpiresAtEvent(); + Storage.delete('token'); + $rootScope.$broadcast('oauth:expired'); + service.runExpired(); + } + } + }); + }; + + /** + * Set the access token. + * + * @param params + * @returns {*|{}} + */ + var setToken = function(params) { + service.token = service.token || {}; // init the token + angular.extend(service.token, params); // set the access token params + setTokenInSession(); // save the token into the session + setExpiresAt(); + setExpiresAtEvent(); // event to fire when the token expires + + return service.token; + }; + + /** + * Parse the fragment URI and return an object + * @param hash + * @returns {{}} + */ + var getTokenFromString = function(hash) { + var params = {}, + regex = /([^&=]+)=([^&]*)/g, + m; + + while ((m = regex.exec(hash)) !== null) { + params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]); + } + + // OpenID Connect + if (params.id_token && !params.error) { + IdToken.validateTokensAndPopulateClaims(params); + return params; + } + + // Oauth2 + if (params.access_token || params.error) { + return params; + } + }; + + /** + * Save the access token into the session + */ + var setTokenInSession = function() { + Storage.set('token', service.token); + }; + + /** + * Set the access token expiration date (useful for refresh logics) + */ + var setExpiresAt = function() { + if (!service.token) { + return; + } + if (typeof(service.token.expires_in) !== 'undefined' && service.token.expires_in !== null) { + var expires_at = new Date(); + expires_at.setSeconds(expires_at.getSeconds() + parseInt(service.token.expires_in) - 60); // 60 seconds less to secure browser and response latency + service.token.expires_at = expires_at; + } else { + service.token.expires_at = null; + } + }; + + + /** + * Set the interval at which the expired event is fired + */ + var setExpiresAtEvent = function() { + // Don't bother if there's no expires token + if (typeof(service.token.expires_at) === 'undefined' || service.token.expires_at === null) { + return; + } + cancelExpiresAtEvent(); + var time = (new Date(service.token.expires_at)) - (new Date()); + if (time && time > 0 && time <= 2147483647) { + if (service.token.refresh_token) { + expiresAtEvent = $interval(function() { + refreshToken(); + }, time); + } else { + expiresAtEvent = $timeout(function() { + $rootScope.$broadcast('oauth:expired'); + service.runExpired(); + }, time, 1); + } + } + }; + + var cancelExpiresAtEvent = function() { + if (expiresAtEvent) { + if (service.token.refresh_token) { + $interval.cancel(expiresAtEvent); + } else { + $timeout.cancel(expiresAtEvent); + } + expiresAtEvent = undefined; + } + }; + + /** + * Remove the oAuth2 pieces from the hash fragment + */ + var removeFragment = function() { + var curHash = $location.hash(); + angular.forEach(hashFragmentKeys, function(hashKey) { + var re = new RegExp('&' + hashKey + '(=[^&]*)?|^' + hashKey + '(=[^&]*)?&?'); + curHash = curHash.replace(re, ''); + }); + + $location.hash(curHash); + }; + + return service; }]); diff --git a/app/scripts/services/endpoint.js b/app/scripts/services/endpoint.js index 6db8df6..f92fa5a 100644 --- a/app/scripts/services/endpoint.js +++ b/app/scripts/services/endpoint.js @@ -67,7 +67,7 @@ endpointClient.factory('Endpoint', ['$rootScope', 'AccessToken', '$q', '$http', */ service.checkValidity = function() { var params = service.config; - if( params.sessionPath ) { + if( params.sessionPath && !params.disableCheckSession ) { var token = AccessToken.get(); if( !token ) { return $q.reject("No token configured"); @@ -82,6 +82,8 @@ endpointClient.factory('Endpoint', ['$rootScope', 'AccessToken', '$q', '$http', return $q.reject("Server replied: token is invalid."); } }); + } else if (params.disableCheckSession) { + return true; } else { return $q.reject("You must give a :session-path param in order to validate the token.") } diff --git a/app/views/templates/button.html b/app/views/templates/button.html index ce7861a..f30d62d 100644 --- a/app/views/templates/button.html +++ b/app/views/templates/button.html @@ -1,5 +1,25 @@ - Login Button - Logout {{profile.email}} - Access denied. + Login Button + Logout {{profile.email}} + Access denied. + + + +
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
diff --git a/app/views/templates/default.html b/app/views/templates/default.html index dbd71f2..96dbe03 100644 --- a/app/views/templates/default.html +++ b/app/views/templates/default.html @@ -1,5 +1,25 @@ - {{text}} - Logout {{profile.email}} - Access denied. Try again. + {{text}} + Logout {{profile.email}} + Access denied. Try again. + + + +
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
diff --git a/dist/oauth-ng.js b/dist/oauth-ng.js index 785f8fa..6d3f630 100644 --- a/dist/oauth-ng.js +++ b/dist/oauth-ng.js @@ -1,4 +1,4 @@ -/* oauth-ng - v0.4.10 - 2016-05-25 */ +/* oauth-ng - v0.4.10 - 2018-06-30 */ 'use strict'; @@ -366,204 +366,374 @@ idTokenService.factory('IdToken', ['Storage', function(Storage) { var accessTokenService = angular.module('oauth.accessToken', []); -accessTokenService.factory('AccessToken', ['Storage', '$rootScope', '$location', '$interval', '$timeout', 'IdToken', function(Storage, $rootScope, $location, $interval, $timeout, IdToken){ +accessTokenService.factory('AccessToken', ['Storage', '$rootScope', '$http', '$q', '$location', '$interval', '$timeout', 'IdToken', function(Storage, $rootScope, $http, $q, $location, $interval, $timeout, IdToken) { + + var service = { + token: null, + typedLogin: "", + typedPassword: "", + scope: "", + runExpired: null + }, + hashFragmentKeys = [ + //Oauth2 keys per http://tools.ietf.org/html/rfc6749#section-4.2.2 + 'access_token', 'token_type', 'expires_in', 'scope', 'state', + 'error', 'error_description', + //Additional OpenID Connect key per http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthResponse + 'id_token' + ]; + var expiresAtEvent = null; + var refreshTokenUri = null; + + /** + * Returns the access token. + */ + service.get = function() { + return this.token; + }; - var service = { - token: null - }, - hashFragmentKeys = [ - //Oauth2 keys per http://tools.ietf.org/html/rfc6749#section-4.2.2 - 'access_token', 'token_type', 'expires_in', 'scope', 'state', - 'error','error_description', - //Additional OpenID Connect key per http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthResponse - 'id_token' - ]; - var expiresAtEvent = null; + /** + * Sets and returns the access token. It tries (in order) the following strategies: + * - Get the token using the code in the url + * - takes the token from the fragment URI + * - takes the token from the sessionStorage + */ + service.set = function(scope) { + refreshTokenUri = scope.site + scope.tokenPath; + this.runExpired = scope.runExpired; - /** - * Returns the access token. - */ - service.get = function(){ - return this.token; - }; + if ($location.search().code) { + return this.setTokenFromCode($location.search(), scope); + } - /** - * Sets and returns the access token. It tries (in order) the following strategies: - * - takes the token from the fragment URI - * - takes the token from the sessionStorage - */ - service.set = function(){ - this.setTokenFromString($location.hash()); + this.setTokenFromString($location.hash()); - //If hash is present in URL always use it, cuz its coming from oAuth2 provider redirect - if(null === service.token){ - setTokenFromSession(); - } + //If hash is present in URL always use it, cuz its coming from oAuth2 provider redirect - return this.token; - }; + var deferred = $q.defer(); - /** - * Delete the access token and remove the session. - * @returns {null} - */ - service.destroy = function(){ - Storage.delete('token'); - this.token = null; - return this.token; - }; + if (this.token) { + deferred.resolve(this.token); + } else { + deferred.reject(); + } - /** - * Tells if the access token is expired. - */ - service.expired = function(){ - return (this.token && this.token.expires_at && new Date(this.token.expires_at) < new Date()); - }; + if (null === service.token) { + return setTokenFromSession(); + } else { + return deferred.promise; + } + }; - /** - * Get the access token from a string and save it - * @param hash - */ - service.setTokenFromString = function(hash){ - var params = getTokenFromString(hash); - - if(params){ - removeFragment(); - setToken(params); - setExpiresAt(); - // We have to save it again to make sure expires_at is set - // and the expiry event is set up properly - setToken(this.token); - $rootScope.$broadcast('oauth:login', service.token); + service.setTokenFromPassword = function(scope, token, typedLogin, typedPassword, oauthScope) { + this.runExpired = scope.runExpired; + if (typedLogin && typedPassword && oauthScope) { + service.typedLogin = typedLogin; + service.typedPassword = typedPassword; + service.scope = oauthScope; + } + setToken(token); + $rootScope.$broadcast('oauth:login', token); } - }; - /** - * updates the expiration of the token - */ - service.updateExpiry = function(newExpiresIn){ - this.token.expires_in = newExpiresIn; - setExpiresAt(); - }; + /** + * Delete the access token and remove the session. + * @returns {null} + */ + service.destroy = function() { + cancelExpiresAtEvent(); + Storage.delete('token'); + this.token = null; + return this.token; + }; - /* * * * * * * * * * - * PRIVATE METHODS * - * * * * * * * * * */ + /** + * Tells if the access token is expired. + */ + service.expired = function() { + return (this.token && this.token.expires_at && new Date(this.token.expires_at) < new Date()); + }; - /** - * Set the access token from the sessionStorage. - */ - var setTokenFromSession = function(){ - var params = Storage.get('token'); - if (params) { - setToken(params); + service.setTokenFromCode = function(search, scope) { + return $http({ + method: "POST", + url: scope.site + scope.tokenPath, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + transformRequest: function(obj) { + var str = []; + for (var p in obj) + str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); + return str.join("&"); + }, + data: { + grant_type: "authorization_code", + code: search.code, + redirect_uri: scope.redirectUri, + client_id: scope.clientId + } + }).then(function(result) { + setToken(result.data); + $rootScope.$broadcast('oauth:login', service.token); + $location.url(http://github.com/$location.path()); + }); } - }; - /** - * Set the access token. - * - * @param params - * @returns {*|{}} - */ - var setToken = function(params){ - service.token = service.token || {}; // init the token - angular.extend(service.token, params); // set the access token params - setTokenInSession(); // save the token into the session - setExpiresAtEvent(); // event to fire when the token expires + /** + * Get the access token from a string and save it + * @param hash + */ + service.setTokenFromString = function(hash) { + var params = getTokenFromString(hash); + + if (params) { + removeFragment(); + setToken(params); + // We have to save it again to make sure expires_at is set + // and the expiry event is set up properly + setToken(this.token); + $rootScope.$broadcast('oauth:login', service.token); + } + }; - return service.token; - }; + /** + * updates the expiration of the token + */ + service.updateExpiry = function(newExpiresIn) { + this.token.expires_in = newExpiresIn; + setExpiresAt(); + }; - /** - * Parse the fragment URI and return an object - * @param hash - * @returns {{}} - */ - var getTokenFromString = function(hash){ - var params = {}, - regex = /([^&=]+)=([^&]*)/g, - m; + service.forceRefresh = function(connect) { + return refreshToken(connect); + }; - while ((m = regex.exec(hash)) !== null) { - params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]); - } + /* * * * * * * * * * + * PRIVATE METHODS * + * * * * * * * * * */ - // OpenID Connect - if (params.id_token && !params.error) { - IdToken.validateTokensAndPopulateClaims(params); - return params; - } + /** + * Set the access token from the sessionStorage. + */ + var setTokenFromSession = function() { + var params = Storage.get('token'); + if (params) { + setToken(params); + if (!params.refresh_token) { + var deferred = $q.defer(); + deferred.resolve(params); + $rootScope.$broadcast('oauth:login', params); + return deferred.promise; + } else { + return refreshToken(true); + } + } else { + var deferred = $q.defer(); + deferred.reject(); + return deferred.promise; + } + }; - // Oauth2 - if(params.access_token || params.error){ - return params; - } - }; + var refreshToken = function(connect) { + if (service.token && service.token.refresh_token) { + return $http({ + method: "POST", + url: refreshTokenUri, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + transformRequest: function(obj) { + var str = []; + for (var p in obj) + str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); + return str.join("&"); + }, + data: { + grant_type: "refresh_token", + refresh_token: service.token.refresh_token + } + }).then(function(result) { + angular.extend(service.token, result.data); + setExpiresAt(); + setTokenInSession(); + if (connect) { + $rootScope.$broadcast('oauth:login', service.token); + } else { + $rootScope.$broadcast('oauth:refresh', service.token); + } + return result.data; + }, function(error) { + if (!!service.typedLogin && !!service.typedPassword) { + return reconnect(); + } else { + if (error.status === 401 || error.status === 400) { + cancelExpiresAtEvent(); + Storage.delete('token'); + $rootScope.$broadcast('oauth:expired'); + service.runExpired(); + } + } + }); + } else { + var deferred = $q.defer(); + deferred.reject(); + return deferred.promise; + } + }; - /** - * Save the access token into the session - */ - var setTokenInSession = function(){ - Storage.set('token', service.token); - }; + var reconnect = function() { + return $http({ + method: "POST", + url: refreshTokenUri, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + transformRequest: function(obj) { + var str = []; + for (var p in obj) + str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); + return str.join("&"); + }, + data: { + grant_type: "password", + username: service.typedLogin, + password: service.typedPassword, + scope: service.scope + } + }).then(function(result) { + angular.extend(service.token, result.data); + setTokenInSession(); + $rootScope.$broadcast('oauth:refresh', service.token); + }, function(error) { + if (!!service.typedLogin && !!service.typedPassword) { + return reconnect(); + } else { + if (error.status === 401 || error.status === 400) { + cancelExpiresAtEvent(); + Storage.delete('token'); + $rootScope.$broadcast('oauth:expired'); + service.runExpired(); + } + } + }); + }; - /** - * Set the access token expiration date (useful for refresh logics) - */ - var setExpiresAt = function(){ - if (!service.token) { - return; - } - if(typeof(service.token.expires_in) !== 'undefined' && service.token.expires_in !== null) { - var expires_at = new Date(); - expires_at.setSeconds(expires_at.getSeconds() + parseInt(service.token.expires_in)-60); // 60 seconds less to secure browser and response latency - service.token.expires_at = expires_at; - } - else { - service.token.expires_at = null; - } - }; + /** + * Set the access token. + * + * @param params + * @returns {*|{}} + */ + var setToken = function(params) { + service.token = service.token || {}; // init the token + angular.extend(service.token, params); // set the access token params + setTokenInSession(); // save the token into the session + setExpiresAt(); + setExpiresAtEvent(); // event to fire when the token expires + + return service.token; + }; + /** + * Parse the fragment URI and return an object + * @param hash + * @returns {{}} + */ + var getTokenFromString = function(hash) { + var params = {}, + regex = /([^&=]+)=([^&]*)/g, + m; - /** - * Set the timeout at which the expired event is fired - */ - var setExpiresAtEvent = function(){ - // Don't bother if there's no expires token - if (typeof(service.token.expires_at) === 'undefined' || service.token.expires_at === null) { - return; - } - cancelExpiresAtEvent(); - var time = (new Date(service.token.expires_at))-(new Date()); - if(time && time > 0 && time <= 2147483647){ - expiresAtEvent = $interval(function(){ - $rootScope.$broadcast('oauth:expired', service.token); - }, time, 1); - } - }; + while ((m = regex.exec(hash)) !== null) { + params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]); + } - var cancelExpiresAtEvent = function() { - if(expiresAtEvent) { - $timeout.cancel(expiresAtEvent); - expiresAtEvent = undefined; - } - }; + // OpenID Connect + if (params.id_token && !params.error) { + IdToken.validateTokensAndPopulateClaims(params); + return params; + } - /** - * Remove the oAuth2 pieces from the hash fragment - */ - var removeFragment = function(){ - var curHash = $location.hash(); - angular.forEach(hashFragmentKeys,function(hashKey){ - var re = new RegExp('&'+hashKey+'(=[^&]*)?|^'+hashKey+'(=[^&]*)?&?'); - curHash = curHash.replace(re,''); - }); - - $location.hash(curHash); - }; + // Oauth2 + if (params.access_token || params.error) { + return params; + } + }; + + /** + * Save the access token into the session + */ + var setTokenInSession = function() { + Storage.set('token', service.token); + }; + + /** + * Set the access token expiration date (useful for refresh logics) + */ + var setExpiresAt = function() { + if (!service.token) { + return; + } + if (typeof(service.token.expires_in) !== 'undefined' && service.token.expires_in !== null) { + var expires_at = new Date(); + expires_at.setSeconds(expires_at.getSeconds() + parseInt(service.token.expires_in) - 60); // 60 seconds less to secure browser and response latency + service.token.expires_at = expires_at; + } else { + service.token.expires_at = null; + } + }; - return service; + + /** + * Set the interval at which the expired event is fired + */ + var setExpiresAtEvent = function() { + // Don't bother if there's no expires token + if (typeof(service.token.expires_at) === 'undefined' || service.token.expires_at === null) { + return; + } + cancelExpiresAtEvent(); + var time = (new Date(service.token.expires_at)) - (new Date()); + if (time && time > 0 && time <= 2147483647) { + if (service.token.refresh_token) { + expiresAtEvent = $interval(function() { + refreshToken(); + }, time); + } else { + expiresAtEvent = $timeout(function() { + $rootScope.$broadcast('oauth:expired'); + service.runExpired(); + }, time, 1); + } + } + }; + + var cancelExpiresAtEvent = function() { + if (expiresAtEvent) { + if (service.token.refresh_token) { + $interval.cancel(expiresAtEvent); + } else { + $timeout.cancel(expiresAtEvent); + } + expiresAtEvent = undefined; + } + }; + + /** + * Remove the oAuth2 pieces from the hash fragment + */ + var removeFragment = function() { + var curHash = $location.hash(); + angular.forEach(hashFragmentKeys, function(hashKey) { + var re = new RegExp('&' + hashKey + '(=[^&]*)?|^' + hashKey + '(=[^&]*)?&?'); + curHash = curHash.replace(re, ''); + }); + + $location.hash(curHash); + }; + + return service; }]); @@ -636,7 +806,7 @@ endpointClient.factory('Endpoint', ['$rootScope', 'AccessToken', '$q', '$http', */ service.checkValidity = function() { var params = service.config; - if( params.sessionPath ) { + if( params.sessionPath && !params.disableCheckSession ) { var token = AccessToken.get(); if( !token ) { return $q.reject("No token configured"); @@ -651,6 +821,8 @@ endpointClient.factory('Endpoint', ['$rootScope', 'AccessToken', '$q', '$http', return $q.reject("Server replied: token is invalid."); } }); + } else if (params.disableCheckSession) { + return true; } else { return $q.reject("You must give a :session-path param in order to validate the token.") } @@ -842,25 +1014,27 @@ directives.directive('oauth', [ restrict: 'AE', replace: true, scope: { - site: '@', // (required) set the oauth server host (e.g. http://oauth.example.com) - clientId: '@', // (required) client id - redirectUri: '@', // (required) client redirect uri - responseType: '@', // (optional) response type, defaults to token (use 'token' for implicit flow and 'code' for authorization code flow - scope: '@', // (optional) scope - profileUri: '@', // (optional) user profile uri (e.g http://example.com/me) - template: '@', // (optional) template to render (e.g bower_components/oauth-ng/dist/views/templates/default.html) - text: '@', // (optional) login text - authorizePath: '@', // (optional) authorization url - state: '@', // (optional) An arbitrary unique string created by your app to guard against Cross-site Request Forgery - storage: '@', // (optional) Store token in 'sessionStorage' or 'localStorage', defaults to 'sessionStorage' - nonce: '@', // (optional) Send nonce on auth request - // OpenID Connect extras, more details in id-token.js: - issuer: '@', // (optional for OpenID Connect) issuer of the id_token, should match the 'iss' claim in id_token payload - subject: '@', // (optional for OpenID Connect) subject of the id_token, should match the 'sub' claim in id_token payload - pubKey: '@', // (optional for OpenID Connect) the public key(RSA public key or X509 certificate in PEM format) to verify the signature - wellKnown: '@', // (optional for OpenID Connect) whether to load public key according to .well-known/openid-configuration endpoint - logoutPath: '@', // (optional) A url to go to at logout - sessionPath: '@' // (optional) A url to use to check the validity of the current token. + site: '@', // (required) set the oauth server host (e.g. http://oauth.example.com) + clientId: '@', // (required) client id + redirectUri: '@', // (required) client redirect uri + responseType: '@', // (optional) response type, defaults to token (use 'token' for implicit flow, 'code' for authorization code flow and 'password' for resource owner password + scope: '@', // (optional) scope + profileUri: '@', // (optional) user profile uri (e.g http://example.com/me) + template: '@', // (optional) template to render (e.g bower_components/oauth-ng/dist/views/templates/default.html) + text: '@', // (optional) login text + authorizePath: '@', // (optional) authorization url + tokenPath: '@', // (optional) token url + state: '@', // (optional) An arbitrary unique string created by your app to guard against Cross-site Request Forgery + storage: '@', // (optional) Store token in 'sessionStorage' or 'localStorage', defaults to 'sessionStorage' + nonce: '@', // (optional) Send nonce on auth request + // OpenID Connect extras, more details in id-token.js: + issuer: '@', // (optional for OpenID Connect) issuer of the id_token, should match the 'iss' claim in id_token payload + subject: '@', // (optional for OpenID Connect) subject of the id_token, should match the 'sub' claim in id_token payload + pubKey: '@', // (optional for OpenID Connect) the public key(RSA public key or X509 certificate in PEM format) to verify the signature + wellKnown: '@', // (optional for OpenID Connect) whether to load public key according to .well-known/openid-configuration endpoint + logoutPath: '@', // (optional) A url to go to at logout + sessionPath: '@', // (optional) A url to use to check the validity of the current token. + disableCheckSession:'@' // (optional) can current token be checked ? } }; @@ -879,22 +1053,29 @@ directives.directive('oauth', [ OidcConfig.load(scope) // loads OIDC configuration from .well-known/openid-configuration if necessary .then(function() { IdToken.set(scope); - AccessToken.set(scope); // sets the access token object (if existing, from fragment or session) - initProfile(scope); // gets the profile resource (if existing the access token) - initView(); // sets the view (logged in or out) - checkValidity(); // ensure the validity of the current token + AccessToken.set(scope).then(function () { // sets the access token object (if existing, from fragment or session) + }) + ["finally"](function () { + initProfile(scope); // gets the profile resource (if existing the access token) + initView(); // sets the view (logged in or out) + checkValidity(); // ensure the validity of the current token + }); }); }; var initAttributes = function() { - scope.authorizePath = scope.authorizePath || '/oauth/authorize'; - scope.tokenPath = scope.tokenPath || '/oauth/token'; - scope.template = scope.template || 'bower_components/oauth-ng/dist/views/templates/default.html'; - scope.responseType = scope.responseType || 'token'; - scope.text = scope.text || 'Sign In'; - scope.state = scope.state || undefined; - scope.scope = scope.scope || undefined; - scope.storage = scope.storage || 'sessionStorage'; + scope.authorizePath = scope.authorizePath || '/oauth/authorize'; + scope.tokenPath = scope.tokenPath || '/oauth/token'; + scope.template = scope.template || 'bower_components/oauth-ng/dist/views/templates/default.html'; + scope.responseType = scope.responseType || 'token'; + scope.text = scope.text || 'Sign In'; + scope.state = scope.state || undefined; + scope.scope = scope.scope || undefined; + scope.storage = scope.storage || 'sessionStorage'; + scope.disableCheckSession = scope.disableCheckSession || false; + scope.typedLogin = ""; + scope.typedPassword = ""; + scope.typedKeepConnection = false; }; var compile = function() { @@ -917,8 +1098,8 @@ directives.directive('oauth', [ var initView = function () { var token = AccessToken.get(); - if (!token) { - return scope.login(); + if (!token && scope.responseType !== "password") { + return expired(); } // without access token it's logged out, so we attempt to log in if (AccessToken.expired()) { return expired(); @@ -936,12 +1117,43 @@ directives.directive('oauth', [ }; scope.logout = function () { + scope.typedLogin = ""; + scope.typedPassword = ""; + scope.typedKeepConnection = false; Endpoint.logout(); $rootScope.$broadcast('oauth:loggedOut'); scope.show = 'logged-out'; + AccessToken.destroy(); + }; + + scope.checkPassword = function () { + $http({ + method: "POST", + url: scope.site + scope.tokenPath, + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + transformRequest: function(obj) { + var str = []; + for(var p in obj) + str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); + return str.join("&"); + }, + data: {grant_type: "password", username: scope.typedLogin, password: scope.typedPassword, scope: scope.scope} + }).then(function (result) { + if (scope.typedKeepConnection) { + AccessToken.setTokenFromPassword(scope, result.data, scope.typedLogin, scope.typedPassword, scope.scope); + } else { + AccessToken.setTokenFromPassword(scope, result.data); + scope.typedLogin = ""; + scope.typedPassword = ""; + scope.typedKeepConnection = false; + } + scope.show = "logged-in"; + }, function () { + $rootScope.$broadcast('oauth:denied'); + }); }; - scope.$on('oauth:expired',expired); + scope.$on('oauth:expired', expired); // user is authorized var authorized = function() { @@ -950,9 +1162,13 @@ directives.directive('oauth', [ }; var expired = function() { - $rootScope.$broadcast('oauth:expired'); + scope.show = 'logged-out'; scope.logout(); }; + + scope.runExpired = function() { + expired(); + }; // set the oauth directive to the denied status var denied = function() { @@ -986,6 +1202,7 @@ directives.directive('oauth', [ scope.$on('$stateChangeSuccess', function () { $timeout(refreshDirective); }); + }; return definition; diff --git a/dist/views/templates/button.html b/dist/views/templates/button.html index ce7861a..f30d62d 100644 --- a/dist/views/templates/button.html +++ b/dist/views/templates/button.html @@ -1,5 +1,25 @@ - Login Button - Logout {{profile.email}} - Access denied. + Login Button + Logout {{profile.email}} + Access denied. + + + +
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
diff --git a/dist/views/templates/default.html b/dist/views/templates/default.html index dbd71f2..96dbe03 100644 --- a/dist/views/templates/default.html +++ b/dist/views/templates/default.html @@ -1,5 +1,25 @@ - {{text}} - Logout {{profile.email}} - Access denied. Try again. + {{text}} + Logout {{profile.email}} + Access denied. Try again. + + + +
+
+
+ + +
+
+ + +
+
+ + +
+ +
+