Disclaimer: CastleDevise is currently in beta. There might be some upcoming breaking changes to the gem before we stabilize the API.
CastleDevise
CastleDevise is a Devise plugin that integrates Castle.
It currently provides the following features:
- preventing bots from registration attacks using Castle's Filter API
- preventing ATO attacks using Castle's Risk API
- blocks attempts to update passwords for high-risk logged-in users
- logs attempts of password reset flows so that you can see them on the Castle dashboard
If you want to learn about all capabilities of Castle, please take a look at our documentation.
Installation
Include castle_devise
in your Gemfile:
gem 'castle_devise'
Create config/initializers/castle_devise.rb
and fill in your API secret and APP_ID from the Castle Dashboard
CastleDevise.configure do |config|
config.api_secret = ENV.fetch('CASTLE_API_SECRET')
config.app_id = ENV.fetch('CASTLE_APP_ID')
# When monitoring mode is enabled, CastleDevise sends
# requests to Castle but it doesn't act on the "deny" verdicts.
#
# This is useful when you want to check out how Castle scores
# your traffic without blocking any of your users.
#
# Once you are ready to use Castle as your security provider,
# you can set monitoring_mode to false.
config.monitoring_mode = true
end
Add :castle_protectable
Devise module to your User model:
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:castle_protectable # <--- add this
end
Add an additional translation to your config/locales/devise.en.yml
:
en:
devise:
registrations:
blocked_by_castle: "Account cannot be created at this moment. Please try again later."
(See devise.en.yml in our specs)
Further steps if you're not using Webpacker
Include Castle's c.js script in the head section of your layout:
<%= castle_javascript_tag %>
Add the following tag to the the <form>
tag in both devise/registrations/new.html.erb
and devise/sessions/new.html.erb
(if you haven't generated them yet, run rails generate devise:views
):
<%= form_for @user, html: { onsubmit: castle_on_form_submit } do |f| %>
…
<% end %>
You're set! Now verify that everything works by logging in to your application as any user. You should be able to see that User on the Castle Users Page
Further steps if you're using Webpacker
Add @castleio/castle-js
to your package.json file:
yarn add @castleio/castle-js
configure castle in your application pack:
import * as Castle from '@castleio/castle-js'
Castle.configure(YOUR_APPLICATION_ID);
for advanced configuration follow the readme
How-Tos
Customize the login flow
Do something after Castle denies a login
We aim to provide sensible defaults, which means that when Castle denies a login, your application will behave as if the User has not been authenticated. You might still want to log such an event, and you can do this in a Warden hook:
Warden::Manager.before_failure do |env, opts|
# The raw Castle response if a request to Castle has been made
castle_response = env["castle_devise.risk_response"]
# CastleDevise::Context, if a request to Castle has been made
castle_context = env["castle_devise.risk_context"]
if castle_response&.dig(:policy, :action) == "deny"
# auth failed because Castle denied
end
end
Implement your own challenge flow or do something after an "allow" action
In your SessionsController
:
class SessionsController < Devise::SessionsController
def create
super do |resource|
if castle_challenge?
# At this point a User is already authenticated, you might want so sign out:
sign_out(resource)
# .... write your own MFA flow
# You can call #castle_risk_response to access Castle response
# see https://docs.castle.io/v1/reference/api-reference/#risk for details
# Fetch the Device token to use it for user feedback
# https://docs.castle.io/v1/tutorials/advanced-features/end-user-feedback
device_token = castle_risk_response.dig(:device, :token)
# You might want to fetch our risk signals as well
# https://docs.castle.io/v1/reference/signals/
event_signals = castle_risk_response[:signals].keys
return
end
# do any other action you'd like to perform after a user has been signed in below
end
end
end
Please note that some Devise extensions might completely override Devise::SessionsController#create
.
In this case, you have to handle everything manually - castle_challenge?
should be called after
a call to warden.authenticate!
has been successful.
Do not sent login/registration events
You can configure CastleDevise not to send login or registration events for a given Devise model:
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:castle_protectable,
castle_hooks: {
# set it to false to prevent CastleDevise
# from sending filter($login)
before_registration: true,
# set it to false to prevent CastleDevise from
# sending risk($login) and log($login, $failed)
after_login: true,
# set it to false to prevent CastleDevise from
# sending log($password_reset_request)
after_password_reset_request: true
}
end
Intercept request/response
You can register before- and after- request hooks in CastleDevise.
CastleDevise.configure do |config|
# Add custom properties to the request but only when sending
# requests to the Risk endpoint
# action - Castle API endpoint (eg. :risk, :filter, :log)
# context - CastleDevise::Context
# payload - Hash (payload passed to the Castle SDK)
config.before_request do |action, context, payload|
if action == :risk
payload[:properties] = {
from_eu: context.resource.ip.from_eu?
}
end
end
config.before_request do |action, context, payload|
# you can register multiple before_request hooks
end
# Intercept the response - enrich your logs with Castle signals
config.after_request do |action, context, payload, response|
Logging.add_tags(response[:signals].keys)
end
end
Development
Setup
bundle install
Running tests
Most of the specs should pass just by running the following command:
bundle exec rake
We also have a few VCR tests that will periodically rebuild the cassettes just to make sure that the integration with Castle API is working. For those, you need to run your specs with a proper Castle API Secret:
CASTLE_API_SECRET=your_api_secret bundle exec rake