How to build an OAuth provider with User management in Ruby on Rails using Devise, Doorkeeper, ActiveAdmin and Cancancan

Henry S
Henry S
Apr 26 · 8 min read
Image for post
Image for post
Photo by Micah Williams on Unsplash

I was recently given a task to build an Identity service on a short notice with Ruby on Rails. No fancy CSS in this prototype. This service will integrate authentication and authorization.

Project requirement:
* User can visit authorized apps after sign-up/sign-in from Identity service
* User can manage other users in the same company
* User can manage associated companies
* Admin can manage all Users, Companies and OAuth applications

This is a “How-to” tutorial. I assume you have a basic understanding of what OAuth2 is and the difference between authentication and authorization.

Gems used:
* Devise: Users authentication. (It’s for users to login and authenticate.)
* Doorkeeper: OAuth 2 provider. (In a nutshell, it’s for applications to authenticate.)
* ActiveAdmin: Provide administration interfaces for user managements.
* Cancancan: Users authorization. We will have 2 roles: User and Admin.
* Httparty: makes http requests

Later, I will build two client applications that auth against this Identity service to tie everything together.

The Goal:

  1. User visit a client app. (I will call it “Client-One”)
  2. Redirect user to the Identity sign-in page
  3. User can sign-up or sign-in
  4. OAuth takes place between Identity and the client app behind the scene
  5. When the dance is finished, redirect user back to the client app with an access token.
  6. User visit the second client app. (I will call it “Client-Two”)
  7. Since user has already logged in, it will redirect user to the second app directly.

Contents:
* Step 1 to Step 8: Identity Service
* Step 9 to Step15: Client applications
* Step 16 to Step19: Cancancan and ActiveAdmin
* Github repos and Demo apps

Let’s start with the Identity Service:

Step 1: Build a new Rails app. (I’m on Rails 5.2.4.2 at the time of writing.)

runrails new Identity --database=postgresql -T

cd into the project

Step 2: Devise

Add Device to Gemfile. gem 'devise' and run the following:

1. bundle install

2. rails g devise:install

3. rails g model User and rails g devise User

4. bundle exec rake db:create and bundle exec rake db:migrate

Step 3: Doorkeeper

Add Doorkeeper to Gemfile. gem 'doorkeeper' and run the following:

  1. bundle install
  2. rails g doorkeeper:install
  3. rails g doorkeeper:migration
  4. Open the newly generated migration file, _create_doorkeeper_tables.rb Uncomment the twoadd_foreign_key lines at the bottom. Replace <model> with :users.
  5. bundle exec rake db:migrate

Step 4: Setup Doorkeeper

In doorkeeper.rb , replace resource_owner_authenticator block with:

resource_owner_authenticator do
if user_signed_in?
current_user
else
# Devise: store last url as long as it isn't a /users path
session[:previous_url] = request.fullpath unless request.fullpath =~ /\/users/
redirect_to(new_user_session_path)
end
end

If user is logged in, return current_user (`current_user` is Devise method). If not, store the original request url in a session and send user to the sign-in page. After user signed in, the session[:previous_url] will be used to redirect user back to where they came from by the adding the following in application_controller.rb :

  def after_sign_in_path_for(_resource)
session[:previous_url] || home_path
end

Step5: Home page

Let’s setup a home screen and controller.

In routes.rb add get '/', to: 'home#index', as: :home

Create a new file called /app/controllers/home_controller.rb and add the following:

class HomeController < ApplicationController
def index
end
end

Create a new file called /app/view/home/index.html.erb and add the following:

<div>
<div>
<h1>Welcome to Auth Manager</h1>
</div>
<div>
<% if current_user.nil? %>
<%= link_to 'Sign in', new_user_session_path %>
<% else %>
<h2>Hello <%= current_user.email %></h2>
<%= link_to 'Admin', '/admin' %> &nbsp;&nbsp; | &nbsp;&nbsp;
<%= link_to 'Applications', '/oauth/applications' %> &nbsp;&nbsp; | &nbsp;&nbsp;
<%= link_to destroy_user_session_path, method: :delete do %>Log out<% end %>
<% end %>
</div>
</div>

Now, fire up rails server and visit localhost:3000 . You should see the sign in page. Time to sign up an account for yourself!

You should see something like this after successful login.

Image for post
Image for post

Step 6: Add admin to user table

run rails g migration AddAdminToUser admin:boolean

open up the migration file and add ,default: false to the end of the add_column line

run rake db:migrate

Make yourself an admin! run

  1. bundle exec rails c
  2. User.last.update(admin:true)

Step 7: More Doorkeeper setup

Open doorkeeper.rb . Add the following after the resource_owner_authenticator block:

admin_authenticator do |_routes|
current_user.try(:admin) ? current_user : redirect_to(new_user_session_path)
end
skip_authorization do
# allow all client apps to be "trusted"
true
end
default_scopes :public

This allows admin to visit the Applications dashboard.

Fire up rails server, visit http://localhost:3000/oauth/applications, you should see a page where you can manage client applications. We will come back to this page later to add applications.

Step 8: Add a protect resource endpoint for the clients

Client apps can call this endpoint for user’s (resource owner) info.

In routes.rb , add the following:

namespace :api do
resources :users, only: [:user] do
collection do
get :owner
end
end
end

Create a new file /app/controllers/api/users_controller.rb , add the following:

module Api
class UsersController < ApplicationController
before_action :doorkeeper_authorize!
respond_to :json def owner
respond_with current_resource_owner
end
private def current_resource_owner
# find logged in user (via devise) if doorkeeper token
User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token
end
end
end

Let’s build the client app to talk to our Identity service.

Step 9: Build a new Rails app. (I’m on Rails 5.2.4.2 at the time of writing.)

run rails new client-one --database=postgresql -T

run bundle exec rake db:create and bundle exec rake db:migrate

Step 10: Add httparty

Open Gemfile , add gem 'httparty' and run bundle install

Step 11: Update routes

Open routes.rb and add:

root to: 'home#index'
get '/oauth/callback', to: 'home#create_session'

Step 12: Create home controller

create /app/controller/home_controller.rb and add the following:

class HomeController < ApplicationController  def index
if session[:access_token]
@owner = HTTParty.get "#{ENV['OAUTH_PROVIDER_URL']}/api/users/owner.json", { query: { access_token: session[:access_token]} }
else
redirect_to "#{ENV['OAUTH_PROVIDER_URL']}/oauth/authorize?client_id=#{ENV['CLIENT_TOKEN']}&redirect_uri=#{ENV['OAUTH_REDIRECT_URI']}&response_type=code"
end
end
def create_session
response = HTTParty.post("#{ENV['OAUTH_PROVIDER_URL']}/oauth/token", body: "client_id=#{ENV['CLIENT_TOKEN']}&client_secret=#{ENV['CLIENT_SECRET']}&code=#{params[:code]}&grant_type=authorization_code&redirect_uri=#{ENV['OAUTH_REDIRECT_URI']}")
session[:access_token] = response['access_token']
redirect_to root_path
end
end

If user visit page with access token, call Identity and fetch resource owner’s info. If user visit page without a token, return user to Identity to sign-in.

create_session is the callback for the auth-code/token exchange.

Step 13: Create a home view

create /app/views/home/index.html.erb and add the following:

<h1>Client ONE</h1>
<% if session[:access_token] %>
<p>You are log in as <%= @owner['email']%></p>
<p><%= link_to 'Client-Two', ENV['CLIENT_TWO_URL'] %></p>
<% else %>
<%= link_to 'Login', ENV['OAUTH_PROVIDER_URL'] %>
<% end %>

Step 14: Register this new client in Identity service

Fire up Identity and visit http://localhost:3000/oauth/applications

Now you can add a new application.
* Name: “Client One”
* Redirect URI: “http://localhost:3001/oauth/callback”
Click “Submit” and remember the UID and Secret.

Image for post
Image for post

Step 15: environment variables

create a .env file in this project root directory. It looks something like this:

export OAUTH_PROVIDER_URL=http://localhost:3000export CLIENT_ID=< UID from previous step >export CLIENT_SECRET=< SECRET from previous step >export OAUTH_REDIRECT_URI=http://localhost:3001/oauth/callbackexport CLIENT_TWO_URL=http://localhost:3002

goto your terminal and run source .env , it will load these variables.

NOTE: Do not commit your secrets to Github. Put .env in .gitignore

run bundle exec rails s -p 3001 , the client app will run on localhost:3001 and you should see “You are log in as your-email@example.com”

Image for post
Image for post

Repeat Step 9 to Step 15 to create Client app two. You pretty much just have to replace “one” with “two”, “two” with “one”. Run client app two on localhost:3002 . You should be able to visit Client-One and Client-Two.

Now, anyone who signed up for an account can access the Identity main page, which manages users, companies and applications. That’s not ideal! Let’s add Cancancan into Identity so that only admin can access the Applications page.

Step 16: Create acompanies table and reference to user

run rails g model Company name
run rails g migration CreateCompaniesUsers user:references company:references
run rake db:migrate

Step 17: Update models

in /app/models/user.rb add: has_and_belongs_to_many :companies
in /app/models/company.rb add: has_and_belongs_to_many :users

Step 18: Add Cancancan

open Gemfile , addgem 'cancancan' and run bundle install

run rails g cancan:ability

open /app/models/ability.rb replace the whole thing with:

# frozen_string_literal: trueclass Ability
include CanCan::Ability
def initialize(user)
if user.admin?
can :manage, User
can :manage, Company
can :manage, ActiveAdmin::Page, name: "Dashboard", namespace_name: "admin"
else
can :manage, Company, id: user.company_ids
can :create, Company
can :manage, User, id: User.joins(:companies).where(companies: { id: user.company_ids }).ids
can :create, User
can :read, ActiveAdmin::Page, name: "Dashboard", namespace_name: "admin"
end
end
end

Admin has full access to User, Company and manage the ActiveAdmin dashboard.
User can manage users belongs to his/her company, companies belongs to the user and read the ActiveAdmin dashboard.

Step 19: Add ActiveAdmin

open Gemfile ,gem 'activeadmin' and run bundle install

run rails g active_admin:install and rails db:migrate
run rails g active_admin:resource User
run rails g active_admin:resource Company

in routes.rb change

devise_for :admin_users, ActiveAdmin::Devise.config

to

devise_for :users, ActiveAdmin::Devise.config

in /config/initializers/active_admin.rb , replace everything with:

ActiveAdmin.setup do |config|
config.site_title = "Identity"
config.authentication_method = :authenticate_user!
config.authorization_adapter = ActiveAdmin::CanCanAdapter
config.on_unauthorized_access = :access_denied
config.current_user_method = :current_user
config.logout_link_path = :destroy_user_session_path
config.batch_actions = true
config.filter_attributes = [:encrypted_password, :password, :password_confirmation]
config.localize_format = :long
end

Now you should be able to visit http://localhost:3000/admin/users

Let’s customize the view. We don’t need to see every columns.

In /app/admin/companies.rb , replace everything with:

ActiveAdmin.register Company do
permit_params [:name]
index do
column :name
column :users
actions
end
show do
attributes_table do
row :name
row :users
row :created_at
row :updated_at
end
active_admin_comments
end
end

In /app/admin/users.rb , replace everything with:

ActiveAdmin.register User do
permit_params [ :email, :password, :password_confirmation, :admin, company_ids:[] ]
# visible columns on /admin/users
index do
column :email
column :companies
column :admin
actions
end
show do
attributes_table do
row :email
row :companies
row :created_at
row :updated_at
row :reset_password_sent_at
row :reset_password_token
row :admin
end
active_admin_comments
end
# show password fiels only on new user page
# shows companies in downdown menu based on current_users's related companies
form do |f|
f.inputs "User" do
f.input :email
f.input :companies, collection: (Company.joins(:users).where(users:{id: current_user.id}) unless current_user.admin)
if f.object.new_record?
f.input :password
f.input :password_confirmation
end
if current_user.admin
f.input :admin
end
end
f.actions
end
controller do
# bypass password validation when updating user
def update_resource(object, attributes)
update_method = attributes.first[:password].present? ? :update_attributes : :update_without_password
object.send(update_method, *attributes)
end
end
end

Now you should be able to visit http://localhost:3000/admin/dashboard and navigate the “Companies” and “Users” tabs on top.

Image for post
Image for post

Whew! That was a lot! We created an Identity provider that can manage Users, Companies, Applications with five very popular gems.

I know I have covered a lot of grounds without much explanations. Please refer to the gem’s github page for more info. They are pretty well documented. There are tons of options and customization that there is noway to cover them all.

Anyway, I hope this could help anyone out there in someway. You can find the github repos at:
https://github.com/hszeto/client-one
https://github.com/hszeto/client-two
https://github.com/hszeto/identity

Feel free to play around with the demo apps:
username: user@example.com
password: password
https://client-one.herokuapp.com/

https://client-two.herokuapp.com/
https://identityservice.herokuapp.com/

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store