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): omniauth sameWindow and inAppBrowser flows #323

Merged
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.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