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

Adding in unlocks controller and specs. This should resolve #927. #971

Merged
merged 4 commits into from
Oct 11, 2017
Merged
Show file tree
Hide file tree
Changes from 2 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
118 changes: 118 additions & 0 deletions app/controllers/devise_token_auth/unlocks_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
module DeviseTokenAuth
class UnlocksController < DeviseTokenAuth::ApplicationController
skip_after_action :update_auth_header, :only => [:create, :show]

# this action is responsible for generating unlock tokens and
# sending emails
def create
unless resource_params[:email]
return render_create_error_missing_email
end

# honor devise configuration for case_insensitive_keys
if resource_class.case_insensitive_keys.include?(:email)
@email = resource_params[:email].downcase
else
@email = resource_params[:email]
end

q = "uid = ? AND provider='email'"

# fix for mysql default case insensitivity
if ActiveRecord::Base.connection.adapter_name.downcase.starts_with? 'mysql'
q = "BINARY uid = ? AND provider='email'"
end

@resource = resource_class.where(q, @email).first

@errors = nil
@error_status = 400

if @resource
yield @resource if block_given?

@resource.send_unlock_instructions({
email: @email,
provider: 'email',
client_config: params[:config_name]
})

if @resource.errors.empty?
return render_create_success
else
@errors = @resource.errors
end
else
@errors = [I18n.t("devise_token_auth.unlocks.user_not_found", email: @email)]
@error_status = 404
end

if @errors
return render_create_error
end
end

def show
@resource = resource_class.unlock_access_by_token(params[:unlock_token])

if @resource && @resource.id
client_id = SecureRandom.urlsafe_base64(nil, false)
token = SecureRandom.urlsafe_base64(nil, false)
token_hash = BCrypt::Password.create(token)
expiry = (Time.now + DeviseTokenAuth.token_lifespan).to_i

@resource.tokens[client_id] = {
token: token_hash,
expiry: expiry
}

@resource.save!
yield @resource if block_given?

redirect_to(@resource.build_auth_url(after_unlock_path_for(@resource), {
token: token,
client_id: client_id,
unlock: true,
config: params[:config]
}))
else
render_show_error
end
end

private
def after_unlock_path_for(resource)
#TODO: This should probably be a configuration option at the very least.
'/'
end

def render_create_error_missing_email
render json: {
success: false,
errors: [I18n.t("devise_token_auth.unlocks.missing_email")]
}, status: 401
end

def render_create_success
render json: {
success: true,
message: I18n.t("devise_token_auth.unlocks.sended", email: @email)
}
end

def render_create_error
render json: {
success: false,
errors: @errors,
}, status: @error_status
end

def render_show_error
raise ActionController::RoutingError.new('Not Found')
end

def resource_params
params.permit(:email, :unlock_token, :config)
end
end
end
15 changes: 15 additions & 0 deletions app/models/devise_token_auth/concerns/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,21 @@ def send_reset_password_instructions(opts=nil)

token
end

# override devise method to include additional info as opts hash
def send_unlock_instructions(opts=nil)
raw, enc = Devise.token_generator.generate(self.class, :unlock_token)
self.unlock_token = enc
save(validate: false)

opts ||= {}

# fall back to "default" config name
opts[:client_config] ||= "default"

send_devise_notification(:unlock_instructions, raw, opts)
raw
end
end

module ClassMethods
Expand Down
2 changes: 1 addition & 1 deletion app/views/devise/mailer/unlock_instructions.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

<p><%= t '.unlock_link_msg' %></p>

<p><%= link_to t('.unlock_link'), unlock_url(@resource, unlock_token: @token) %></p>
<p><%= link_to t('.unlock_link'), unlock_url(@resource, unlock_token: @token, config: message['client-config'].to_s) %></p>
4 changes: 4 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ en:
password_not_required: "This account does not require a password. Sign in using your '%{provider}' account instead."
missing_passwords: "You must fill out the fields labeled 'Password' and 'Password confirmation'."
successfully_updated: "Your password has been successfully updated."
unlocks:
missing_email: "You must provide an email address."
sended: "An email has been sent to '%{email}' containing instructions for unlocking your account."
user_not_found: "Unable to find user with email '%{email}'."
errors:
messages:
validate_sign_up_params: "Please submit proper sign up data in request body."
Expand Down
2 changes: 1 addition & 1 deletion lib/devise_token_auth/rails/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def mount_devise_token_auth_for(resource, opts)
confirmations_ctrl = opts[:controllers][:confirmations] || "devise_token_auth/confirmations"
token_validations_ctrl = opts[:controllers][:token_validations] || "devise_token_auth/token_validations"
omniauth_ctrl = opts[:controllers][:omniauth_callbacks] || "devise_token_auth/omniauth_callbacks"
unlocks_ctrl = opts[:controllers][:unlocks]
unlocks_ctrl = opts[:controllers][:unlocks] || "devise_token_auth/unlocks"

# define devise controller mappings
controllers = {:sessions => sessions_ctrl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,7 @@ class DeviseTokenAuth::PasswordsControllerTest < ActionController::TestCase
@resource.reload
end
end

describe 'unconfirmable user' do
setup do
@request.env['devise.mapping'] = Devise.mappings[:unconfirmable_user]
Expand Down
197 changes: 197 additions & 0 deletions test/controllers/devise_token_auth/unlocks_controller_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
require 'test_helper'

# was the web request successful?
# was the user redirected to the right page?
# was the user successfully authenticated?
# was the correct object stored in the response?
# was the appropriate message delivered in the json payload?

class DeviseTokenAuth::UnlocksControllerTest < ActionController::TestCase
describe DeviseTokenAuth::UnlocksController do
setup do
@request.env['devise.mapping'] = Devise.mappings[:lockable_user]
end

teardown do
@request.env['devise.mapping'] = Devise.mappings[:user]
end

before do
@original_lock_strategy = Devise.lock_strategy
@original_unlock_strategy = Devise.unlock_strategy
@original_maximum_attempts = Devise.maximum_attempts
Devise.lock_strategy = :failed_attempts
Devise.unlock_strategy = :email
Devise.maximum_attempts = 5
end

after do
Devise.lock_strategy = @original_lock_strategy
Devise.maximum_attempts = @original_maximum_attempts
Devise.unlock_strategy = @original_unlock_strategy
end

describe "Unlocking user" do
before do
@resource = lockable_users(:unlocked_user)
end

describe 'not email should return 401' do
Copy link
Collaborator

@MaicolBen MaicolBen Oct 10, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this message correct? I would put request unlock without email

before do
@auth_headers = @resource.create_new_auth_token
@new_password = Faker::Internet.password

xhr :post, :create, {}
@data = JSON.parse(response.body)
end

test 'response should fail' do
assert_equal 401, response.status
end
test 'error message should be returned' do
assert @data["errors"]
assert_equal @data["errors"], [I18n.t("devise_token_auth.passwords.missing_email")]
end
end

describe 'request unlock' do
describe 'unknown user should return 404' do
before do
xhr :post, :create, {
email: 'chester@cheet.ah'
}
@data = JSON.parse(response.body)
end
test 'unknown user should return 404' do
assert_equal 404, response.status
end

test 'errors should be returned' do
assert @data["errors"]
assert_equal @data["errors"], [I18n.t("devise_token_auth.passwords.user_not_found", email: 'chester@cheet.ah')]
end
end

describe 'successfully requested unlock' do
before do
xhr :post, :create, {
email: @resource.email
}

@data = JSON.parse(response.body)
end

test 'response should not contain extra data' do
assert_nil @data["data"]
end
end

describe 'case-sensitive email' do
before do
xhr :post, :create, {
email: @resource.email
}

@mail = ActionMailer::Base.deliveries.last
@resource.reload
@data = JSON.parse(response.body)

@mail_config_name = CGI.unescape(@mail.body.match(/config=([^&]*)&/)[1])
@mail_reset_token = @mail.body.match(/unlock_token=(.*)\"/)[1]
end

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

test 'response should contains message' do
assert_equal @data["message"], I18n.t("devise_token_auth.unlocks.sended", email: @resource.email)
end

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

test 'the email should be addressed to the user' do
assert_equal @mail.to.first, @resource.email
end

test 'the client config name should fall back to "default"' do
assert_equal 'default', @mail_config_name
end

test 'the email body should contain a link with reset token as a query param' do
user = LockableUser.unlock_access_by_token(@mail_reset_token)
assert_equal user.id, @resource.id
end

describe 'unlock link failure' do
test 'response should return 404' do
assert_raises(ActionController::RoutingError) {
xhr :get, :show, {
unlock_token: "bogus"
}
}
end
end

describe 'password reset link success' do
before do
xhr :get, :show, {
unlock_token: @mail_reset_token
}

@resource.reload

raw_qs = response.location.split('?')[1]
@qs = Rack::Utils.parse_nested_query(raw_qs)

@client_id = @qs["client_id"]
@expiry = @qs["expiry"]
@unlock = @qs["unlock"]
@token = @qs["token"]
@uid = @qs["uid"]
end

test 'respones should have success redirect status' do
assert_equal 302, response.status
end

test 'response should contain auth params' do
assert @client_id
assert @expiry
assert @unlock
assert @token
assert @uid
end

test 'response auth params should be valid' do
assert @resource.valid_token?(@token, @client_id)
end
end
end

describe 'case-insensitive email' do
before do
@resource_class = LockableUser
@request_params = {
email: @resource.email.upcase
}
end

test 'response should return success status if configured' do
@resource_class.case_insensitive_keys = [:email]
xhr :post, :create, @request_params
assert_equal 200, response.status
end

test 'response should return failure status if not configured' do
@resource_class.case_insensitive_keys = []
xhr :post, :create, @request_params
assert_equal 404, response.status
end
end
end
end
end
end
1 change: 0 additions & 1 deletion test/models/user_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ class UserTest < ActiveSupport::TestCase
@resource.tokens[@client_id]['expiry'] = Time.now.to_i - 10.seconds
refute @resource.token_is_current?(@token, @client_id)
end

end

describe 'user specific token lifespan' do
Expand Down