Skip to content

Commit

Permalink
feat(improved-omniauth): add support for sameWindow and inAppBrowser …
Browse files Browse the repository at this point in the history
…omniauth flows
  • Loading branch information
booleanbetrayal committed Jul 29, 2015
1 parent 757b0d9 commit 40a7f08
Show file tree
Hide file tree
Showing 9 changed files with 338 additions and 98 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<a name="0.0.27"></a>
# 0.0.27 (2015-??-??)

## Features

- **Improved OAuth Flow**: Supports new OAuth window flows, allowing options for `sameWindow`, `externalWindow`, and `inAppBrowser`

## Breaking Changes

- `forceHardReload` has been removed in favor of `omniauthWindowType`. The new behavior now defaults to `sameWindow` mode, whereas the previous implementation mimicked the functionality of `externalWindow`. This was changed due to limitations with the `postMessage` API support in popular browsers, as well as feedback from user-experience testing.
34 changes: 25 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ angular.module('myApp', ['ng-token-auth'])
storage: 'cookies',
proxyIf: function() { return false; },
proxyUrl: '/proxy',
omniauthWindowType: 'sameWindow',
authProviderPaths: {
github: '/auth/github',
facebook: '/auth/facebook',
Expand Down Expand Up @@ -211,6 +212,7 @@ angular.module('myApp', ['ng-token-auth'])
| **handleLoginResponse** | a function that will identify and return the current user's info (id, username, etc.) in the response of a successful login request. [Read more](#using-alternate-response-formats). |
| **handleAccountUpdateResponse** | a function that will identify and return the current user's info (id, username, etc.) in the response of a successful account update request. [Read more](#using-alternate-response-formats). |
| **handleTokenValidationResponse** | a function that will identify and return the current user's info (id, username, etc.) in the response of a successful token validation request. [Read more](#using-alternate-response-formats). |
| **omniauthWindowType** | Dictates the methodolgy of the OAuth login flow. One of: `sameWindow` (default), `externalWindow`, `inAppBrowser` [Read more](#oauth2-authentication-flow). |

#### Custom Storage Object
Must implement the following interface:
Expand Down Expand Up @@ -1077,20 +1079,28 @@ The following diagram illustrates the steps necessary to authenticate a client u
![oauth flow](https://github.com/lynndylanhurley/ng-token-auth/raw/master/test/app/images/flow/omniauth-flow.jpg)
When authenticating with a 3rd party provider, the following steps will take place.
When authenticating with a 3rd party provider, the following steps will take place, assuming the backend server is configured appropriately. [devise token auth](https://github.com/lynndylanhurley/devise_token_auth) already accounts for these flows.
1. An external window will be opened to the provider's authentication page.
1. Once the user signs in, they will be redirected back to the API at the callback uri that was registered with the oauth2 provider.
1. The API will send the user's info back to the client via `postMessage` event, and then close the external window.
- `sameWindow` Mode
1. The existing window will be used to access the provider's authentication page.
2. Once the user signs in, they will be redirected back to the API using the same window, with the user and authentication tokens being set.

The postMessage event must include the following a parameters:
- `externalWindow` Mode
1. An external window will be opened to the provider's authentication page.
2. Once the user signs in, they will be redirected back to the API at the callback uri that was registered with the oauth2 provider.
3. The API will send the user's info back to the client via `postMessage` event, and then close the external window.

- `inAppBrowser` Mode
- This mode is virtually identical to the `externalWindow` flow, except the flow varies slightly to account for limitations with the [Cordova inAppBrowser Plugin](https://github.com/apache/cordova-plugin-inappbrowser) and the `postMessage` API.

The `postMessage` event (utilized for both `externalWindow` and `inAppBrowser` modes) must include the following a parameters:
* **message** - this must contain the value `"deliverCredentials"`
* **auth_token** - a unique token set by your server.
* **uid** - the id that was returned by the provider. For example, the user's facebook id, twitter id, etc.
Rails example: [controller](https://github.com/lynndylanhurley/ng-token-auth-api-rails/blob/master/app/controllers/users/auth_controller.rb#L21), [layout](https://github.com/lynndylanhurley/ng-token-auth-api-rails/blob/master/app/views/layouts/oauth_response.html.erb), [view](https://github.com/lynndylanhurley/ng-token-auth-api-rails/blob/master/app/views/users/auth/oauth_success.html.erb).
Rails externalWindow example: [controller](https://github.com/lynndylanhurley/ng-token-auth-api-rails/blob/master/app/controllers/users/auth_controller.rb#L21), [layout](https://github.com/lynndylanhurley/ng-token-auth-api-rails/blob/master/app/views/layouts/oauth_response.html.erb), [view](https://github.com/lynndylanhurley/ng-token-auth-api-rails/blob/master/app/views/users/auth/oauth_success.html.erb).
##### Example redirect_uri destination:
##### Example externalWindow redirect_uri destination:
~~~html
<!DOCTYPE html>
Expand Down Expand Up @@ -1277,7 +1287,7 @@ app.all('/proxy/*', function(req, res, next) {
The above example assumes that you're using [express](http://expressjs.com/), [request](https://github.com/mikeal/request), and [http-proxy](https://github.com/nodejitsu/node-http-proxy), and that you have set the API_URL value using [node-config](https://github.com/lorenwest/node-config).
#### IE8+ must use hard redirects for provider authentication
#### IE8+ must use `sameWindow` for provider authentication
Most modern browsers can communicate across tabs and windows using [postMessage](https://developer.mozilla.org/en-US/docs/Web/API/Window.postMessage). This doesn't work for certain flawed browsers. In these cases the client must take the following steps when performing provider authentication (facebook, github, etc.):
Expand All @@ -1286,7 +1296,13 @@ Most modern browsers can communicate across tabs and windows using [postMessage]
1. navigate from the provider to the API
1. navigate from the API back to the client
These steps are taken automatically when using this module with IE8+.
If you prefer to use the `externalWindow` mode, be sure to handle this in the configuration. Eg:
```javascript
$authProvider.configure({
omniauthWindowType: isIE ? `sameWindow` : `externalWindow`
})
```
---
Expand Down
9 changes: 3 additions & 6 deletions bower.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@
"tests"
],
"dependencies": {
"angular": ">=1.2.0",
"angular-cookie": ">=4.0.6"
},
"resolutions": {
"angular": "1.2.25"
"angular": "~1.4.4",
"angular-cookie": "~4.0.9"
}
}
}
109 changes: 73 additions & 36 deletions dist/ng-token-auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ angular.module('ng-token-auth', ['ipCookie']).provider('$auth', function() {
},
proxyUrl: '/proxy',
validateOnPageLoad: true,
forceHardRedirect: false,
omniauthWindowType: 'sameWindow',
storage: 'cookies',
tokenFormat: {
"access-token": "{{ token }}",
Expand Down Expand Up @@ -95,6 +95,7 @@ angular.module('ng-token-auth', ['ipCookie']).provider('$auth', function() {
listener: null,
initialize: function() {
this.initializeListeners();
this.cancelOmniauthInAppBrowserListeners = (function() {});
return this.addScopeMethods();
},
initializeListeners: function() {
Expand All @@ -104,15 +105,16 @@ angular.module('ng-token-auth', ['ipCookie']).provider('$auth', function() {
}
},
cancel: function(reason) {
if (this.t != null) {
$timeout.cancel(this.t);
if (this.requestCredentialsPollingTimer != null) {
$timeout.cancel(this.requestCredentialsPollingTimer);
}
this.cancelOmniauthInAppBrowserListeners();
if (this.dfd != null) {
this.rejectDfd(reason);
}
return $timeout(((function(_this) {
return function() {
return _this.t = null;
return _this.requestCredentialsPollingTimer = null;
};
})(this)), 0);
},
Expand Down Expand Up @@ -280,55 +282,92 @@ angular.module('ng-token-auth', ['ipCookie']).provider('$auth', function() {
return this.persistData('currentConfigName', configName, configName);
},
openAuthWindow: function(provider, opts) {
var authUrl;
authUrl = this.buildAuthUrl(provider, opts);
if (this.useExternalWindow()) {
return this.requestCredentials(this.createPopup(authUrl));
} else {
var authUrl, omniauthWindowType;
omniauthWindowType = this.getConfig(opts.config).omniauthWindowType;
authUrl = this.buildAuthUrl(omniauthWindowType, provider, opts);
if (omniauthWindowType === 'externalWindow') {
return this.requestCredentialsViaPostMessage(this.createPopup(authUrl));
} else if (omniauthWindowType === 'inAppBrowser') {
return this.requestCredentialsViaExecuteScript(this.createPopup(authUrl));
} else if (omniauthWindowType === 'sameWindow') {
return this.visitUrl(authUrl);
} else {
throw 'Unsupported omniauthWindowType "#{omniauthWindowType}"';
}
},
visitUrl: function(url) {
return $window.location.replace(url);
},
buildAuthUrl: function(provider, opts) {
var authUrl, key, val, _ref;
buildAuthUrl: function(omniauthWindowType, provider, opts) {
var authUrl, key, params, val;
if (opts == null) {
opts = {};
}
authUrl = this.getConfig(opts.config).apiUrl;
authUrl += this.getConfig(opts.config).authProviderPaths[provider];
authUrl += '?auth_origin_url=' + encodeURIComponent($window.location.href);
if (opts.params != null) {
_ref = opts.params;
for (key in _ref) {
val = _ref[key];
authUrl += '&';
authUrl += encodeURIComponent(key);
authUrl += '=';
authUrl += encodeURIComponent(val);
}
params = angular.extend({}, opts.params || {}, {
omniauth_window_type: omniauthWindowType
});
for (key in params) {
val = params[key];
authUrl += '&';
authUrl += encodeURIComponent(key);
authUrl += '=';
authUrl += encodeURIComponent(val);
}
return authUrl;
},
requestCredentials: function(authWindow) {
requestCredentialsViaPostMessage: function(authWindow) {
if (authWindow.closed) {
this.cancel({
reason: 'unauthorized',
errors: ['User canceled login']
});
return $rootScope.$broadcast('auth:window-closed');
return this.handleAuthWindowClose(authWindow);
} else {
authWindow.postMessage("requestCredentials", "*");
return this.t = $timeout(((function(_this) {
return this.requestCredentialsPollingTimer = $timeout(((function(_this) {
return function() {
return _this.requestCredentials(authWindow);
return _this.requestCredentialsViaPostMessage(authWindow);
};
})(this)), 500);
}
},
requestCredentialsViaExecuteScript: function(authWindow) {
var cancelOmniauthInAppBrowserListeners, handleAuthWindowClose, handleLoadStop;
this.cancelOmniauthInAppBrowserListeners();
handleAuthWindowClose = this.handleAuthWindowClose.bind(this, authWindow);
handleLoadStop = this.handleLoadStop.bind(this, authWindow);
authWindow.addEventListener('loadstop', handleLoadStop);
authWindow.addEventListener('exit', handleAuthWindowClose);
return cancelOmniauthInAppBrowserListeners = function() {
authWindow.removeEventListener('loadstop', handleLoadStop);
return authWindow.removeEventListener('exit', handleAuthWindowClose);
};
},
handleLoadStop: function(authWindow) {
_this = this;
return authWindow.executeScript({
code: 'requestCredentials()'
}, function(response) {
var data, ev;
data = response[0];
if (data) {
ev = new Event('message');
ev.data = data;
$window.dispatchEvent(ev);
_this.cancelOmniauthInAppBrowserListeners();
return authWindow.close();
}
});
},
handleAuthWindowClose: function(authWindow) {
this.cancel({
reason: 'unauthorized',
errors: ['User canceled login']
});
this.cancelOmniauthInAppBrowserListeners;
return $rootScope.$broadcast('auth:window-closed');
},
createPopup: function(url) {
return $window.open(url);
return $window.open(url, '_blank');
},
resolveDfd: function() {
this.dfd.resolve(this.user);
Expand Down Expand Up @@ -382,8 +421,8 @@ angular.module('ng-token-auth', ['ipCookie']).provider('$auth', function() {
search = $location.search();
location_parse = this.parseLocation(window.location.search);
params = Object.keys(search).length === 0 ? location_parse : search;
if (params.token !== void 0) {
token = params.token;
token = params.auth_token || params.token;
if (token !== void 0) {
clientId = params.client_id;
uid = params.uid;
expiry = params.expiry;
Expand Down Expand Up @@ -514,9 +553,10 @@ angular.module('ng-token-auth', ['ipCookie']).provider('$auth', function() {
if (setHeader == null) {
setHeader = false;
}
if (this.t != null) {
$timeout.cancel(this.t);
if (this.requestCredentialsPollingTimer != null) {
$timeout.cancel(this.requestCredentialsPollingTimer);
}
this.cancelOmniauthInAppBrowserListeners();
angular.extend(this.user, user);
this.user.signedIn = true;
this.user.configName = this.getCurrentConfigName();
Expand Down Expand Up @@ -601,9 +641,6 @@ angular.module('ng-token-auth', ['ipCookie']).provider('$auth', function() {
}
return result;
},
useExternalWindow: function() {
return !(this.getConfig().forceHardRedirect || $window.isIE());
},
initDfd: function() {
return this.dfd = $q.defer();
},
Expand Down
Loading

0 comments on commit 40a7f08

Please sign in to comment.