Skip to content

Commit

Permalink
allow default email confirmation and password reset confirmation redi…
Browse files Browse the repository at this point in the history
…rect urls. fixes #176
  • Loading branch information
lynndylanhurley committed Mar 13, 2015
1 parent 1b5e1f5 commit 0b8ad52
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 6 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ The following settings are available for configuration in `config/initializers/d
| **`token_lifespan`** | `2.weeks` | Set the length of your tokens' lifespans. Users will need to re-authenticate after this duration of time has passed since their last login. |
| **`batch_request_buffer_throttle`** | `5.seconds` | Sometimes it's necessary to make several requests to the API at the same time. In this case, each request in the batch will need to share the same auth token. This setting determines how far apart the requests can be while still using the same auth token. [Read more](#about-batch-requests). |
| **`omniauth_prefix`** | `"/omniauth"` | This route will be the prefix for all oauth2 redirect callbacks. For example, using the default '/omniauth' setting, the github oauth2 provider will redirect successful authentications to '/omniauth/github/callback'. [Read more](#omniauth-provider-settings). |
| **`default_confirm_success_url`** | `nil` | By default this value is expected to be sent by the client so that the API knows where to redirect users after successful email confirmation. If this param is set, the API will redirect to this value when no value is provided by the cilent. |
| **`default_password_reset_url`** | `nil` | By default this value is expected to be sent by the client so that the API knows where to redirect users after successful password resets. If this param is set, the API will redirect to this value when no value is provided by the cilent. |
| **`redirect_whitelist`** | `nil` | As an added security measure, you can limit the URLs to which the API will redirect after email token validation (password reset, email confirmation, etc.). This value should be an array containing exact matches to the client URLs to be visited after validation. |


## OmniAuth authentication
Expand Down
23 changes: 20 additions & 3 deletions app/controllers/devise_token_auth/passwords_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,30 @@ def create
}, status: 401
end

unless params[:redirect_url]
# give redirect value from params priority
redirect_url = params[:redirect_url]

# fall back to default value if provided
redirect_url ||= DeviseTokenAuth.default_password_reset_url

unless redirect_url
return render json: {
success: false,
errors: ['Missing redirect url.']
}, status: 401
end

# if whitelist is set, validate redirect_url against whitelist
if DeviseTokenAuth.redirect_whitelist
unless DeviseTokenAuth.redirect_whitelist.include?(redirect_url)
return render json: {
status: 'error',
data: @resource.as_json,
errors: ["Redirect to #{redirect_url} not allowed."]
}, status: 403
end
end

# honor devise configuration for case_insensitive_keys
if resource_class.case_insensitive_keys.include?(:email)
email = resource_params[:email].downcase
Expand All @@ -43,7 +60,7 @@ def create
@resource.send_reset_password_instructions({
email: email,
provider: 'email',
redirect_url: params[:redirect_url],
redirect_url: redirect_url,
client_config: params[:config_name]
})

Expand All @@ -70,7 +87,7 @@ def create
end


# this is where users arrive after visiting the email confirmation link
# this is where users arrive after visiting the password reset confirmation link
def edit
@resource = resource_class.reset_password_by_token({
reset_password_token: resource_params[:reset_password_token]
Expand Down
21 changes: 19 additions & 2 deletions app/controllers/devise_token_auth/registrations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,32 @@ def create
@resource.email = sign_up_params[:email]
end

# give redirect value from params priority
redirect_url = params[:confirm_success_url]

# fall back to default value if provided
redirect_url ||= DeviseTokenAuth.default_confirm_success_url

# success redirect url is required
if resource_class.devise_modules.include?(:confirmable) && !params[:confirm_success_url]
if resource_class.devise_modules.include?(:confirmable) && !redirect_url
return render json: {
status: 'error',
data: @resource.as_json,
errors: ["Missing `confirm_success_url` param."]
}, status: 403
end

# if whitelist is set, validate redirect_url against whitelist
if DeviseTokenAuth.redirect_whitelist
unless DeviseTokenAuth.redirect_whitelist.include?(redirect_url)
return render json: {
status: 'error',
data: @resource.as_json,
errors: ["Redirect to #{redirect_url} not allowed."]
}, status: 403
end
end

begin
# override email confirmation, must be sent manually from ctrl
resource_class.skip_callback("create", :after, :send_on_create_confirmation_instructions)
Expand All @@ -32,7 +49,7 @@ def create
# user will require email authentication
@resource.send_confirmation_instructions({
client_config: params[:config_name],
redirect_url: params[:confirm_success_url]
redirect_url: redirect_url
})

else
Expand Down
8 changes: 7 additions & 1 deletion lib/devise_token_auth/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,18 @@ class Engine < ::Rails::Engine
mattr_accessor :change_headers_on_each_request,
:token_lifespan,
:batch_request_buffer_throttle,
:omniauth_prefix
:omniauth_prefix,
:default_confirm_success_url,
:default_password_reset_url,
:redirect_whitelist

self.change_headers_on_each_request = true
self.token_lifespan = 2.weeks
self.batch_request_buffer_throttle = 5.seconds
self.omniauth_prefix = '/omniauth'
self.default_confirm_success_url = nil
self.default_password_reset_url = nil
self.redirect_whitelist = nil

def self.setup(&block)
yield self
Expand Down
66 changes: 66 additions & 0 deletions test/controllers/devise_token_auth/passwords_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,72 @@ class DeviseTokenAuth::PasswordsControllerTest < ActionController::TestCase
end
end

describe 'Using default_password_reset_url' do
before do
@resource = users(:confirmed_email_user)
@redirect_url = 'http://ng-token-auth.dev'

DeviseTokenAuth.default_password_reset_url = @redirect_url

xhr :post, :create, {
email: @resource.email,
redirect_url: @redirect_url
}

@mail = ActionMailer::Base.deliveries.last
@resource.reload

@sent_redirect_url = CGI.unescape(@mail.body.match(/redirect_url=([^&]*)&/)[1])
end

teardown do
DeviseTokenAuth.default_password_reset_url = nil
end

test 'response should return success status' do
assert_equal 200, response.status
end

test 'action should send an email' do
assert @mail
end

test 'the email body should contain a link with redirect url as a query param' do
assert_equal @redirect_url, @sent_redirect_url
end
end

describe 'Using redirect_whitelist' do
before do
@resource = users(:confirmed_email_user)
@good_redirect_url = Faker::Internet.url
@bad_redirect_url = Faker::Internet.url
DeviseTokenAuth.redirect_whitelist = [@good_redirect_url]
end

teardown do
DeviseTokenAuth.redirect_whitelist = nil
end

test "request to whitelisted redirect should be successful" do
xhr :post, :create, {
email: @resource.email,
redirect_url: @good_redirect_url
}

assert_equal 200, response.status
end

test "request to non-whitelisted redirect should fail" do
xhr :post, :create, {
email: @resource.email,
redirect_url: @bad_redirect_url
}

assert_equal 403, response.status
end
end

describe "change password" do
describe 'success' do
before do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,79 @@ class DeviseTokenAuth::RegistrationsControllerTest < ActionDispatch::Integration
end
end

describe 'Using redirect_whitelist' do
before do
@good_redirect_url = Faker::Internet.url
@bad_redirect_url = Faker::Internet.url
DeviseTokenAuth.redirect_whitelist = [@good_redirect_url]
end

teardown do
DeviseTokenAuth.redirect_whitelist = nil
end

test "request to whitelisted redirect should be successful" do
post '/auth', {
email: Faker::Internet.email,
password: "secret123",
password_confirmation: "secret123",
confirm_success_url: @good_redirect_url,
unpermitted_param: '(x_x)'
}

assert_equal 200, response.status
end

test "request to non-whitelisted redirect should fail" do
post '/auth', {
email: Faker::Internet.email,
password: "secret123",
password_confirmation: "secret123",
confirm_success_url: @bad_redirect_url,
unpermitted_param: '(x_x)'
}

assert_equal 403, response.status
end
end

describe 'Using default_confirm_success_url' do
before do
@mails_sent = ActionMailer::Base.deliveries.count
@redirect_url = Faker::Internet.url

DeviseTokenAuth.default_confirm_success_url = @redirect_url

post '/auth', {
email: Faker::Internet.email,
password: "secret123",
password_confirmation: "secret123",
unpermitted_param: '(x_x)'
}

@resource = assigns(:resource)
@data = JSON.parse(response.body)
@mail = ActionMailer::Base.deliveries.last
@sent_redirect_url = URI.decode(@mail.body.match(/redirect_url=([^&]*)(&|\")/)[1])
end

teardown do
DeviseTokenAuth.default_confirm_success_url = nil
end

test "request should be successful" do
assert_equal 200, response.status
end

test "the email was sent" do
assert_equal @mails_sent + 1, ActionMailer::Base.deliveries.count
end

test 'email contains the default redirect url' do
assert_equal @redirect_url, @sent_redirect_url
end
end

describe 'using namespaces' do
before do
@mails_sent = ActionMailer::Base.deliveries.count
Expand Down

0 comments on commit 0b8ad52

Please sign in to comment.