Ruby on Rails web authentication with devise is easy to get started with, but how can we handle authentication with devise and doorkeeper in a rails api. This article focuses on steps to add authentication after creating a new project.
Buckle up, required gems installation, Starting with devise
.
# add to Gemfile
gem 'devise'
# install gem
bundle install
# devise initialization.
# for more context on devise refer to official
rails generate devise:install
# generate User model migration.
rails generate devise User
# Run rails migration
rails db:migrate
# add doorkeeper to Gemfile
gem 'doorkeeper'
# install gem
bundle install
# initialize doorkeeper
rails generate doorkeeper:install
# Above command integrate doorkeeper into your
# project and create and initializer file in
# config/initializers/doorkeeper.rb, add doorkeeper
# routes to your route file(config/routes.rb) and locale
# file in config/locales/doorkeeper.en.yml
# for more insight check official docs.
# generate doorkeeper migration
rails generate doorkeeper:migration
# run rails migration
rails db:migrate
We need two(2) routes for login
and signup
. In config/routes.rb
lets comment out use_doorkeeper
and devise routes. Create new routing to look something like this.
# config/routes.rb
# comment or remove doorkeeper routes
# use_doorkeeper
# not exposing devise routes
devise_for :users, only: []
# our auth routes auth/signup and auth/login
scope "auth" do
post "/signup", to: "auth#signup"
post "/login", to: "auth#login"
end
doorkeeper configuration needs some adjustment since we're using custom routes.
# config/initializers/doorkeeper.rb
Doorkeeper.configure do
# Change the ORM that doorkeeper will use (requires ORM extensions installed).
# Check the list of supported ORMs here: https://github.com/doorkeeper-gem/doorkeeper#orms
orm :active_record
# This block will be called to check whether the resource owner is authenticated or not.
resource_owner_authenticator do
current_user || warden.authenticate!(scope: :user)
end
resource_owner_from_credentials do |_routes|
User.authenticate!(params[:email], params[:password]) # we need to add this method in our user model
end
grant_flows %w[authorization_code client_credentials password]
use_refresh_token
client_credentials :from_basic, :from_params
access_token_methods :from_bearer_authorization, :from_access_token_param, :from_bearer_param
access_token_expires_in 1.hour
end
lets adjust User
model to include authenticate!
method
# app/models/user.rb
class User < ApplicationRecord
# add this method to find and authenticate users
def self.authenticate!(email, password)
user = find_by(email: email.downcase)
user if user&.valid_password?(password)
end
end
One of the very capabilities of doorkeeper is the ability to manage multiple platforms, like a doorkeeper client app for android
, web
etc. In our database seed (db/seeds.rb) lets create an app for our IOS app like this.
# db/seeds.rb
Doorkeeper::Application.find_or_create_by(name: "IOS APP") do |app|
app.redirect_uri = "urn:ietf:wg:oauth:2.0:oob"
app.secret = "my_secret"
app.uid = "my_uid"
app.save!
end
run rails db:seed
to insert above doorkeeper app into our database.
Now we need auth
controller to be able to handle requests from our signup and login routes. create a file app/controllers/auth_controller.rb
# app/controllers/auth_controller.rb
# signup method
def signup
client_app = Doorkeeper::Application.find_by(uid: params[:client_id])
unless client_app
return render json: { error: I18n.t("doorkeeper.errors.messages.invalid_client") },
status: :unauthorized
end
@user = User.new(user_params)
@user.save
unless @user
render json: { message: "registration failed" }, status: :unprocessable_entity
end
@results = model_results(@user, client_app)
@results
end
def login
response = strategy.authorize
@token = response.status == :ok ? response.token : nil
if @token&.resource_owner_id
@user ||= User.find(@token.resource_owner_id)
end
self.response_body =
if response.status == :unauthorized
render json: {error: "unauthorized" }, status: 404
else
user_json(@user, @token)
end
end
private
# for a more cleaner approach, separate this into concerns or an isolated class.
def model_results(user, client_app, token_type = "Bearer")
access_token = Doorkeeper::AccessToken.find_or_create_for(
resource_owner: user.id,
application: client_app,
refresh_token: generate_refresh_token,
expires_in: Doorkeeper.configuration.access_token_expires_in.to_i,
scopes: ""
)
return { user: user, tokens: {refresh_token: access_token.refresh_token, access_token: access_token.token }
end
def generate_refresh_token
loop do
token = SecureRandom.hex(32)
break token unless Doorkeeper::AccessToken.exists?(refresh_token: token)
end
end
def user_json(user, token)
{
user: user,
auth: {
access_token: token.token,
refresh_token: token.refresh_token
}
}.to_json
end
def user_params
params.require(:user).permit(:email ,:password, :password_confirmation)
end
Finally how do we secure endpoints?
For any controller actions we desire to protect, we place this in our controller.
class ProfileController < ApplicationController
# this protects profile actions.
before_action :doorkeeper_authorize!
def create
end
end
Happy Coding 🎉
Top comments (3)
If you're a reader stumbling upon this, there will be many changes and fixes required to make this implementation function properly.
Can you be more descriptive. Thanks
rails g doorkeeper:install
. It’s been brought up in doorkeeper’s GitHub as an issue. The work-around is creating the initializer manually with an emptyDoorkeeper.configure do; end
block and then runningrails g doorkeeper:install
. You might make a note in the post about it as no fix has been committed.user_json
method is used as though it accepts arguments. But the method is not written to accept any arguments.user_params
method should require:auth
and not:users
.signup
method, you are not rendering themodel_results
as json, so nothing comes across in the response.I don't see how any of your code could have worked as-is. Typically, for tutorials like this, its a good idea to publish a repository of the working code just in case there are dependency issues or caveats and considerations not covered in the tutorial.