Using Duo MFA with Devise for Rails
Roll your own two-factor authentication solution
Duo is a really nifty, easy-to-use service. And Devise is the undisputed heavyweight Rails authentication engine. So, I figured it’d be easy to find out how to make them work together.
Turns out, I was wrong. (It’s almost 2019, for crying out loud!)
Here, I’ll try and help fix that.
Credit where it’s due
Actually, this post by TJ Oyeniyi did end up proving very useful, but it’s behind a paywall, and you can’t copy/paste the code. So I figured presenting some similar information for free, with my own spin, and in an easier-to-copy format, would be worthwhile. (But still, maybe thank him if you can.)
Okay, setting up…
First things first. Install the Duo gem: gem install duo_web
. Or put it in your Gemfile: gem 'duo_web'
. Then set some environment variables, either in your .env
file or as Heroku config vars or whatever.
DUO_INT_KEY=your_identity_key_from_duo DUO_SECRET_KEY=your_secret_key_from_duo DUO_APP_KEY=your_secret_string # Generate with SecureRandom.gen_random(40)
DUO_HOST=api-123abc.duosecurity.com
Controllers
To handle the actions, create a RegistrationsController
that inherits from your Devise::RegistrationsController
and add the following:
class RegistrationsController < Devise::RegistrationsController
skip_before_action :require_no_authentication, only: [:verify_duo]
skip_before_action :verify_authenticity_token
def connect_with_duo
@sig_request = Duo.sign_request(ENV["DUO_INT_KEY"], ENV["DUO_SECRET_KEY"], ENV["DUO_APP_KEY"], current_user.email)
end
def verify_duo
@authenticated_user = Duo.verify_response(ENV["DUO_INT_KEY"], ENV["DUO_SECRET_KEY"], ENV["DUO_APP_KEY"], params['sig_response'])
if @authenticated_user
session[:duo_authentication] = true
redirect_to some_path_you_want
else
redirect_to some_other_path_you_want
end
end
end
In your ApplicationController
you can check to see if the session[:duo_authentication]
variable is set.
def confirm_duo_authentication
if !session[:duo_authentication]
redirect_to connect_with_duo_path
end
end
You can invoke this method from any individual controller. Or you can require it for all controllers by putting the following at the top of your ApplicationController
:
before_action :confirm_duo_authentication
Routing
Assuming you’ve already got Devise routes up and running, just include a few more lines to the relevant controller actions, above.
devise_scope :user do
post 'registrations/verify_duo', to: 'registrations#verify_duo', as: :verify_duo
get 'registrations/connect_with_duo', to: 'registrations#connect_with_duo', as: :connect_with_duo
authenticated do
root :to => "registrations#connect_with_duo"
end
end
Views
The last thing you’ll have to do is allow a user to actually use the Duo setup. For that you’ll need a view corresponding to your controller action; in this case, at views/registrations/connect_with_duo.html.erb
. You can pipe in the Duo magic using JavaScript, and display it via an iframe:
<script src="https://api.duosecurity.com/frame/hosted/Duo-Web-v2.js" type="text/javascript"></script><iframe id="connect_with_duo" data-host="<%= ENV["DUO_HOST"] %>" data-sig-request="<%= @sig_request %>" data-post-action="/registrations/verify_duo"></iframe>
You’ll need to stylize your iframe to work with your app (setting min-width
and min-height
most importantly).
Flexible implementation
You can easily determine where and when the Duo connection setup takes effect. If you’re working with users in an existing app and want them to configure Duo for the first time, that’s possible, too. Playing with the ApplicationController
, after_sign_in_path_for()
, and the routes are all that’s required.
This setup also works with Duo Access Gateway, which makes it easy to tap into existing directory credentials for things like Microsoft Active Directory or Google Apps with SAML.
The whole thing
Here’s a gist of the whole thing. Enjoy!