Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: SOFIA-local accounts with login (with optional 2fa) #925

Draft
wants to merge 2 commits into
base: staging
Choose a base branch
from
Draft
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
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
source 'https://rubygems.org'

gem 'active_model_otp', '~> 2.3', '>= 2.3.1'
gem 'bcrypt', '~> 3.1.0'
gem 'bootsnap', '~> 1.12.0'
gem 'browser', '~> 5.3.0'
Expand All @@ -18,6 +19,7 @@
gem 'net-pop', require: false
gem 'net-smtp', require: false
gem 'omniauth', '~> 2.0.0'
gem "omniauth-identity", "~> 3.0", ">= 3.0.9"

Check failure on line 22 in Gemfile

View workflow job for this annotation

GitHub Actions / Lint

[Correctable] Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols. (https://rubystyle.guide#consistent-string-literals)

Check failure on line 22 in Gemfile

View workflow job for this annotation

GitHub Actions / Lint

[Correctable] Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols. (https://rubystyle.guide#consistent-string-literals)

Check failure on line 22 in Gemfile

View workflow job for this annotation

GitHub Actions / Lint

[Correctable] Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols. (https://rubystyle.guide#consistent-string-literals)
gem 'omniauth-oauth2', '~> 1.7.0'
gem 'omniauth-rails_csrf_protection', '~> 1.0'
gem 'paper_trail', '~> 14.0.0'
Expand All @@ -30,6 +32,7 @@
gem 'rails-i18n', '~> 7.0.0'
gem 'redis-rails', '~> 5.0.0'
gem 'rest-client', '~> 2.1.0'
gem 'rqrcode', '~> 2.2'
gem 'sassc-rails', '~> 2.1.0'
gem 'sentry-rails', '~> 5.5'
gem 'sentry-ruby', '~> 5.5'
Expand Down
15 changes: 15 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ GEM
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
active_model_otp (2.3.4)
activemodel
rotp (~> 6.3.0)
activejob (7.0.4.3)
activesupport (= 7.0.4.3)
globalid (>= 0.3.6)
Expand Down Expand Up @@ -98,6 +101,7 @@ GEM
capistrano (>= 3.9.0)
capistrano-bundler
sidekiq (>= 6.0)
chunky_png (1.4.0)
coderay (1.1.3)
colorize (0.8.1)
concurrent-ruby (1.2.2)
Expand Down Expand Up @@ -277,6 +281,9 @@ GEM
hashie (>= 3.4.6)
rack (>= 1.6.2, < 3)
rack-protection
omniauth-identity (3.0.9)
bcrypt
omniauth
omniauth-oauth2 (1.7.2)
oauth2 (~> 1.4)
omniauth (>= 1.9, < 3)
Expand Down Expand Up @@ -388,6 +395,11 @@ GEM
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
rexml (3.2.5)
rotp (6.3.0)
rqrcode (2.2.0)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
rspec (3.12.0)
rspec-core (~> 3.12.0)
rspec-expectations (~> 3.12.0)
Expand Down Expand Up @@ -533,6 +545,7 @@ PLATFORMS
x86_64-linux

DEPENDENCIES
active_model_otp (~> 2.3, >= 2.3.1)
awesome_print
bcrypt (~> 3.1.0)
better_errors
Expand Down Expand Up @@ -564,6 +577,7 @@ DEPENDENCIES
net-pop
net-smtp
omniauth (~> 2.0.0)
omniauth-identity (~> 3.0, >= 3.0.9)
omniauth-oauth2 (~> 1.7.0)
omniauth-rails_csrf_protection (~> 1.0)
paper_trail (~> 14.0.0)
Expand All @@ -580,6 +594,7 @@ DEPENDENCIES
rb-readline
redis-rails (~> 5.0.0)
rest-client (~> 2.1.0)
rqrcode (~> 2.2)
rspec-rails
rubocop (~> 1.30.0)
rubocop-performance
Expand Down
8 changes: 8 additions & 0 deletions app/assets/stylesheets/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ a {
}
}

.identity-input {
min-width: 18rem;
}

.qr-code {
max-width: 20rem;
}

.footer {
position: absolute;
bottom: 0;
Expand Down
6 changes: 6 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
class ApplicationController < ActionController::Base
include Pundit

protect_from_forgery with: :exception, prepend: true

before_action :set_sentry_context
before_action :set_paper_trail_whodunnit
before_action :set_layout_flag
Expand Down Expand Up @@ -34,4 +36,8 @@
@show_navigationbar = true
@show_extras = true
end

def normalize_error_messages(full_messages)
return full_messages.map{ |message| message.downcase }.join(", ")

Check failure on line 41 in app/controllers/application_controller.rb

View workflow job for this annotation

GitHub Actions / Lint

[Correctable] Style/RedundantReturn: Redundant return detected. (https://rubystyle.guide#no-explicit-return)

Check failure on line 41 in app/controllers/application_controller.rb

View workflow job for this annotation

GitHub Actions / Lint

[Correctable] Layout/SpaceBeforeBlockBraces: Space missing to the left of {.

Check failure on line 41 in app/controllers/application_controller.rb

View workflow job for this annotation

GitHub Actions / Lint

[Correctable] Style/SymbolProc: Pass &:downcase as an argument to map instead of a block.

Check failure on line 41 in app/controllers/application_controller.rb

View workflow job for this annotation

GitHub Actions / Lint

[Correctable] Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols. (https://rubystyle.guide#consistent-string-literals)
end
end
49 changes: 48 additions & 1 deletion app/controllers/callbacks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,54 @@
sign_in(:user, user)
redirect_to user.roles.any? ? root_path : user_path(user.id)
else
redirect_to root_path, flash: { error: 'Authentication failed' }
redirect_to root_path, flash: { error: 'Inloggen gefaald.' }
end
end

def identity

Check failure on line 13 in app/controllers/callbacks_controller.rb

View workflow job for this annotation

GitHub Actions / Lint

Metrics/AbcSize: Assignment Branch Condition size for identity is too high. [<3, 32, 12> 34.31/17] (http://c2.com/cgi/wiki?AbcMetric, https://en.wikipedia.org/wiki/ABC_Software_Metric)

Check failure on line 13 in app/controllers/callbacks_controller.rb

View workflow job for this annotation

GitHub Actions / Lint

Metrics/CyclomaticComplexity: Cyclomatic complexity for identity is too high. [10/7]

Check failure on line 13 in app/controllers/callbacks_controller.rb

View workflow job for this annotation

GitHub Actions / Lint

Metrics/MethodLength: Method has too many lines. [24/10] (https://rubystyle.guide#short-methods)
user = User.from_omniauth_inspect(request.env['omniauth.auth'])

if user.persisted?
identity = Identity.find_by(user_id: user.id)
if user.deactivated
render(json: { state: "password_prompt", error_message: "Uw account is gedeactiveerd, dus inloggen is niet mogelijk." })
elsif identity && identity.otp_enabled
one_time_password = params[:verification_code]
if !one_time_password
# OTP code not present, so request it
render(json: { state: "otp_prompt" })
elsif identity.authenticate_otp(one_time_password)
# OTP code correct
sign_in(:user, user)
render(json: { state: "logged_in", redirect_url: user.roles.any? ? root_path : user_path(user.id) })
else
# OTP code incorrect
render(json: { state: "otp_prompt", error_message: "Inloggen mislukt. De authenticatiecode is incorrect." })
end
elsif identity
# no OTP enabled
sign_in(:user, user)
render(json: { state: "logged_in", redirect_url: user.roles.any? ? root_path : user_path(user.id) })
else
# identity does not exist, should not be possible
render(json: { state: "password_prompt", error_message: "Inloggen mislukt door een error. Herlaad de pagina en probeer het nog een keer. <br/><i>Werkt het na een paar keer proberen nog steeds niet? Neem dan contact op met de ICT-commissie.</i>" })
end
else
render(json: { state: "password_prompt", error_message: "Inloggen mislukt. De ingevulde gegevens zijn incorrect." })
end
end

def failure
error_message = "Inloggen mislukt."
if request.env['omniauth.error.strategy'].instance_of? OmniAuth::Strategies::Identity
if request.env['omniauth.error.type'].to_s == "invalid_credentials"
error_message << " De ingevulde gegevens zijn incorrect."
else
error_message << " #{request.env['omniauth.error.type'].to_s}"

Check warning on line 52 in app/controllers/callbacks_controller.rb

View workflow job for this annotation

GitHub Actions / Lint

[Correctable] Lint/RedundantStringCoercion: Redundant use of Object#to_s in interpolation. (https://rubystyle.guide#no-to-s)
end
render(json: { state: "password_prompt", error_message: error_message })
else
render(json: { state: "password_prompt", error_message: error_message })
end
end
end
208 changes: 208 additions & 0 deletions app/controllers/identities_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
class IdentitiesController < ApplicationController

def omniauth_redirect_login
redirect_to '/identities/login'
end

def omniauth_redirect_register
redirect_to :root
end

def create
begin
user_id = params.require(:user_id)

user = User.find_by(id: user_id)
if !user
raise "uw account bestaat niet"
elsif user.deactivated
raise "uw account is gedeactiveerd"
elsif user.identity
raise "uw account is al geactiveerd"
end

activation_token = params.require(:activation_token)
if user.activation_token != activation_token || user.activation_token_valid_till.try(:<, Time.zone.now)
new_activation_link_url = Identity.new_activation_link_url(user.id)
raise "de activatielink is verlopen of ongeldig. Een nieuwe activatielink kan worden aangevraagd via <a href='#{new_activation_link_url}'>#{new_activation_link_url}</a>"
end

identity = Identity.new(permitted_attributes.merge(user_id: user_id))
if !identity.save
raise normalize_error_messages(identity.errors.full_messages)
end

user.reload
user.activation_token = nil
user.activation_token_valid_till = nil
if params[:user] && params[:user][:email]
if user.email # should not happen as the form only shows the email field when the user has no email
identity.destroy
raise "u heeft al een e-mailadres, dus u moet dat veld leeg laten"
end
user.email = params[:user][:email]
end
if !user.save
identity.destroy
raise normalize_error_messages(user.errors.full_messages)
end

sign_in(:user, user)
redirect_to user.roles.any? ? root_path : user_path(user.id), flash: { success: 'Account geactiveerd!' }
rescue ActionController::ParameterMissing => e
param_name = I18n.t("activerecord.attributes.identity.#{e.param}")
redirect_back_or_to :root, flash: { error: "Account activeren mislukt: #{param_name.downcase} is niet aanwezig." }
rescue StandardError => e
redirect_back_or_to :root, flash: { error: "Account activeren mislukt: #{e.message}." }
end
end

def update_password
@identity = Identity.find(params[:id])
authorize @identity

begin
new_attributes = params.require(:identity).permit(%i[password password_confirmation])

if [email protected](params.require(:identity)[:old_password])
raise "het oude wachtwoord is fout of niet opgegeven"
elsif new_attributes[:password].blank? # identity.update(...) just does nothing instead of showing error, so we show error manually
raise "wachtwoord moet opgegeven zijn"
elsif [email protected](new_attributes)
raise normalize_error_messages(@identity.errors.full_messages)
end

redirect_to user_path(@identity.user_id), flash: { success: "Wachtwoord gewijzigd." }
rescue ActionController::ParameterMissing => e

Check warning on line 76 in app/controllers/identities_controller.rb

View workflow job for this annotation

GitHub Actions / Lint

Lint/UselessAssignment: Useless assignment to variable - e. (https://rubystyle.guide#underscore-unused-vars)
redirect_back_or_to :root, flash: { error: "Wachtwoord wijzigen mislukt: identity is niet aanwezig." }
rescue StandardError => e
redirect_back_or_to :root, flash: { error: "Wachtwoord wijzigen mislukt: #{e.message}." }
end
end

def enable_otp
@identity = Identity.find(params[:id])
authorize @identity

if @identity.authenticate_otp(params.require(:verification_code))
@identity.update(otp_enabled: true)
flash[:success] = 'Two-factor-authenticatie aangezet!'
else
flash[:error] = "Two-factor-authenticatie aanzetten mislukt: de verificatie token is ongeldig."
end

redirect_to user_path(@identity.user_id)
end

def disable_otp
@identity = Identity.find(params[:id])
authorize @identity

@identity.update(otp_enabled: false)

redirect_to user_path(@identity.user_id)
end

def activate_account
@user_id = params[:user_id]
@activation_token = params[:activation_token]
@user = User.find_by(id: @user_id)
@request_email = @user&.email == nil
@identity = Identity.new()
end

def new_activation_link
begin
user = User.find_by(id: params.require(:user_id))
if !user
raise "uw account bestaat niet"
elsif user.deactivated
raise "uw account is gedeactiveerd"
elsif user.identity
raise "uw account is al geactiveerd"
elsif !user.email
raise "uw account heeft geen emailadres"
end

user.update(activation_token: SecureRandom.urlsafe_base64, activation_token_valid_till: 1.day.from_now)
UserMailer.new_activation_link_email(user).deliver_later
@message = "Er is een nieuwe activatielink voor uw account verstuurd naar uw emailadres."
rescue ActionController::ParameterMissing => e
param_name = I18n.t("activerecord.attributes.identity.#{e.param}")
@message = "Uw kunt geen nieuwe activatielink aanvragen: #{param_name.downcase} is niet aanwezig."
rescue StandardError => e
@message = "Uw kunt geen nieuwe activatielink aanvragen: #{e.message}."
end
end

def forgot_password
begin
identity = Identity.find_by(username: params.require(:username))
if !identity
raise "uw gebruikersnaam bestaat niet"
end

if !identity.user.email
raise "uw account heeft geen emailadres"
end

identity.user.update(activation_token: SecureRandom.urlsafe_base64, activation_token_valid_till: 1.day.from_now)
UserMailer.forgot_password_email(identity.user).deliver_later
redirect_to :root, flash: { success: 'Een link om uw wachtwoord te resetten is verstuurd naar uw emailadres.' }
rescue ActionController::ParameterMissing => e
param_name = I18n.t("activerecord.attributes.identity.#{e.param}")
redirect_back_or_to forgot_password_view_identities_path, flash: { error: "Account activeren mislukt: #{param_name.downcase} is niet aanwezig." }
rescue StandardError => e
redirect_back_or_to forgot_password_view_identities_path, flash: { error: "Account activeren mislukt: #{e.message}." }
end
end

def reset_password_view
@activation_token = params[:activation_token]
end

def reset_password
begin
identity = Identity.find_by(id: params.require(:id))
if !identity
raise "uw account bestaat niet"
end

user = identity.user
activation_token = params.require(:activation_token)
if user.activation_token != activation_token || user.activation_token_valid_till.try(:<, Time.zone.now)
forgot_password_url = Identity.forgot_password_url
raise "de resetlink is verlopen of ongeldig. Een nieuwe resetlink kan worden aangevraagd via <a href='#{forgot_password_url}'>#{forgot_password_url}</a>"
end

if params.require(:identity)[:password].blank? # identity.update(...) just does nothing instead of showing error, so we show error manually
raise "wachtwoord moet opgegeven zijn"
elsif !identity.update(params.require(:identity).permit(%i[password password_confirmation]))
raise normalize_error_messages(identity.errors.full_messages)
end

user.update(
activation_token: nil,
activation_token_valid_till: nil
)
redirect_to login_identities_path, flash: { success: 'Wachtwoord ingesteld!' }
rescue ActionController::ParameterMissing => e
param_name = I18n.t("activerecord.attributes.identity.#{e.param}")
redirect_back_or_to :root, flash: { error: "Wachtwoord resetten mislukt: #{param_name.downcase} is niet aanwezig." }
rescue StandardError => e
redirect_back_or_to :root, flash: { error: "Wachtwoord resetten mislukt: #{e.message}." }
end
end

def login
if current_user
redirect_to current_user.roles.any? ? root_path : user_path(current_user.id)
end
end

private

def permitted_attributes
params.require(:identity).permit(%i[username password password_confirmation])
end
end
Loading
Loading