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
nbrustein authored and booleanbetrayal committed Aug 9, 2015
1 parent 14b66a9 commit e3e24b7
Show file tree
Hide file tree
Showing 23 changed files with 459 additions and 260 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.1.33"></a>
# 0.1.33 (2015-08-09)

## Features

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

## Breaking Changes

- The new OmniAuth callback 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.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ group :development, :test do
gem 'guard-minitest'
gem 'faker'
gem 'fuzz_ball'
gem 'mocha'
end

# code coverage, metrics
Expand Down
8 changes: 6 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ GIT
PATH
remote: .
specs:
devise_token_auth (0.1.32)
devise_token_auth (0.1.33)
devise (~> 3.3)
rails (~> 4.2)

Expand Down Expand Up @@ -128,6 +128,7 @@ GEM
lumberjack (1.0.9)
mail (2.6.3)
mime-types (>= 1.16, < 3)
metaclass (0.0.4)
method_source (0.8.2)
mime-types (2.6.1)
mini_portile (0.6.2)
Expand All @@ -142,10 +143,12 @@ GEM
builder
minitest (>= 5.0)
ruby-progressbar
mocha (1.1.0)
metaclass (~> 0.0.1)
multi_json (1.11.2)
multi_xml (0.5.5)
multipart-post (2.0.0)
mysql2 (0.3.18)
mysql2 (0.3.19)
nenv (0.2.0)
nokogiri (1.6.6.2)
mini_portile (~> 0.6.0)
Expand Down Expand Up @@ -245,6 +248,7 @@ DEPENDENCIES
minitest-focus
minitest-rails
minitest-reporters
mocha
mysql2
omniauth-facebook!
omniauth-github!
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -846,5 +846,12 @@ To run the test suite do the following:

The last command will open the [guard](https://github.com/guard/guard) test-runner. Guard will re-run each test suite when changes are made to its corresponding files.

To run just one test:
1. Clone this repo
2. Run `bundle install`
3. Run `rake db:migrate`
4. Run `RAILS_ENV=test rake db:migrate`
5. See this link for various ways to run a single file or a single test: http://flavio.castelli.name/2010/05/28/rails_execute_single_test/

# License
This project uses the WTFPL
10 changes: 10 additions & 0 deletions app/controllers/devise_token_auth/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
+<a name="0.1.33"></a>
+# 0.1.33 (2015-??-??)
+
+## Features
+
+- **Improved OAuth Flow**: Supports new OAuth window flows, allowing options for `sameWindow`, `newWindow`, and `inAppBrowser`
+
+## Breaking Changes
+
+- The new OAuth redirect 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.
165 changes: 122 additions & 43 deletions app/controllers/devise_token_auth/omniauth_callbacks_controller.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
module DeviseTokenAuth
class OmniauthCallbacksController < DeviseTokenAuth::ApplicationController

attr_reader :auth_params
skip_before_filter :set_user_by_token
skip_after_filter :update_auth_header

# intermediary route for successful omniauth authentication. omniauth does
# not support multiple models, so we must resort to this terrible hack.
def redirect_callbacks

# derive target redirect route from 'resource_class' param, which was set
# before authentication.
devise_mapping = request.env['omniauth.params']['resource_class'].underscore.to_sym
Expand All @@ -19,49 +22,113 @@ def redirect_callbacks
redirect_to redirect_route
end

def omniauth_success
def get_resource_from_auth_hash
# find or create user by provider and provider uid
@resource = resource_class.where({
uid: auth_hash['uid'],
provider: auth_hash['provider']
}).first_or_initialize
@oauth_registration = @resource.new_record?

if @resource.new_record?
@oauth_registration = true
set_random_password
end

# sync user info with provider, update/generate auth token
assign_provider_attrs(@resource, auth_hash)

# assign any additional (whitelisted) attributes
extra_params = whitelisted_params
@resource.assign_attributes(extra_params) if extra_params

@resource
end

def set_random_password
# set crazy password for new oauth users. this is only used to prevent
# access via email sign-in.
p = SecureRandom.urlsafe_base64(nil, false)
@resource.password = p
@resource.password_confirmation = p
end

def create_token_info
# create token info
@client_id = SecureRandom.urlsafe_base64(nil, false)
@token = SecureRandom.urlsafe_base64(nil, false)
@expiry = (Time.now + DeviseTokenAuth.token_lifespan).to_i
@config = omniauth_params['config_name']
end

auth_origin_url_params = {
token: @token,
def create_auth_params
@auth_params = {
auth_token: @token,
client_id: @client_id,
uid: @resource.uid,
expiry: @expiry,
config: @config
}
auth_origin_url_params.merge!(oauth_registration: true) if @oauth_registration
@auth_origin_url = generate_url(omniauth_params['auth_origin_url'], auth_origin_url_params)

# set crazy password for new oauth users. this is only used to prevent
# access via email sign-in.
unless @resource.id
p = SecureRandom.urlsafe_base64(nil, false)
@resource.password = p
@resource.password_confirmation = p
end
@auth_params.merge!(oauth_registration: true) if @oauth_registration
@auth_params
end

def set_token_on_resource
@resource.tokens[@client_id] = {
token: BCrypt::Password.create(@token),
expiry: @expiry
}
end

# sync user info with provider, update/generate auth token
assign_provider_attrs(@resource, auth_hash)
def render_data(message, data)
@data = data.merge({
message: message
})
render :layout => nil, :template => "devise_token_auth/omniauth_external_window"
end

# assign any additional (whitelisted) attributes
extra_params = whitelisted_params
@resource.assign_attributes(extra_params) if extra_params
def render_data_or_redirect(message, data)

# We handle inAppBrowser and newWindow the same, but it is nice
# to support values in case people need custom implementations for each case
# (For example, nbrustein does not allow new users to be created if logging in with
# an inAppBrowser)
#
# See app/views/devise_token_auth/omniauth_external_window.html.erb to understand
# why we can handle these both the same. The view is setup to handle both cases
# at the same time.
if ['inAppBrowser', 'newWindow'].include?(omniauth_window_type)
render_data(message, data)

elsif auth_origin_url # default to same-window implementation, which forwards back to auth_origin_url

# build and redirect to destination url
redirect_to DeviseTokenAuth::Url.generate(auth_origin_url, data)
else

# there SHOULD always be an auth_origin_url, but if someone does something silly
# like coming straight to this url or refreshing the page at the wrong time, there may not be one.
# In that case, just render in plain text the error message if there is one or otherwise
# a generic message.
fallback_render data[:error] || 'An error occurred'
end
end

def fallback_render(text)
render inline: %Q|
<html>
<head></head>
<body>
#{text}
</body>
</html>|
end

def omniauth_success
get_resource_from_auth_hash
create_token_info
set_token_on_resource
create_auth_params

if resource_class.devise_modules.include?(:confirmable)
# don't send confirmation email!!!
Expand All @@ -74,8 +141,7 @@ def omniauth_success

yield if block_given?

# render user info to javascript postMessage communication window
render :layout => "layouts/omniauth_response", :template => "devise_token_auth/omniauth_success"
render_data_or_redirect('deliverCredentials', @resource.as_json.merge(@auth_params.as_json))
end


Expand All @@ -92,7 +158,7 @@ def assign_provider_attrs(user, auth_hash)

def omniauth_failure
@error = params[:message]
render :layout => "layouts/omniauth_response", :template => "devise_token_auth/omniauth_failure"
render_data_or_redirect('authFailure', {error: @error})
end


Expand All @@ -109,10 +175,13 @@ def whitelisted_params
}
end

# pull resource class from omniauth return
def resource_class(mapping = nil)
if omniauth_params
if omniauth_params['resource_class']
omniauth_params['resource_class'].constantize
elsif params['resource_class']
params['resource_class'].constantize
else
raise "No resource_class found"
end
end

Expand All @@ -126,14 +195,37 @@ def resource_name
# request.env variable. this variable is then persisted thru the redirect
# using our own dta.omniauth.params session var. the omniauth_success
# method will access that session var and then destroy it immediately
# after use.
# after use. In the failure case, finally, the omniauth params
# are added as query params in our monkey patch to OmniAuth in engine.rb
def omniauth_params
if request.env['omniauth.params']
request.env['omniauth.params']
else
@_omniauth_params ||= session.delete('dta.omniauth.params')
@_omniauth_params
if !defined?(@_omniauth_params)
if request.env['omniauth.params'] && request.env['omniauth.params'].any?
@_omniauth_params = request.env['omniauth.params']
elsif session['dta.omniauth.params'] && session['dta.omniauth.params'].any?
@_omniauth_params ||= session.delete('dta.omniauth.params')
@_omniauth_params
elsif params['omniauth_window_type']
@_omniauth_params = params.slice('omniauth_window_type', 'auth_origin_url', 'resource_class', 'origin')
else
@_omniauth_params = {}
end
end
@_omniauth_params

end

def omniauth_window_type
omniauth_params['omniauth_window_type']
end

def auth_origin_url
omniauth_params['auth_origin_url'] || omniauth_params['origin']
end

# in the success case, omniauth_window_type is in the omniauth_params.
# in the failure case, it is in a query param. See monkey patch above
def omniauth_window_type
omniauth_params.nil? ? params['omniauth_window_type'] : omniauth_params['omniauth_window_type']
end

# this sesison value is set by the redirect_callbacks method. its purpose
Expand All @@ -159,18 +251,5 @@ def devise_mapping
end
end

def generate_url(url, params = {})
auth_url = url

# ensure that hash-bang is present BEFORE querystring for angularjs
unless url.match(/#/)
auth_url += '#'
end

# add query AFTER hash-bang
auth_url += "?#{params.to_query}"

return auth_url
end
end
end
15 changes: 1 addition & 14 deletions app/models/devise_token_auth/concerns/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ def build_auth_url(base_url, args)
args[:uid] = self.uid
args[:expiry] = self.tokens[args[:client_id]]['expiry']

generate_url(base_url, args)
DeviseTokenAuth::Url.generate(base_url, args)
end


Expand All @@ -222,19 +222,6 @@ def token_validation_response

protected


def generate_url(url, params = {})
uri = URI(url)

res = "#{uri.scheme}://#{uri.host}"
res += ":#{uri.port}" if (uri.port and uri.port != 80 and uri.port != 443)
res += "#{uri.path}" if uri.path
res += "?#{params.to_query}"
res += "##{uri.fragment}" if uri.fragment

return res
end

# only validate unique email among users that registered by email
def unique_email_user
if provider == 'email' and self.class.where(provider: 'email', email: email).count > 0
Expand Down
38 changes: 38 additions & 0 deletions app/views/devise_token_auth/omniauth_external_window.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<script>
/*
The data is accessible in two ways:
1. Using the postMessage api, this window will respond to a
'message' event with a post of all the data. (This can
be used by browsers other than IE if this window was
opened with window.open())
2. This window has a function called requestCredentials which,
when called, will return the data. (This can be
used if this window was opened in an inAppBrowser using
Cordova / PhoneGap)
*/

var data = <%= @data.to_json.html_safe %>;

window.addEventListener("message", function(ev) {
if (ev.data === "requestCredentials") {
ev.source.postMessage(data, '*');
window.close();
}
});
function requestCredentials() {
return data;
}
setTimeout(function() {
window.getElementById('text').innerHTML = 'Redirecting...';
}, 1000);
</script>
</head>
<body>
<pre id="text">
</pre>
</body>
</html>
Loading

0 comments on commit e3e24b7

Please sign in to comment.