Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ gem 'devise' # Flexible authentication solution for Rails with Warden

# Authentications & Authorizations
gem 'pundit' # Minimal authorization through OO design and pure Ruby classes
gem 'doorkeeper' # OAuth 2 provider for your Rails / Grape app

# Assets
gem 'sassc'
Expand Down
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ GEM
docile (1.4.0)
dockerfile-rails (1.3.0)
rails
doorkeeper (5.6.6)
railties (>= 5)
erubi (1.12.0)
fabrication (2.30.0)
faraday (2.7.5)
Expand Down Expand Up @@ -464,6 +466,7 @@ DEPENDENCIES
devise
discard
dockerfile-rails (>= 1.2)
doorkeeper
fabrication
ffaker
figaro
Expand Down
17 changes: 17 additions & 0 deletions app/controllers/api/v1/application_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module Api
module V1
Copy link
Copy Markdown

@github-actions github-actions Bot Jun 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Has the name 'v1'

class ApplicationController < ActionController::API
# equivalent of authenticate_user! on devise, but this one will check the oauth token
before_action :doorkeeper_authorize!

private

# helper method to access the current user from the token
def current_user
@current_user ||= User.find_by(id: doorkeeper_token[:resource_owner_id])
end
end
end
end
75 changes: 75 additions & 0 deletions app/controllers/api/v1/users_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# frozen_string_literal: true

module Api
module V1
Copy link
Copy Markdown

@github-actions github-actions Bot Jun 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Has the name 'v1'

class UsersController < ApplicationController
skip_before_action :doorkeeper_authorize!, only: %i[create]

def create
Copy link
Copy Markdown

@github-actions github-actions Bot Jun 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Has approx 6 statements

user = build_user
client_app = find_client_application

return render(json: { error: 'Invalid client ID' }, status: :forbidden) unless client_app

if user.save
access_token = create_access_token(user, client_app)
render_success_response(user, access_token)
else
render_failure_response(user)
end
end

private

def user_params
params.permit(:email, :password)
end

def build_user
User.new(email: user_params[:email], password: user_params[:password])
end

def find_client_application
Doorkeeper::Application.find_by(uid: params[:client_id])
end

def generate_refresh_token
loop do
token = SecureRandom.hex(32)
break token unless Doorkeeper::AccessToken.exists?(refresh_token: token)
end
end

def create_access_token(user, client_app)
Doorkeeper::AccessToken.create(
resource_owner_id: user.id,
application_id: client_app.id,
refresh_token: generate_refresh_token,
expires_in: Doorkeeper.configuration.access_token_expires_in.to_i,
scopes: ''
)
end

def render_success_response(user, access_token)
user_data = build_user_data(user, access_token)
render(json: { user: user_data })
end

def build_user_data(user, access_token)
Copy link
Copy Markdown

@github-actions github-actions Bot Jun 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Doesn't depend on instance state (maybe move it to another class?)

{
id: user.id,
email: user.email,
access_token: access_token.token,
token_type: 'bearer',
expires_in: access_token.expires_in,
refresh_token: access_token.refresh_token,
created_at: access_token.created_at.to_time.to_i
}
end

def render_failure_response(user)
render(json: { error: user.errors.full_messages }, status: :unprocessable_entity)
end
end
end
end
2 changes: 2 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@
class ApplicationController < ActionController::Base
include Localization
include Pagy::Backend

before_action :authenticate_user!, unless: -> { is_a?(HealthCheckController) }
end
7 changes: 7 additions & 0 deletions app/models/concerns/authenticable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,12 @@ module Authenticable
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable

# the authenticate method from devise documentation
# todo extract it inside authenticable
def self.authenticate(email, password)
user = User.find_for_authentication(email: email)
user&.valid_password?(password) ? user : nil
Copy link
Copy Markdown

@github-actions github-actions Bot Jun 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Is controlled by argument 'password'

end
end
end
26 changes: 26 additions & 0 deletions config/initializers/doorkeeper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

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
# Put your resource owner authentication logic here.
# Example implementation:
# User.find_by(id: session[:user_id]) || redirect_to(new_user_session_url)
end

resource_owner_from_credentials do |_routes|
User.authenticate(params[:email], params[:password])
end

use_refresh_token
allow_blank_redirect_uri true
grant_flows %w[password]
skip_authorization do |resource_owner, client|
true
end

end
151 changes: 151 additions & 0 deletions config/locales/doorkeeper.en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
en:
activerecord:
attributes:
doorkeeper/application:
name: 'Name'
redirect_uri: 'Redirect URI'
errors:
models:
doorkeeper/application:
attributes:
redirect_uri:
fragment_present: 'cannot contain a fragment.'
invalid_uri: 'must be a valid URI.'
unspecified_scheme: 'must specify a scheme.'
relative_uri: 'must be an absolute URI.'
secured_uri: 'must be an HTTPS/SSL URI.'
forbidden_uri: 'is forbidden by the server.'
scopes:
not_match_configured: "doesn't match configured on the server."

doorkeeper:
applications:
confirmations:
destroy: 'Are you sure?'
buttons:
edit: 'Edit'
destroy: 'Destroy'
submit: 'Submit'
cancel: 'Cancel'
authorize: 'Authorize'
form:
error: 'Whoops! Check your form for possible errors'
help:
confidential: 'Application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential.'
redirect_uri: 'Use one line per URI'
blank_redirect_uri: "Leave it blank if you configured your provider to use Client Credentials, Resource Owner Password Credentials or any other grant type that doesn't require redirect URI."
scopes: 'Separate scopes with spaces. Leave blank to use the default scopes.'
edit:
title: 'Edit application'
index:
title: 'Your applications'
new: 'New Application'
name: 'Name'
callback_url: 'Callback URL'
confidential: 'Confidential?'
actions: 'Actions'
confidentiality:
'yes': 'Yes'
'no': 'No'
new:
title: 'New Application'
show:
title: 'Application: %{name}'
application_id: 'UID'
secret: 'Secret'
secret_hashed: 'Secret hashed'
scopes: 'Scopes'
confidential: 'Confidential'
callback_urls: 'Callback urls'
actions: 'Actions'
not_defined: 'Not defined'

authorizations:
buttons:
authorize: 'Authorize'
deny: 'Deny'
error:
title: 'An error has occurred'
new:
title: 'Authorization required'
prompt: 'Authorize %{client_name} to use your account?'
able_to: 'This application will be able to'
show:
title: 'Authorization code'
form_post:
title: 'Submit this form'

authorized_applications:
confirmations:
revoke: 'Are you sure?'
buttons:
revoke: 'Revoke'
index:
title: 'Your authorized applications'
application: 'Application'
created_at: 'Created At'
date_format: '%Y-%m-%d %H:%M:%S'

pre_authorization:
status: 'Pre-authorization'

errors:
messages:
# Common error messages
invalid_request:
unknown: 'The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.'
missing_param: 'Missing required parameter: %{value}.'
request_not_authorized: 'Request need to be authorized. Required parameter for authorizing request is missing or invalid.'
invalid_redirect_uri: "The requested redirect uri is malformed or doesn't match client redirect URI."
unauthorized_client: 'The client is not authorized to perform this request using this method.'
access_denied: 'The resource owner or authorization server denied the request.'
invalid_scope: 'The requested scope is invalid, unknown, or malformed.'
invalid_code_challenge_method: 'The code challenge method must be plain or S256.'
server_error: 'The authorization server encountered an unexpected condition which prevented it from fulfilling the request.'
temporarily_unavailable: 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.'

# Configuration error messages
credential_flow_not_configured: 'Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.'
resource_owner_authenticator_not_configured: 'Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfigured.'
admin_authenticator_not_configured: 'Access to admin panel is forbidden due to Doorkeeper.configure.admin_authenticator being unconfigured.'

# Access grant errors
unsupported_response_type: 'The authorization server does not support this response type.'
unsupported_response_mode: 'The authorization server does not support this response mode.'

# Access token errors
invalid_client: 'Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.'
invalid_grant: 'The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.'
unsupported_grant_type: 'The authorization grant type is not supported by the authorization server.'

invalid_token:
revoked: "The access token was revoked"
expired: "The access token expired"
unknown: "The access token is invalid"
revoke:
unauthorized: "You are not authorized to revoke this token"

forbidden_token:
missing_scope: 'Access to this resource requires scope "%{oauth_scopes}".'

flash:
applications:
create:
notice: 'Application created.'
destroy:
notice: 'Application deleted.'
update:
notice: 'Application updated.'
authorized_applications:
destroy:
notice: 'Application revoked.'

layouts:
admin:
title: 'Doorkeeper'
nav:
oauth2_provider: 'OAuth2 Provider'
applications: 'Applications'
home: 'Home'
application:
title: 'OAuth authorization required'
10 changes: 10 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@

Rails.application.routes.draw do
use_doorkeeper do
skip_controllers :authorizations, :applications, :authorized_applications
end
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
devise_for :users

Expand All @@ -9,4 +12,11 @@

get "/health_check", to: 'health_check#health_check', as: :rails_health_check
resources :search_stats, only: [:index, :show]

namespace :api do
namespace :v1 do
# User sign_up
resources :users, only: :create
end
end
end
Loading