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 Jul 31, 2015
1 parent 14b66a9 commit c867529
Show file tree
Hide file tree
Showing 20 changed files with 405 additions and 251 deletions.
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
6 changes: 5 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
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.
157 changes: 121 additions & 36 deletions app/controllers/devise_token_auth/omniauth_callbacks_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,47 @@
if defined? OmniAuth
module OmniAuth


# Omniauth currently does not pass along omniauth.params upon failure redirect
# see also: https://github.com/intridea/omniauth/issues/626
class FailureEndpoint
def redirect_to_failure
message_key = env['omniauth.error.type']
origin_query_param = env['omniauth.origin'] ? "&origin=#{CGI.escape(env['omniauth.origin'])}" : ""
strategy_name_query_param = env['omniauth.error.strategy'] ? "&strategy=#{env['omniauth.error.strategy'].name}" : ""
extra_params = env['omniauth.params'] ? "&#{env['omniauth.params'].to_query}" : ""
new_path = "#{env['SCRIPT_NAME']}#{OmniAuth.config.path_prefix}/failure?message=#{message_key}#{origin_query_param}#{strategy_name_query_param}#{extra_params}"
Rack::Response.new(["302 Moved"], 302, 'Location' => new_path).finish
end
end


# Omniauth currently removes omniauth.params during mocked requests
# see also: https://github.com/intridea/omniauth/pull/812
module Strategy
def mock_callback_call
setup_phase
@env['omniauth.origin'] = session.delete('omniauth.origin')
@env['omniauth.origin'] = nil if env['omniauth.origin'] == ''
@env['omniauth.params'] = session.delete('omniauth.params') || {}
mocked_auth = OmniAuth.mock_auth_for(name.to_s)
if mocked_auth.is_a?(Symbol)
fail!(mocked_auth)
else
@env['omniauth.auth'] = mocked_auth
OmniAuth.config.before_callback_phase.call(@env) if OmniAuth.config.before_callback_phase
call_app!
end
end
end

end
end

module DeviseTokenAuth
class OmniauthCallbacksController < DeviseTokenAuth::ApplicationController

attr_reader :auth_params
skip_before_filter :set_user_by_token
skip_after_filter :update_auth_header

Expand All @@ -19,49 +61,95 @@ 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)

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

# build and redirect to destination url
redirect_to DeviseTokenAuth::Url.generate(omniauth_params['auth_origin_url'], data)
end
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 +162,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 +179,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 +196,15 @@ def whitelisted_params
}
end

# pull resource class from omniauth return
# in the success case, resource_class is in the omniauth_params.
# in the failure case, it is in a query param. See monkey patch above
def resource_class(mapping = nil)
if omniauth_params
omniauth_params['resource_class'].constantize
elsif params['resource_class']
params['resource_class'].constantize
else
raise "No resource_class found"
end
end

Expand All @@ -136,6 +228,12 @@ def omniauth_params
end
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
# is to persist the omniauth auth hash value thru a redirect. the value
# must be destroyed immediatly after it is accessed by omniauth_success
Expand All @@ -159,18 +257,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>
2 changes: 0 additions & 2 deletions app/views/devise_token_auth/omniauth_failure.html.erb

This file was deleted.

12 changes: 0 additions & 12 deletions app/views/devise_token_auth/omniauth_success.html.erb

This file was deleted.

Loading

0 comments on commit c867529

Please sign in to comment.