From 8f7d1dea291421ab0f21097bb147d132233d6382 Mon Sep 17 00:00:00 2001 From: Maciek Ruckgaber Date: Mon, 9 Dec 2013 11:06:07 -0500 Subject: [PATCH 1/8] Adding state param --- src/js/angularOauth.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/js/angularOauth.js b/src/js/angularOauth.js index d244c63..240bbf2 100644 --- a/src/js/angularOauth.js +++ b/src/js/angularOauth.js @@ -3,7 +3,7 @@ angular.module('angularOauth', []). - provider('Token', function() { + provider('Token', function($log) { /** * Given an flat object, returns a query string for use in URLs. Note @@ -67,12 +67,17 @@ angular.module('angularOauth', []). var getParams = function() { // TODO: Facebook uses comma-delimited scopes. This is not compliant with section 3.3 but perhaps support later. + // Send a state to authorization endpoint + // this state should be sent back from the endpoint and should + // match the original value + $rootScope.oauth_state = Math.Random() + new Date.getTime(); return { response_type: RESPONSE_TYPE, client_id: config.clientId, redirect_uri: config.redirectUri, - scope: config.scopes.join(" ") + scope: config.scopes.join(" "), + state: $rootScope.oauth_state } }; @@ -168,7 +173,9 @@ angular.module('angularOauth', []). // TODO: binding occurs for each reauthentication, leading to leaks for long-running apps. angular.element($window).bind('message', function(event) { - if (event.source == popup && event.origin == window.location.origin) { + if (event.source == popup && event.origin == window.location.origin && + event.state == $rootScope.oauth_state) { + $log.log('State', $rootScope.oauth_state, event.state); $rootScope.$apply(function() { if (event.data.access_token) { deferred.resolve(event.data) From 50cdca74f5d882eae976e3e005f114888b424030 Mon Sep 17 00:00:00 2001 From: Maciek Ruckgaber Date: Mon, 9 Dec 2013 15:37:58 -0500 Subject: [PATCH 2/8] Fixed $log dep problem --- component.json | 2 +- src/js/angularOauth.js | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/component.json b/component.json index 5415b03..7e9c825 100644 --- a/component.json +++ b/component.json @@ -1,6 +1,6 @@ { "name": "angular-oauth", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "angular": ">= 1.1.4" }, diff --git a/src/js/angularOauth.js b/src/js/angularOauth.js index 240bbf2..2fe7dad 100644 --- a/src/js/angularOauth.js +++ b/src/js/angularOauth.js @@ -3,7 +3,7 @@ angular.module('angularOauth', []). - provider('Token', function($log) { + provider('Token', function() { /** * Given an flat object, returns a query string for use in URLs. Note @@ -175,7 +175,9 @@ angular.module('angularOauth', []). angular.element($window).bind('message', function(event) { if (event.source == popup && event.origin == window.location.origin && event.state == $rootScope.oauth_state) { - $log.log('State', $rootScope.oauth_state, event.state); + console.log('Event State'); + console.log(event.state); + console.log($rootScope.oauth_state); $rootScope.$apply(function() { if (event.data.access_token) { deferred.resolve(event.data) From 298f46a730bb2be0da1def00cb00122498480434 Mon Sep 17 00:00:00 2001 From: Maciek Ruckgaber Date: Tue, 10 Dec 2013 23:58:01 -0500 Subject: [PATCH 3/8] Callback function is a standard function with added state verification --- src/js/angularOauth.js | 56 ++++++----------------------------------- src/oauth2callback.html | 21 ++++++++++++---- 2 files changed, 24 insertions(+), 53 deletions(-) diff --git a/src/js/angularOauth.js b/src/js/angularOauth.js index 2fe7dad..a39d759 100644 --- a/src/js/angularOauth.js +++ b/src/js/angularOauth.js @@ -70,7 +70,7 @@ angular.module('angularOauth', []). // Send a state to authorization endpoint // this state should be sent back from the endpoint and should // match the original value - $rootScope.oauth_state = Math.Random() + new Date.getTime(); + $rootScope.oauth_state = Math.random() + new Date().getTime(); return { response_type: RESPONSE_TYPE, @@ -172,19 +172,15 @@ angular.module('angularOauth', []). // TODO: binding occurs for each reauthentication, leading to leaks for long-running apps. - angular.element($window).bind('message', function(event) { - if (event.source == popup && event.origin == window.location.origin && - event.state == $rootScope.oauth_state) { - console.log('Event State'); - console.log(event.state); - console.log($rootScope.oauth_state); - $rootScope.$apply(function() { - if (event.data.access_token) { - deferred.resolve(event.data) + window.setOauthParams = angular.bind(this, function(params) { + if(params.state == $rootScope.oauth_state){ + $rootScope.$apply(function(){ + if (params.access_token) { + deferred.resolve(params) } else { - deferred.reject(event.data) + deferred.reject(params) } - }) + }); } }); @@ -194,40 +190,4 @@ angular.module('angularOauth', []). } } } - }). - - /** - * A controller for the redirect endpoint that inspects the URL redirected to by the authorization server and sends - * it back to other windows using. - */ - controller('CallbackCtrl', function($scope, $location) { - - /** - * Parses an escaped url query string into key-value pairs. - * - * (Copied from Angular.js in the AngularJS project.) - * - * @returns Object.<(string|boolean)> - */ - function parseKeyValue(/**string*/keyValue) { - var obj = {}, key_value, key; - angular.forEach((keyValue || "").split('&'), function(keyValue){ - if (keyValue) { - key_value = keyValue.split('='); - key = decodeURIComponent(key_value[0]); - obj[key] = angular.isDefined(key_value[1]) ? decodeURIComponent(key_value[1]) : true; - } - }); - return obj; - } - - var queryString = $location.path().substring(1); // preceding slash omitted - var params = parseKeyValue(queryString); - - // TODO: The target origin should be set to an explicit origin. Otherwise, a malicious site that can receive - // the token if it manages to change the location of the parent. (See: - // https://developer.mozilla.org/en/docs/DOM/window.postMessage#Security_concerns) - - window.opener.postMessage(params, "*"); - window.close(); }); diff --git a/src/oauth2callback.html b/src/oauth2callback.html index 6c52618..e0cba11 100644 --- a/src/oauth2callback.html +++ b/src/oauth2callback.html @@ -1,13 +1,24 @@ - + Receiving authentication +

Finishing authentication...

- - - - \ No newline at end of file + From f286fdfd0ab45eda97b75cab2ad6734a18f2fe16 Mon Sep 17 00:00:00 2001 From: Maciek Ruckgaber Date: Wed, 11 Dec 2013 01:15:00 -0500 Subject: [PATCH 4/8] Added getTokenType() method Adding a stub for getTokenType() --- src/js/angularOauth.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/js/angularOauth.js b/src/js/angularOauth.js index a39d759..c3e560b 100644 --- a/src/js/angularOauth.js +++ b/src/js/angularOauth.js @@ -187,6 +187,10 @@ angular.module('angularOauth', []). // TODO: reject deferred if the popup was closed without a message being delivered + maybe offer a timeout return deferred.promise; + }, + + getTokenType: function(){ + return 'Bearer' } } } From 099a1c2416218e7b93782d6af311077722d38345 Mon Sep 17 00:00:00 2001 From: Maciek Ruckgaber Date: Wed, 22 Jan 2014 11:11:53 -0500 Subject: [PATCH 5/8] Implements load from localstorage Full token info is now stored in localStorage. If user reloads the page TokenProvider can be configured to load configuration from the localStorage info. --- README.rst | 14 ++++++++-- src/js/angularOauth.js | 63 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 0a62add..717bc20 100644 --- a/README.rst +++ b/README.rst @@ -12,9 +12,9 @@ Features ``Token.getTokenByPopup()`` function, that presents the user with the authorization endpoint and returns the token asynchronously. - (Implementation detail: A successfully obtained access token is handed back - to the parent window via ``window.opener.postMessage`` and the source and - origin of the sending window are verified by the parent.) +* __ Implementation detail:__ A successfully obtained access token is handed back + to the parent window verifying a random ``state`` variable generated at + token request. * Access token verification using ``Token.verifyAsync``, by requesting token information from the authorization server, verifying that the token @@ -24,6 +24,14 @@ Features * Storage and retrieval of tokens via the `Token.get` and `Token.set` calls in the `Token` service. +* TokenProvider configuratio can be loaded from localStorage at page reloads, by using:: + + .config(function(TokenProvider) { + TokenProvider.autoloadFromStorage(); + }); + +* Added Token expiration validation, via ``Token.hasValidToken()``. + * A preconfigured module for use with Google authentication. Check out the `example/js/demo.js `_ and `example/demo.html `_ for an example. diff --git a/src/js/angularOauth.js b/src/js/angularOauth.js index c3e560b..4fb9efa 100644 --- a/src/js/angularOauth.js +++ b/src/js/angularOauth.js @@ -26,6 +26,7 @@ angular.module('angularOauth', []). return str.join("&"); }; + // This response_type MUST be passed to the authorization endpoint using // the implicit grant flow (4.2.1 of RFC 6749). var RESPONSE_TYPE = 'token'; @@ -43,10 +44,38 @@ angular.module('angularOauth', []). scopes: [] }; + var getTokenFromStorage = function(){ + var tokenData = localStorage[config.localStorageName]; + if(tokenData){ + return JSON.parse(tokenData); + } + return null; + }; + + var storeTokenToStorage = function(tokenData){ + localStorage[config.localStorageName] = JSON.stringify(tokenData); + }; + + var isValidToken = function(tokenData){ + if(tokenData && tokenData.token && tokenData.expires){ + var now = new Date().getTime(); + return now <= tokenData.expires; + } + return false; + }; + this.extendConfig = function(configExtension) { config = angular.extend(config, configExtension); }; + // Attempt to load a previously saved config in localstorage + this.autoloadFromStorage = function(){ + var token = getTokenFromStorage(); + if(token && isValidToken(token)){ + this.extendConfig(token); + } + }; + this.$get = function($q, $http, $window, $rootScope) { var requiredAndMissing = []; angular.forEach(config, function(value, key) { @@ -91,16 +120,23 @@ angular.module('angularOauth', []). * @returns {string} The access token. */ get: function() { - return localStorage[config.localStorageName]; + return getTokenFromStorage(); }, /** * Persist the access token so that it can be retrieved later by. * - * @param accessToken + * @param accessToken (string) verified access token + * @param expiresIn (int) number of seconds until token expiration */ - set: function(accessToken) { - localStorage[config.localStorageName] = accessToken; + set: function(accessToken, expiresIn) { + var params = getParams(), data = {}; + data.client_id = params.client_id; + data.redirect_uri = params.redirect_uri; + data.scope = params.scope; + data.token = accessToken; + data.expires = new Date().getTime() + (parseInt(expiresIn) * 1000); + storeTokenToStorage(data); }, /** @@ -191,6 +227,25 @@ angular.module('angularOauth', []). getTokenType: function(){ return 'Bearer' + }, + + // Checks if the token is defined and has not expired yet + hasValidToken: function(){ + return isValidToken(getTokenFromStorage()); + }, + + // Utility function for getting the full string to be set + // on the Authorization header + getTokenAsString: function(){ + var token = getTokenFromStorage(); + if(isValidToken(token)){ + return 'Bearer ' + token.token; + } + return null; + }, + + unset: function(){ + localStorage.removeItem(config.localStorageName); } } } From 5612eb2b2e1b285f8a431dffac353c8c5f0175e5 Mon Sep 17 00:00:00 2001 From: Maciek Ruckgaber Date: Wed, 22 Jan 2014 19:43:17 -0500 Subject: [PATCH 6/8] Adds get token as auth header --- bower.json | 12 ++++++++++++ component.json | 12 ------------ src/js/angularOauth.js | 8 ++++++++ 3 files changed, 20 insertions(+), 12 deletions(-) create mode 100644 bower.json delete mode 100644 component.json diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..749db1f --- /dev/null +++ b/bower.json @@ -0,0 +1,12 @@ +{ + "name": "angular-oauth", + "version": "1.2.1", + "dependencies": { + "angular": ">= 1.2.0" + }, + "homepage": "https://github.com/maciekrb/angular-oauth", + "repository": { + "type": "git", + "url": "git://github.com/maciekrb/angular-oauth.git" + } +} diff --git a/component.json b/component.json deleted file mode 100644 index 7e9c825..0000000 --- a/component.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "angular-oauth", - "version": "1.0.1", - "dependencies": { - "angular": ">= 1.1.4" - }, - "homepage": "https://github.com/enginous/angular-oauth", - "repository": { - "type": "git", - "url": "git://github.com/enginous/angular-oauth.git" - } -} diff --git a/src/js/angularOauth.js b/src/js/angularOauth.js index 4fb9efa..a999733 100644 --- a/src/js/angularOauth.js +++ b/src/js/angularOauth.js @@ -243,6 +243,14 @@ angular.module('angularOauth', []). } return null; }, + + getAsHeaderConfig: function(){ + var token = getTokenFromStorage(), auth_header = {}; + if(isValidToken(token)){ + auth_header['Authorization'] = 'Bearer ' + token.token; + } + return auth_header; + }, unset: function(){ localStorage.removeItem(config.localStorageName); From 00203113626b3e2e19c65c4a37be8f4238218cdb Mon Sep 17 00:00:00 2001 From: Maciek Ruckgaber Date: Wed, 29 Jan 2014 19:27:17 -0500 Subject: [PATCH 7/8] Token now can be instanced without configuration --- src/js/angularOauth.js | 42 +++++++++++++++++++++++++++++++++++++----- src/js/googleOauth.js | 8 +++++--- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/js/angularOauth.js b/src/js/angularOauth.js index a999733..fedf3ac 100644 --- a/src/js/angularOauth.js +++ b/src/js/angularOauth.js @@ -1,3 +1,5 @@ +(function(){ + 'use strict'; @@ -47,7 +49,12 @@ angular.module('angularOauth', []). var getTokenFromStorage = function(){ var tokenData = localStorage[config.localStorageName]; if(tokenData){ - return JSON.parse(tokenData); + try { + return JSON.parse(tokenData); + } + catch(e){ + localStorage.removeItem(config.localStorageName); + } } return null; }; @@ -64,19 +71,24 @@ angular.module('angularOauth', []). return false; }; - this.extendConfig = function(configExtension) { + var extendConfig = function(configExtension){ config = angular.extend(config, configExtension); }; + this.extendConfig = function(configExtension){ + extendConfig(configExtension); + }; + // Attempt to load a previously saved config in localstorage this.autoloadFromStorage = function(){ var token = getTokenFromStorage(); if(token && isValidToken(token)){ - this.extendConfig(token); + extendConfig(token); } }; - this.$get = function($q, $http, $window, $rootScope) { + this.$get = ['$q', '$http', '$window', '$rootScope', function($q, $http, $window, $rootScope) { + /** var requiredAndMissing = []; angular.forEach(config, function(value, key) { if (value === REQUIRED_AND_MISSING) { @@ -93,6 +105,7 @@ angular.module('angularOauth', []). if (!config.clientId) { throw new Error("clientId needs to be configured using TokenProvider."); } + **/ var getParams = function() { // TODO: Facebook uses comma-delimited scopes. This is not compliant with section 3.3 but perhaps support later. @@ -114,6 +127,9 @@ angular.module('angularOauth', []). // TODO: get/set might want to support expiration to reauthenticate // TODO: check for localStorage support and otherwise perhaps use other methods of storing data (e.g. cookie) + extendConfig: function(config){ + extendConfig(config); + }, /** * Returns the stored access token. * @@ -177,6 +193,21 @@ angular.module('angularOauth', []). * `status`, `headers`, `config`). */ getTokenByPopup: function(extraParams, popupOptions) { + + var params = getParams(); + var requiredAndMissing = []; + angular.forEach(params, function(value, key) { + if (value === REQUIRED_AND_MISSING) { + requiredAndMissing.push(key); + } + }); + + if (requiredAndMissing.length) { + throw new Error("TokenProvider is insufficiently configured. Please " + + "configure the following options using " + + "TokenProvider.extendConfig: " + requiredAndMissing.join(", ")) + } + popupOptions = angular.extend({ name: 'AuthPopup', openParams: { @@ -256,5 +287,6 @@ angular.module('angularOauth', []). localStorage.removeItem(config.localStorageName); } } - } + }]; }); +})(); diff --git a/src/js/googleOauth.js b/src/js/googleOauth.js index d40d1e4..ad39326 100644 --- a/src/js/googleOauth.js +++ b/src/js/googleOauth.js @@ -1,3 +1,5 @@ +(function(){ + 'use strict'; /** @@ -38,11 +40,11 @@ angular.module('googleOauth', ['angularOauth']). }]); }). - config(function(TokenProvider, GoogleTokenVerifier) { + config(['TokenProvider', 'GoogleTokenVerifier', function(TokenProvider, GoogleTokenVerifier) { TokenProvider.extendConfig({ authorizationEndpoint: 'https://accounts.google.com/o/oauth2/auth', scopes: ["https://www.googleapis.com/auth/userinfo.email"], verifyFunc: GoogleTokenVerifier }); - }); - + }]); +})(); From 309b705fa35cfe7c4026e195a6bbc0874e82eeab Mon Sep 17 00:00:00 2001 From: Maciek Ruckgaber Date: Thu, 3 Apr 2014 20:15:16 -0500 Subject: [PATCH 8/8] Implemented WindowsLive authentication Created factories to config authentication --- src/js/angularOauth.js | 19 +-------- src/js/{googleOauth.js => google.js} | 33 +++++++++------ src/js/windowsid.js | 62 ++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 30 deletions(-) rename src/js/{googleOauth.js => google.js} (61%) create mode 100644 src/js/windowsid.js diff --git a/src/js/angularOauth.js b/src/js/angularOauth.js index fedf3ac..fc594dc 100644 --- a/src/js/angularOauth.js +++ b/src/js/angularOauth.js @@ -175,23 +175,6 @@ angular.module('angularOauth', []). return config.verifyFunc(config, accessToken); }, - /** - * Verifies an access token asynchronously. - * - * @param extraParams An access token received from the authorization server. - * @param popupOptions Settings for the display of the popup. - * @returns {Promise} Promise that will be resolved when the authorization server has verified that the - * token is valid, and we've verified that the token is passed back has audience that matches our client - * ID (to prevent the Confused Deputy Problem). - * - * If there's an error verifying the token, the promise is rejected with an object identifying the `name` error - * in the name member. The `name` can be either: - * - * - `invalid_audience`: The audience didn't match our client ID. - * - `error_response`: The server responded with an error, typically because the token was invalid. In this - * case, the callback parameters to `error` callback on `$http` are available in the object (`data`, - * `status`, `headers`, `config`). - */ getTokenByPopup: function(extraParams, popupOptions) { var params = getParams(); @@ -289,4 +272,4 @@ angular.module('angularOauth', []). } }]; }); -})(); +}()); diff --git a/src/js/googleOauth.js b/src/js/google.js similarity index 61% rename from src/js/googleOauth.js rename to src/js/google.js index ad39326..1c52d34 100644 --- a/src/js/googleOauth.js +++ b/src/js/google.js @@ -8,17 +8,34 @@ * * Guide: https://developers.google.com/accounts/docs/OAuth2UserAgent */ -angular.module('googleOauth', ['angularOauth']). +angular.module('oauth.google', ['angularOauth']) - constant('GoogleTokenVerifier', function(config, accessToken) { + .factory('GoogleTokenProvider', [ 'Token', 'GoogleTokenVerifier', function(Token, GoogleTokenVerifier){ + + // Configure the Google Token Provider with API Keys and scopes + return function(clientId, oAuthScopes, redirectUri){ + Token.extendConfig({ + authorizationEndpoint: 'https://accounts.google.com/o/oauth2/auth', + verifyFunc: GoogleTokenVerifier, + clientId: clientId, + redirectUri: redirectUri, + scopes: oAuthScopes + }); + return Token; + } + }]) + + .constant('GoogleTokenVerifier', function(config, accessToken) { var $injector = angular.injector(['ng']); return $injector.invoke(['$http', '$rootScope', '$q', function($http, $rootScope, $q) { var deferred = $q.defer(); var verificationEndpoint = 'https://www.googleapis.com/oauth2/v1/tokeninfo'; $rootScope.$apply(function() { + debug.debug("access token", accessToken); $http({method: 'GET', url: verificationEndpoint, params: {access_token: accessToken}}). success(function(data) { + debug.debug("data", data); if (data.audience == config.clientId) { deferred.resolve(data); } else { @@ -38,13 +55,5 @@ angular.module('googleOauth', ['angularOauth']). return deferred.promise; }]); - }). - - config(['TokenProvider', 'GoogleTokenVerifier', function(TokenProvider, GoogleTokenVerifier) { - TokenProvider.extendConfig({ - authorizationEndpoint: 'https://accounts.google.com/o/oauth2/auth', - scopes: ["https://www.googleapis.com/auth/userinfo.email"], - verifyFunc: GoogleTokenVerifier - }); - }]); -})(); + }); +}()); diff --git a/src/js/windowsid.js b/src/js/windowsid.js new file mode 100644 index 0000000..c33697d --- /dev/null +++ b/src/js/windowsid.js @@ -0,0 +1,62 @@ +(function(){ + +'use strict'; + +/** + * A module to include instead of `angularOauth` for a service preconfigured + * for Google OAuth authentication. + * + * Guide: http://msdn.microsoft.com/en-us/library/live/hh243647.aspx + */ +angular.module('oauth.windowsid', ['angularOauth']) + + .factory('WindowsidTokenProvider', [ 'Token', 'WindowsidTokenVerifier', function(Token, WindowsidTokenVerifier){ + + // Configure the WindowsId Token Provider with API Keys and scopes + return function(clientId, oAuthScopes, redirectUri){ + Token.extendConfig({ + authorizationEndpoint: 'https://login.live.com/oauth20_authorize.srf', + verifyFunc: WindowsidTokenVerifier, + clientId: clientId, + redirectUri: redirectUri, + scopes: oAuthScopes + }); + return Token; + }; + }]) + + .constant('WindowsidTokenVerifier', function(config, accessToken) { + var $injector = angular.injector(['ng']); + return $injector.invoke(['$http', '$rootScope', '$q', function($http, $rootScope, $q) { + var deferred = $q.defer(); + var verificationEndpoint = 'https://login.live.com/oauth20_token.srf'; + + $rootScope.$apply(function() { + //@TODO WindowsLive token validation ????? + /** + $http({method: 'GET', url: verificationEndpoint, params: {access_token: accessToken}}) + .success(function(data) { + debug.debug("Access token ID", accessToken); + debug.debug("Debugging windows ID", data); + if (data.audience == config.clientId) { + } else { + deferred.reject({name: 'invalid_audience'}); + } + }) + .error(function(data, status, headers, config) { + deferred.reject({ + name: 'error_response', + data: data, + status: status, + headers: headers, + config: config + }); + }); + **/ + deferred.resolve(accessToken); + }); + + return deferred.promise; + }]); + }); +}());