Skip to content

Commit

Permalink
Remove database authentication and replace with omniauth
Browse files Browse the repository at this point in the history
  • Loading branch information
pixeltrix committed Jan 26, 2024
1 parent 902c078 commit 7fd91b5
Show file tree
Hide file tree
Showing 23 changed files with 104 additions and 498 deletions.
3 changes: 2 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ gem 'rails', '6.1.7.6'
gem 'rake'
gem 'pg'
gem 'devise'
gem 'devise-encryptable'
gem 'will_paginate'
gem 'json'
gem 'delayed_job_active_record'
Expand All @@ -35,6 +34,8 @@ gem 'image_processing'
gem 'maxminddb'
gem 'redcarpet'
gem 'scrypt'
gem 'omniauth'
gem 'omniauth-rails_csrf_protection'

gem 'aws-sdk-codedeploy'
gem 'aws-sdk-cloudwatchlogs'
Expand Down
15 changes: 12 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,6 @@ GEM
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
devise-encryptable (0.2.0)
devise (>= 2.1.0)
diff-lcs (1.4.4)
docile (1.3.1)
dotenv (2.7.6)
Expand Down Expand Up @@ -174,6 +172,7 @@ GEM
globalid (1.2.1)
activesupport (>= 6.1)
hashdiff (1.0.1)
hashie (5.0.0)
htmlentities (4.3.4)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
Expand Down Expand Up @@ -238,6 +237,13 @@ GEM
racc (~> 1.4)
nokogiri (1.15.4-x86_64-linux)
racc (~> 1.4)
omniauth (2.1.1)
hashie (>= 3.4.6)
rack (>= 2.2.3)
rack-protection
omniauth-rails_csrf_protection (1.0.1)
actionpack (>= 4.2)
omniauth (~> 2.0)
orm_adapter (0.5.0)
pg (1.2.3)
pry (0.13.1)
Expand All @@ -248,6 +254,8 @@ GEM
nio4r (~> 2.0)
racc (1.7.1)
rack (2.2.8)
rack-protection (3.1.0)
rack (~> 2.2, >= 2.2.4)
rack-test (2.1.0)
rack (>= 1.3)
rails (6.1.7.6)
Expand Down Expand Up @@ -396,7 +404,6 @@ DEPENDENCIES
delayed-web
delayed_job_active_record
devise
devise-encryptable
dotenv-rails
email_spec
factory_bot_rails
Expand All @@ -413,6 +420,8 @@ DEPENDENCIES
maxminddb
net-http-persistent
nokogiri
omniauth
omniauth-rails_csrf_protection
pg
pry
puma (< 6)
Expand Down
25 changes: 25 additions & 0 deletions app/controllers/admin/omniauth_callbacks_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class Admin::OmniauthCallbacksController < Devise::OmniauthCallbacksController
skip_before_action :require_admin
skip_before_action :verify_authenticity_token, only: %i[developer]

def developer
@user = AdminUser.find_by(email: omniauth_hash["uid"])

if @user.present?
sign_in_and_redirect @user, event: :authentication
set_flash_message(:notice, :signed_in) if is_navigational_format?
else
redirect_to admin_login_url, alert: :invalid_login
end
end

private

def after_omniauth_failure_path_for(scope)
admin_login_url
end

def omniauth_hash
request.env["omniauth.auth"]
end
end
79 changes: 8 additions & 71 deletions app/models/admin_user.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
class AdminUser < ActiveRecord::Base
DISABLED_LOGIN_COUNT = 5
SYSADMIN_ROLE = 'sysadmin'
MODERATOR_ROLE = 'moderator'
REVIEWER_ROLE = 'reviewer'
ROLES = [SYSADMIN_ROLE, MODERATOR_ROLE, REVIEWER_ROLE]
PASSWORD_REGEX = /\A.*(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[\W_]).*\z/

class CannotDeleteCurrentUser < RuntimeError; end
class MustBeAtLeastOneAdminUser < RuntimeError; end

devise :database_authenticatable, :encryptable, :trackable, :timeoutable, :lockable
devise :trackable, :timeoutable, :omniauthable, omniauth_providers: %i[developer]

# TODO: Drop these columns once rollout of SSO has been completed
self.ignored_columns = %i[
encrypted_password password_salt
force_password_reset password_changed_at
failed_attempts locked_at
]

with_options dependent: :restrict_with_exception do
with_options foreign_key: :moderated_by_id do
Expand All @@ -22,23 +27,10 @@ class MustBeAtLeastOneAdminUser < RuntimeError; end
validates :first_name, :last_name, presence: true
validates :email, presence: true, email: true
validates :email, uniqueness: { case_sensitive: false }
validates :password, presence: true, on: [:create, :update_password]
validates :password, length: { minimum: 8, allow_blank: true }
validates :password, format: { with: PASSWORD_REGEX, allow_blank: true }
validates :password, confirmation: true, on: :update_password
validates :role, inclusion: { in: ROLES }

validate on: :update_password do
errors.add(:current_password, :blank) if current_password.blank?
errors.add(:current_password, :invalid) unless valid_password?(current_password)
errors.add(:password, :taken) if valid_password?(password)
end

# = Callbacks =
before_save :clear_locked_at, if: :enabling_account?
before_save :set_locked_at, if: :disabling_account?
before_save :reset_persistence_token, unless: :persistence_token?
before_update :reset_password_tracking, if: :encrypted_password_changed?

# = Finders =
scope :by_name, -> { order(:last_name, :first_name) }
Expand All @@ -49,11 +41,6 @@ def self.timeout_in
Site.login_timeout.seconds
end

def reset_password_tracking
self.force_password_reset = false
self.password_changed_at = Time.current
end

def reset_persistence_token
self.persistence_token = SecureRandom.hex(64)
end
Expand All @@ -66,28 +53,6 @@ def valid_persistence_token?(token)
persistence_token == token
end

def current_password
defined?(@current_password) ? @current_password : nil
end

def current_password=(value)
@current_password = value
end

def valid_password?(password)
encryptor_class.compare(encrypted_password_in_database, password, nil, password_salt_in_database, nil)
end

def update_password(params)
assign_attributes(params)

save(context: :update_password).tap do
self.current_password = nil
self.password = nil
self.password_confirmation = nil
end
end

def destroy(current_user: nil)
if self == current_user
raise CannotDeleteCurrentUser, "Cannot delete current user"
Expand Down Expand Up @@ -122,10 +87,6 @@ def is_a_reviewer?
self.role == 'reviewer'
end

def has_to_change_password?
self.force_password_reset or (self.password_changed_at and self.password_changed_at < 9.months.ago)
end

def can_take_petitions_down?
is_a_sysadmin? || is_a_moderator?
end
Expand All @@ -142,30 +103,6 @@ def can_moderate_petitions?
is_a_sysadmin? || is_a_moderator?
end

def account_disabled
self.failed_attempts >= DISABLED_LOGIN_COUNT
end

def account_disabled=(flag)
self.failed_attempts = (flag == "0" or !flag) ? 0 : DISABLED_LOGIN_COUNT
end

def enabling_account?
failed_attempts_changed? && failed_attempts.zero?
end

def disabling_account?
failed_attempts_changed? && failed_attempts >= DISABLED_LOGIN_COUNT
end

def clear_locked_at
self.locked_at = nil
end

def set_locked_at(now = Time.current)
self.locked_at = now
end

def elapsed_time(last_request_at, now = Time.current)
(now - last_request_at).floor
end
Expand Down
12 changes: 2 additions & 10 deletions app/views/admin/sessions/new.html.erb
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
<h1>Sign in</h1>
<div class="grid-row">
<div class="column-half">
<%= form_for(resource, as: resource_name, url: admin_login_path) do |f| %>
<div class="form-group">
<%= f.label :email, "Email", class: "form-label" %>
<%= f.text_field :email, :class => 'form-control', :type => 'email', :autofocus => 'autofocus' %>
</div>
<div class="form-group">
<%= f.label :password, "Password", class: "form-label" %>
<%= f.password_field :password, :class => 'form-control' %>
</div>
<%= f.submit 'Sign in', :class => 'button' %>
<%= form_tag(admin_auth_developer_path, method: :post) do %>
<button type="submit" class="button">Login with developer strategy</button>
<% end %>
</div>
</div>
22 changes: 4 additions & 18 deletions config/initializers/devise.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
require 'devise/encryptable/encryptors/authlogic_scrypt'

Devise.setup do |config|
# ==> Controller configuration
config.parent_controller = 'Admin::AdminController'
Expand All @@ -17,29 +15,17 @@
config.case_insensitive_keys = [:email]
config.strip_whitespace_keys = [:email]

# ==> Configuration for :database_authenticatable
config.stretches = Rails.env.test? ? 1 : 12

# ==> Configuration for :validatable
config.password_length = 6..128
config.email_regexp = /\A[^@\s]+@[^@\s]+\z/

# ==> Configuration for :timeoutable
# This is overidden on the AdminUser model to use Site.login_timeout
config.timeout_in = 24.hours

# ==> Configuration for :lockable
config.lock_strategy = :failed_attempts
config.unlock_strategy = :none
config.maximum_attempts = 5
config.last_attempt_warning = true

# ==> Configuration for :encryptable
config.encryptor = :authlogic_scrypt

# ==> Navigation configuration
config.sign_out_via = :get

# ==> Omniauth configuration
config.omniauth_path_prefix = '/admin/auth'
config.omniauth :developer, fields: %i[email]

# ==> Warden configuration
# Reset the token after logging in so that other sessions are logged out
Warden::Manager.after_set_user except: :fetch do |user, warden, options|
Expand Down
3 changes: 3 additions & 0 deletions config/initializers/omniauth.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
OmniAuth.configure do |config|
config.logger = Rails.logger
end
8 changes: 0 additions & 8 deletions config/locales/activerecord.en-GB.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,6 @@ en-GB:
models:
admin_user:
attributes:
current_password:
blank: "Current password can’t be blank"
invalid: "Current password is incorrect"
password:
invalid: "Password must contain at least one digit, a lower and upper case letter and a special character"
taken: "Password is the same as the current password"
password_confirmation:
confirmation: "Password confirmation doesn’t match password"
role:
inclusion: "Role '%{value}' is invalid"

Expand Down
4 changes: 1 addition & 3 deletions config/locales/admin.en-GB.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,7 @@ en-GB:
invalidation_removed: "Invalidation removed successfully"
invalidation_started: "Enqueued the invalidation %{summary}"
invalidation_updated: "Invalidation updated successfully"
last_login: "You have one more attempt before your account is temporarily disabled"
disabled_login: "Failed login limit exceeded, your account has been temporarily disabled"
invalid_login: "Invalid email/password combination"
invalid_login: "Invalid login details"
logged_out: "You have been logged out"
moderator_required: "You must be logged in as a moderator or system administrator to view this page"
moderation_delay_sent: "An email has been sent to creators that moderation has been delayed"
Expand Down
2 changes: 2 additions & 0 deletions config/locales/devise.en-GB.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ en:
other_login: "You have been logged out"
not_found_in_database: "Invalid email/password combination"
timedout: "Your session expired. Please log in again to continue"
omniauth_callbacks:
signed_in: "Logged in successfully"
sessions:
signed_in: "Logged in successfully"
signed_out: "You have been logged out"
Expand Down
7 changes: 5 additions & 2 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -247,12 +247,11 @@

end

devise_for :users, class_name: 'AdminUser', skip: %i[sessions]
devise_for :users, class_name: 'AdminUser', module: 'admin', skip: %i[sessions]

as :user do
controller 'admin/sessions' do
get '/admin/login', action: 'new'
post '/admin/login', action: 'create', as: nil
get '/admin/logout', action: 'destroy'
get '/admin/continue', action: 'continue'
get '/admin/status', action: 'status'
Expand All @@ -263,5 +262,9 @@
# Devise needs a `new_user_session_url` helper for its failure app
direct(:new_user_session) { route_for(:admin_login) }

# Friendly url helpers for Omniauth
direct(:admin_auth_developer) { route_for(:user_developer_omniauth_authorize) }
direct(:admin_auth_developer_callback) { route_for(:user_developer_omniauth_callback) }

get 'ping', to: 'ping#ping'
end
Loading

0 comments on commit 7fd91b5

Please sign in to comment.