Rom-rails Authenticating With Omniauth

May 10, 2018

This is the second step of a walkthrough of setting up a rom/rails project. The goal here is to add and configure an omniauth integration, pulling and storing user authentication data. I’ll also show how to restrict authentication to a particular domain.

This is a follow-on to step one where we initialize and configure a new rails app with rom-rails.

Installation

Omniauth is a ruby gem for interacting with multiple OAuth providers. It is one of the available backends for devise, and provides adapters for working single-signon-providers such as Google, Facebook, Twitter and Github.

# Gemfile

gem "omniauth"
gem "omniauth-google-oauth2"
gem "warden"

I’m going to be using the google authentication strategy because the site I’m building happens to handle it’s email via google apps, I’ll be able to wire up the application to only accept auth requests from my specific domain. If I were writing a more open application, I could also add authentication strategies to pull from Facebook, Twitter, Github, or even (for those who prefer to avoid centralized accounts) a locally stored username and password.

For a start, though, I’m just going to wire up the developer strategy.

Connecting authentication

I’ll use an initializer to configure my omniauth middleware:

# config/initializers/authentication.rb

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :developer unless Rails.env.production?
end

From here, if we start the Rails server, we can hit /auth/developer and receive a prompt for name and email. What we get back is the absolute basic amount of detail provided by OAuth, sent to a callback controller. We’ll need to actually do something with that, since we haven’t yet created a controller.

Generally, the Omniauth readme serves as a good guide here. We’ll create a sessions controller, and then use that to register authentication information. For the purposes of checking out what’s going on, our create method is, for the moment, going to skip the usual post-and-redirect pattern. Worry not, this will change later.

# config/routes.rb
Rails.application.routes.draw do
  # ...
  match "auth/:provider/callback", to: "sessions#create", via: %i[get post]
end


# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  protect_from_forgery except: "create"
  def create
    @auth = request.env['omniauth.auth'].to_h
  end
end

# app/views/sessions/create.html.erb
<pre>

</pre>

A run through this controller produces:

{"provider"=>"developer", "uid"=>"[email protected]", "info"=>#<OmniAuth::AuthHash::InfoHash email="[email protected]" name="Chris Flipse">, "credentials"=>#<OmniAuth::AuthHash>, "extra"=>#<OmniAuth::AuthHash>}

For the full list of what’s possible under info, credentials and extra, we can look at the schema It’s worth noting that only the provider, uid and info[name] keys are required. Literally everything else is gravy. When we add providers, it’s important to check and see what is given.

The simplest thing

The absolute simplest thing that could possibly work:

# app/controllers/application_controller.rb
helper_method :current_user, :logged_in?

def current_user
  @current_user ||= session[:current_user]
end

def logged_in?
  current_user.present?
end

def login_required
  redirect_to new_session_url unless logged_in?
end


# app/controllers/sessions_controller.rb

def create
  session[:current_user] = request.env['omniauth.auth'].uid
  redirect_to "/"
end

login_required, when used as a filter, checks to see if there is a current user, which translates to looking for a session key. If one isn’t found, bounce to new_session_url, which will be a simple page linking to the different OAuth authentication endpoints we’ll support.

There’s nothing necessarily wrong with this, but it lacks permanence. Authenticated users exist only within the session; we’re forced to use the uid parameter, which may not be the same between providers; there’s no way to realize that a twitter auth and a gmail auth are the same identity … it’s completely ephemeral. We want better tracking, and better control. Plus, there’s that profiles table we created last session …

A cleaner way

The Warden gem is a rack-based middleware for persisting authentication. Importanly, it does not handle authentication; it leaves that responsibility to you, the user. Finding and providing seams like this is fantastic. They give us focal points to help reduce complexity, and easy locations to swap behavior at a later time.

To make use of Warden, we’ll have to connect it to our middleware and define a strategy:

# config/initializers/authentication.rb

Rails.application.configure.middleware.use Warden::Manager do |manager|
  manager.default_strategies :omniauth
  manager.failure_app = ->(env) { SessionsController.action(:new).call(env) }
end

Warden::Manager.serialize_into_session do |user|
  [user.provier, user.uid]
end

Warden.serialize_from_session do |keys|
  keys.last
end

Warden::Strategies.add(:omniauth) do
  def omniauth
    request.env['omniauth.auth']
  end

  def valid?
    omniauth.present?
  end

  # rubocop:disable Style/SignalException
  #   fail has been overriden with a specific, non-exception meaning for warden
  def autheticate!
    if omniauth.info.email.blank?
      fail "no email found!"
    else
      success!  omniauth
    end
  end
  # rubocop:enable Style/SignalException
end

This configures a warden strategy using omniauth in our initializers. Note that it’s not doing anything more clever than in the in-rails variant above; we’re still just caring that a valid omniauth hash has been provided, and still just returning the uid as our user.

Also important to note: Since this is in a serializer, Rails will not reload when changes have been made; you’ll have to restart your rails server when editing this file.

The controller changes become:

# app/controllers/application_controller.rb
helper_method :logged_in?, :current_user
def warden
  request.env["warden"]
end

def current_user
  warden.user
end

def logged_in?
  warden.authenticate
end

def login_required
  warden.authenticate!
end

# app/controllers/sessions_controller.rb
def new
  flash.now[:alert] = warden.message if warden.message.present?
end

def create
  warden.authenticate!
  redirect_to "/"
end

def destroy
  warden.logout
  redirect_to "/"
end

This constitutes the very narrow interface between our rails app and warden. It seems slightly like overkill at the moment, but there are effectively two seams here: The first is between omniauth and warden, and the second is between warden and rails. If we want to add new authentication strategies, our rails code needs to know nothing about it, and in tests, we can bypass the mechanics of authentication entirely by hooking in to warden.

Connecting to something real

So, let’s see what it takes to go from our demo app to something real. I’m going to use omniauth’s google authenticator and add show how to add some domain filtering to that.

Configuring Omniauth to talk to google’s authentication provider is relatively straightforward, using the omniauth-google-oauth2 gem. The instructions on that page should be kept up to date with google’s current process for aquiring application tokens, so I’m not going to repeat that here.

There are a couple of places that one can stash the tokens so that they’re available to the runtime, but not persisted in git. Rails has a new credentials feature that seems designed for this sort of thing, however I prefer that my tokens never end up in the same repository, encrypted or not. dotenv supports a .env.development.local override file, and I prefer to keep credentials here. Sure, it’s slightly more work when cloning a project and setting up a new environment, but it also reduces the chances of the wrong credentials leaking.

Add the folllowing configuration to config/initialiers/authentication.rb

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :developer unless Rails.env.production?
  provider :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET']
end

And add a link to the authenticator on our login page:

<a href="/auth/google_oauth2">Google Auth</a>

This adds a link to the login page which will, when clicked, send the user to the google accounts page, where the user’s various gmail accounts are presented. Selecting one will then redirect the user back to our demo site, where we will see a much greater amount of OAuth data than was available to our development shim.

Filtering authentication domains

If you’re building an internal service, there is a useful shorthand that can be configured here. Assuming my company is using devcaffeine.com and that we’re using google provided services, we can scope things down so that only internal users are granted access to the application.

In the authentication configuration, I’ll add an additional validation check:

Warden::Strategies.add(:omniauth) do
  # ...
  def authenticate!
    if omniauth.info.email.blank?
      fail "no email found!"
    elsif !omniauth.info.email.match(/@devcaffeine.com$/)
      fail "not authorized for your domain!"
    else
      success! omniauth
    end
  end
end

With this change, attempting to authenticate with your plain @gmail.com, address will be rejected and return a warning message to the user. This is easy to test at the moment, before we trigger the next step:

# spec/system/authentication_system_spec.rb
require "rails_helper"

RSpec.describe "Authentication filtering", type: :system do
  before do
    driven_by(:rack_test)
  end
  it "allows authentication by domain users" do
    visit "/auth/developer"

    fill_in "Name", with: "John Doe"
    fill_in "Email", with: "[email protected]"

    click_button "Sign In"

    expect(page).to have_content("Welcome John Doe")
  end

  it "refuses authentication from unknown domains" do
    visit "/auth/developer"

    fill_in "Name", with: "Mike Smith"
    fill_in "Email", with: "[email protected]"

    click_button "Sign In"

    expect(page).to have_content("not authorized for your domain!")
  end
end

Lastly: It’s annoying to get prompted for a bunch of options when only one is valid. Fortunately, there is a configuration option we can use here: hd restricts the authenticator to a particular hosted domain:

Rails.application.config.middleware.use OmniAuth::Builder do
  # ...
  provider :google_oauth2, ENV['GOOGLE_CLIENT_ID'], ENV['GOOGLE_CLIENT_SECRET'],
    hd: 'devcaffeine.com'
end

Using this option skips the account prompt for currently logged in users, and if not logged in, presents a login prompt for the correct domain, presenting a much smoother process for focused authentication schemes.

Now that we are able to pull in authentication information, we need a way to keep track of it. I’ll cover the process of writing and persisting all this data using rom-sql and rom-repository in the next step…