/* oauth-ng - v0.4.2 - 2015-08-27 */ 'use strict'; // App libraries angular.module('oauth', [ 'oauth.directive', // login directive 'oauth.accessToken', // access token service 'oauth.endpoint', // oauth endpoint service 'oauth.profile', // profile model 'oauth.storage', // storage 'oauth.interceptor', // bearer token interceptor 'oauth.configuration' // token appender ]) .config(['$locationProvider','$httpProvider', function($locationProvider, $httpProvider) { $httpProvider.interceptors.push('ExpiredInterceptor'); }]); 'use strict'; var accessTokenService = angular.module('oauth.accessToken', []); accessTokenService.factory('AccessToken', ['Storage', '$rootScope', '$location', '$interval', function(Storage, $rootScope, $location, $interval){ var service = { token: null }, oAuth2HashTokens = [ //per http://tools.ietf.org/html/rfc6749#section-4.2.2 'access_token', 'token_type', 'expires_in', 'scope', 'state', 'error','error_description' ]; /** * 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(); } 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); } }; /* * * * * * * * * * * 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]); } 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 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; } var time = (new Date(service.token.expires_at))-(new Date()); if(time && time > 0){ $interval(function(){ $rootScope.$broadcast('oauth:expired', service.token); }, time, 1); } }; /** * Remove the oAuth2 pieces from the hash fragment */ var removeFragment = function(){ var curHash = $location.hash(); angular.forEach(oAuth2HashTokens,function(hashKey){ var re = new RegExp('&'+hashKey+'(=[^&]*)?|^'+hashKey+'(=[^&]*)?&?'); curHash = curHash.replace(re,''); }); $location.hash(curHash); }; return service; }]); 'use strict'; var endpointClient = angular.module('oauth.endpoint', []); endpointClient.factory('Endpoint', function() { var service = {}; /* * Defines the authorization URL */ service.set = function(configuration) { this.config = configuration; return this.get(); }; /* * Returns the authorization URL */ service.get = function( overrides ) { var params = angular.extend( {}, service.config, overrides); var oAuthScope = (params.scope) ? encodeURIComponent(params.scope) : '', state = (params.state) ? encodeURIComponent(params.state) : '', authPathHasQuery = (params.authorizePath.indexOf('?') === -1) ? false : true, appendChar = (authPathHasQuery) ? '&' : '?', //if authorizePath has ? already append OAuth2 params responseType = (params.responseType) ? encodeURIComponent(params.responseType) : ''; var url = params.site + params.authorizePath + appendChar + 'response_type=' + responseType + '&' + 'client_id=' + encodeURIComponent(params.clientId) + '&' + 'redirect_uri=' + encodeURIComponent(params.redirectUri) + '&' + 'scope=' + oAuthScope + '&' + 'state=' + state; if( params.nonce ) { url = url + '&nonce=' + params.nonce; } return url; }; /* * Redirects the app to the authorization URL */ service.redirect = function( overrides ) { var targetLocation = this.get( overrides ); window.location.replace(targetLocation); }; return service; }); 'use strict'; var profileClient = angular.module('oauth.profile', []); profileClient.factory('Profile', ['$http', 'AccessToken', '$rootScope', function($http, AccessToken, $rootScope) { var service = {}; var profile; service.find = function(uri) { var promise = $http.get(uri, { headers: headers() }); promise.success(function(response) { profile = response; $rootScope.$broadcast('oauth:profile', profile); }); return promise; }; service.get = function() { return profile; }; service.set = function(resource) { profile = resource; return profile; }; var headers = function() { return { Authorization: 'Bearer ' + AccessToken.get().access_token }; }; return service; }]); 'use strict'; var storageService = angular.module('oauth.storage', ['ngStorage']); storageService.factory('Storage', ['$rootScope', '$sessionStorage', '$localStorage', function($rootScope, $sessionStorage, $localStorage){ var service = { storage: $sessionStorage // By default }; /** * Deletes the item from storage, * Returns the item's previous value */ service.delete = function (name) { var stored = this.get(name); delete this.storage[name]; return stored; }; /** * Returns the item from storage */ service.get = function (name) { return this.storage[name]; }; /** * Sets the item in storage to the value specified * Returns the item's value */ service.set = function (name, value) { this.storage[name] = value; return this.get(name); }; /** * Change the storage service being used */ service.use = function (storage) { if (storage === 'sessionStorage') { this.storage = $sessionStorage; } else if (storage === 'localStorage') { this.storage = $localStorage; } }; return service; }]); 'use strict'; var oauthConfigurationService = angular.module('oauth.configuration', []); oauthConfigurationService.provider('OAuthConfiguration', function() { var _config = {}; this.init = function(config, httpProvider) { _config.protectedResources = config.protectedResources || []; httpProvider.interceptors.push('AuthInterceptor'); }; this.$get = function() { return { getConfig: function() { return _config; } }; }; }) .factory('AuthInterceptor', function($q, $rootScope, OAuthConfiguration, AccessToken) { return { 'request': function(config) { OAuthConfiguration.getConfig().protectedResources.forEach(function(resource) { // If the url is one of the protected resources, we want to see if there's a token and then // add the token if it exists. if (config.url.indexOf(resource) > -1) { var token = AccessToken.get(); if (token) { config.headers.Authorization = 'Bearer ' + token.access_token; } } }); return config; } }; }); 'use strict'; var interceptorService = angular.module('oauth.interceptor', []); interceptorService.factory('ExpiredInterceptor', ['Storage', '$rootScope', function (Storage, $rootScope) { var service = {}; service.request = function(config) { var token = Storage.get('token'); if (token && expired(token)) { $rootScope.$broadcast('oauth:expired', token); } return config; }; var expired = function(token) { return (token && token.expires_at && new Date(token.expires_at) < new Date()); }; return service; }]); 'use strict'; var directives = angular.module('oauth.directive', []); directives.directive('oauth', [ 'AccessToken', 'Endpoint', 'Profile', 'Storage', '$location', '$rootScope', '$compile', '$http', '$templateCache', function(AccessToken, Endpoint, Profile, Storage, $location, $rootScope, $compile, $http, $templateCache) { var definition = { 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' } }; definition.link = function postLink(scope, element) { scope.show = 'none'; scope.$watch('clientId', function() { init(); }); var init = function() { initAttributes(); // sets defaults Storage.use(scope.storage);// set storage compile(); // compiles the desired layout Endpoint.set(scope); // sets the oauth authorization url 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) }; 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'; }; var compile = function() { $http.get(scope.template, { cache: $templateCache }).success(function(html) { element.html(html); $compile(element.contents())(scope); }); }; var initProfile = function(scope) { var token = AccessToken.get(); if (token && token.access_token && scope.profileUri) { Profile.find(scope.profileUri).success(function(response) { scope.profile = response; }); } }; var initView = function() { var token = AccessToken.get(); if (!token) { return loggedOut(); // without access token it's logged out } if (token.access_token) { return authorized(); // if there is the access token we are done } if (token.error) { return denied(); // if the request has been denied we fire the denied event } }; scope.login = function() { Endpoint.redirect(); }; scope.logout = function() { AccessToken.destroy(scope); $rootScope.$broadcast('oauth:logout'); loggedOut(); }; scope.$on('oauth:expired', function() { AccessToken.destroy(scope); scope.show = 'logged-out'; }); // user is authorized var authorized = function() { $rootScope.$broadcast('oauth:authorized', AccessToken.get()); scope.show = 'logged-in'; }; // set the oauth directive to the logged-out status var loggedOut = function() { $rootScope.$broadcast('oauth:loggedOut'); scope.show = 'logged-out'; }; // set the oauth directive to the denied status var denied = function() { scope.show = 'denied'; $rootScope.$broadcast('oauth:denied'); }; // Updates the template at runtime scope.$on('oauth:template:update', function(event, template) { scope.template = template; compile(scope); }); // Hack to update the directive content on logout // TODO think to a cleaner solution scope.$on('$routeChangeSuccess', function () { init(); }); }; return definition; } ]);