Using Duo MFA with Devise for Rails

Roll your own two-factor authentication solution

Andrew Haines
Fiat Insight
3 min readOct 22, 2018

--

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!

--

--