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 (4)
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.
Where is the strategy method, im getting an error in that line strategy.authorize. undefined local variable or method `strategy' thanks