8000 Changes to oAuth callback and state verification by maciekrb · Pull Request #13 · angular-oauth/angular-oauth · GitHub
[go: up one dir, main page]

Skip to content

Changes to oAuth callback and state verification #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 <example/js/demo.js>`_ and
`example/demo.html <example/demo.html>`_ for an example.
Expand Down
12 changes: 12 additions & 0 deletions bower.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
12 changes: 0 additions & 12 deletions component.json

This file was deleted.

185 changes: 118 additions & 67 deletions src/js/angularOauth.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
(function(){

'use strict';


Expand Down Expand Up @@ -26,6 +28,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';
Expand All @@ -43,11 +46,49 @@ angular.module('angularOauth', []).
scopes: []
};

this.extendConfig = function(configExtension) {
var getTokenFromStorage = function(){
var tokenData = localStorage[config.localStorageName];
if(tokenData){
try {
return JSON.parse(tokenData);
}
catch(e){
localStorage.removeItem(config.localStorageName);
}
}
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;
};

var extendConfig = function(configExtension){
config = angular.extend(config, configExtension);
};

this.$get = function($q, $http, $window, $rootScope) {
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)){
extendConfig(token);
}
};

this.$get = ['$q', '$http', '$window', '$rootScope', function($q, $http, $window, $rootScope) {
/**
var requiredAndMissing = [];
angular.forEach(config, function(value, key) {
if (value === REQUIRED_AND_MISSING) {
Expand All @@ -64,38 +105,54 @@ 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.
// 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
}
};

return {
// 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.
*
* @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);
},

/**
Expand All @@ -118,24 +175,22 @@ 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();
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: {
Expand Down Expand Up @@ -167,58 +222,54 @@ 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) {
$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)
}
})
});
}
});

// TODO: reject deferred if the popup was closed without a message being delivered + maybe offer a timeout

return deferred.promise;
}
}
}
}).

/**
* 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);
getTokenType: function(){
return 'Bearer'
},

// 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)
// 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;
},

window.opener.postMessage(params, "*");
window.close();
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);
}
}
}];
});
}());
33 changes: 22 additions & 11 deletions src/js/googleOauth.js → src/js/google.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
(function(){

'use strict';

/**
Expand All @@ -6,17 +8,34 @@
*
* Guide: https://developers.google.com/accounts/docs/OAuth2UserAgent
*/
angular.module('googleOauth', ['angularOauth']).
angular.module('oauth.google', ['angularOauth'])

.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) {
.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 {
Expand All @@ -36,13 +55,5 @@ angular.module('googleOauth', ['angularOauth']).

return deferred.promise;
}]);
}).

config(function(TokenProvider, GoogleTokenVerifier) {
TokenProvider.extendConfig({
authorizationEndpoint: 'https://accounts.google.com/o/oauth2/auth',
scopes: ["https://www.googleapis.com/auth/userinfo.email"],
verifyFunc: GoogleTokenVerifier
});
});

}());
Loading
0