Skip to content
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

feat(improved-omniauth): add support for sameWindow and inAppBrowser omniauth flows #188

Merged
merged 1 commit into from
Aug 9, 2015
Merged
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
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.28"></a>
# 0.0.28 (2015-08-09)

## Features

- **Improved OAuth Flow**: Supports new OAuth window flows, allowing options for `sameWindow`, `newWindow`, 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 `newWindow`. 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), `newWindow`, `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:
- `newWindow` 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 `newWindow` 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 `newWindow` 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 newWindow 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 newWindow 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-11 / iOS 8.2 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 `newWindow` mode, be sure to handle this in the configuration. Eg:

```javascript
$authProvider.configure({
omniauthWindowType: isIE ? `sameWindow` : `newWindow`
})
```

---

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"
}
}
}
112 changes: 76 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,95 @@ 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 === 'newWindow') {
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();
_this.initDfd();
return _this.handleValidAuth(data, true).then(function() {
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 +424,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 +556,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 +644,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