diff --git a/lib/castle_devise.rb b/lib/castle_devise.rb index fc6026a..6e76dca 100644 --- a/lib/castle_devise.rb +++ b/lib/castle_devise.rb @@ -41,6 +41,7 @@ def castle require_relative "castle_devise/hooks/castle_protectable" require_relative "castle_devise/models/castle_protectable" require_relative "castle_devise/patches/registrations_controller" +require_relative "castle_devise/patches/sessions_controller" require_relative "castle_devise/rails" diff --git a/lib/castle_devise/patches.rb b/lib/castle_devise/patches.rb index 62246fa..fea5b9b 100644 --- a/lib/castle_devise/patches.rb +++ b/lib/castle_devise/patches.rb @@ -5,6 +5,7 @@ module Patches class << self def apply Devise::RegistrationsController.send(:include, Patches::RegistrationsController) + Devise::SessionsController.send(:include, Patches::SessionsController) end end end diff --git a/lib/castle_devise/patches/sessions_controller.rb b/lib/castle_devise/patches/sessions_controller.rb new file mode 100644 index 0000000..95e328b --- /dev/null +++ b/lib/castle_devise/patches/sessions_controller.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module CastleDevise + module Patches + module SessionsController + extend ActiveSupport::Concern + + included do + before_action :castle_filter, only: :create + end + + def castle_filter + response = CastleDevise.sdk_facade.filter( + event: "$login", + context: CastleDevise::Context.from_rack_env(request.env) + ) + + case response.dig(:policy, :action) + when "deny" + throw(:warden, scope: resource_name, message: :not_found_in_database) + else + # everything fine, continue + end + rescue Castle::Error => e + # log API errors and allow + logger.info "#{e}: #{e.message}" + end + end + end +end diff --git a/spec/castle_devise/integration/login_spec.rb b/spec/castle_devise/integration/login_spec.rb index 2d1df88..0e11650 100644 --- a/spec/castle_devise/integration/login_spec.rb +++ b/spec/castle_devise/integration/login_spec.rb @@ -13,11 +13,72 @@ before do allow(CastleDevise).to receive(:sdk_facade).and_return(facade) - allow(facade).to receive(:risk).and_return(castle_response) + allow(facade).to receive(:risk).and_return(castle_risk_response) + allow(facade).to receive(:filter).and_return(castle_filter_response) end - describe "with correct password" do - let(:castle_response) do + context "when filter denies" do + let(:castle_filter_response) do + { + risk: 0.92, + signals: {}, + policy: { + action: "deny", + name: "My Policy", + id: "e14c5a8d-c682-4a22-bbca-04fa6b98ad0c", + revision_id: "b5cf794e-88c0-426e-8276-037ba1e7ceca" + }, + } + end + let(:castle_risk_response) { double } + + before do + post "/users/sign_in", + params: { + user: {email: user.email, password: "123456"}, + castle_request_token: "token123" + } + end + + it "calls the facade with valid arguments" do + expect(facade).to have_received(:filter) do |event:, context:| + expect(event).to eq("$login") + expect(context).to be_a(CastleDevise::Context) + end + end + + it "does not call risk" do + expect(facade).not_to have_received(:risk) + end + + it "does not authenticate the user" do + expect(request.env["warden"].user(:user)).to be_nil + end + + it "sets a flash message" do + expect(flash.alert).to match(/invalid email or password/i) + end + + it "redirects to sign_in path" do + expect(response).to redirect_to('/users/sign_in') + end + end + + context "with correct password" do + let(:castle_filter_response) do + { + risk: 0.32, + signals: {}, + policy: { + action: "allow", + name: "My Policy", + id: "e14c5a8d-c682-4a22-bbca-04fa6b98ad0c", + revision_id: "b5cf794e-88c0-426e-8276-037ba1e7ceca" + }, + } + end + + let(:castle_risk_response) do { risk: 0.4, signals: {}, @@ -90,6 +151,10 @@ it "sets a flash message" do expect(flash.alert).to match(/invalid email or password/i) end + + it "redirects to sign_in path" do + expect(response).to redirect_to('/users/sign_in') + end end end end