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

Implementing SSO #230

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
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
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
gem "ruby-saml", "~> 1.15"
gem "devise_saml_authenticatable", "~> 1.9"
gem "redis", "~> 5.0" # Redis client for Ruby
gem "redis-actionpack", "~> 5.3" # Redis session store for ActionPack
gem "rubocop-rails-omakase", require: false, group: [ :development ]

gem 'simplecov', require: false, group: :test
54 changes: 40 additions & 14 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ GEM
public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2)
bcrypt (3.1.20)
bootsnap (1.17.1)
bootsnap (1.18.3)
msgpack (~> 1.2)
brakeman (6.1.1)
brakeman (6.1.2)
racc
builder (3.2.4)
bundler-audit (0.9.1)
Expand All @@ -91,6 +91,7 @@ GEM
marcel (~> 1.0.0)
mime-types (~> 3.0)
concurrent-ruby (1.2.3)
connection_pool (2.4.1)
crass (1.0.6)
date (3.3.4)
devise (4.9.3)
Expand All @@ -106,17 +107,20 @@ GEM
devise_ldap_authenticatable (0.8.7)
devise (>= 3.4.1)
net-ldap (>= 0.16.0)
devise_saml_authenticatable (1.9.1)
devise (> 2.0.0)
ruby-saml (~> 1.7)
docile (1.4.0)
doorkeeper (5.6.8)
doorkeeper (5.6.9)
railties (>= 5)
doorkeeper-i18n (5.2.7)
doorkeeper (>= 5.2)
dotenv (2.8.1)
dotenv-rails (2.8.1)
dotenv (= 2.8.1)
railties (>= 3.2)
dotenv (3.0.2)
dotenv-rails (3.0.2)
dotenv (= 3.0.2)
railties (>= 6.1)
erubi (1.12.0)
factory_bot (6.4.5)
factory_bot (6.4.6)
activesupport (>= 5.0.0)
factory_bot_rails (6.4.3)
factory_bot (~> 6.4)
Expand Down Expand Up @@ -145,12 +149,12 @@ GEM
method_source (1.0.0)
mime-types (3.5.2)
mime-types-data (~> 3.2015)
mime-types-data (3.2023.1205)
mime-types-data (3.2024.0206)
mini_magick (4.12.0)
mini_mime (1.1.5)
minitest (5.21.2)
minitest (5.22.2)
msgpack (1.7.2)
net-imap (0.4.9.1)
net-imap (0.4.10)
date
net-protocol
net-ldap (0.19.0)
Expand All @@ -170,7 +174,7 @@ GEM
parser (3.3.0.5)
ast (~> 2.4.1)
racc
pg (1.5.4)
pg (1.5.5)
public_suffix (5.0.4)
puma (5.6.8)
nio4r (~> 2.0)
Expand All @@ -180,6 +184,8 @@ GEM
rack (2.2.8)
rack-cors (2.0.1)
rack (>= 2.0.0)
rack-session (1.0.2)
rack (< 3)
rack-test (2.1.0)
rack (>= 1.3)
rails (7.0.8)
Expand Down Expand Up @@ -215,6 +221,19 @@ GEM
zeitwerk (~> 2.5)
rainbow (3.1.1)
rake (13.1.0)
redis (5.1.0)
redis-client (>= 0.17.0)
redis-actionpack (5.4.0)
actionpack (>= 5, < 8)
redis-rack (>= 2.1.0, < 4)
redis-store (>= 1.1.0, < 2)
redis-client (0.20.0)
connection_pool
redis-rack (3.0.0)
rack-session (>= 0.2.0)
redis-store (>= 1.2, < 2)
redis-store (1.10.0)
redis (>= 4, < 6)
regexp_parser (2.9.0)
responders (3.1.1)
actionpack (>= 5.2)
Expand Down Expand Up @@ -250,7 +269,10 @@ GEM
rubocop-performance
rubocop-rails
ruby-progressbar (1.13.0)
ruby-vips (2.2.0)
ruby-saml (1.16.0)
nokogiri (>= 1.13.10)
rexml
ruby-vips (2.2.1)
ffi (~> 1.12)
simplecov (0.22.0)
docile (~> 1.1)
Expand All @@ -269,7 +291,7 @@ GEM
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
zeitwerk (2.6.12)
zeitwerk (2.6.13)

PLATFORMS
arm64-darwin-23
Expand All @@ -287,6 +309,7 @@ DEPENDENCIES
devise-i18n
devise-security
devise_ldap_authenticatable
devise_saml_authenticatable (~> 1.9)
doorkeeper
doorkeeper-i18n
dotenv-rails
Expand All @@ -299,9 +322,12 @@ DEPENDENCIES
rack-cors
rails (~> 7.0.3)
rails-i18n
redis (~> 5.0)
redis-actionpack (~> 5.3)
rubocop
rubocop-rails
rubocop-rails-omakase
ruby-saml (~> 1.15)
simplecov
tzinfo-data

Expand Down
8 changes: 6 additions & 2 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ class ApplicationController < ActionController::API
except: %i[info check_uuid password_forgotten change_password]

def info
client_app = Doorkeeper::Application.find_by(uid: params["client_id"], secret: params["client_secret"])
render json: { valid: client_app.present?, auth: ENV['ENABLE_AUTHENTICATION'].present? }
client_app = Doorkeeper::Application.find_by(uid: params['client_id'], secret: params['client_secret'])
render json: {
valid: client_app.present?,
auth: ENV['ENABLE_AUTHENTICATION'].present?,
sso_enabled: ENV['ENABLE_SSO'].present? ? ActiveModel::Type::Boolean.new.cast(ENV['ENABLE_SSO']) : false
}
end

protected
Expand Down
3 changes: 2 additions & 1 deletion app/controllers/pias_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ def index
res = []
# check if user is technical else his pias
pias = if ENV['ENABLE_AUTHENTICATION'].blank? || current_user.is_technical_admin
Pia.all
Pia.eager_load(:user_pias)
.all
else
policy_scope(Pia)
end
Expand Down
103 changes: 103 additions & 0 deletions app/controllers/saml_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
class SamlController < Doorkeeper::TokensController
# skip_before_action :doorkeeper_authorize!

def metadata
meta = OneLogin::RubySaml::Metadata.new
render xml: meta.generate(settings, true)
end

def sso
request = OneLogin::RubySaml::Authrequest.new
redirect_to(request.create(settings), allow_other_host: true)
end

def consume
response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], settings:)

if response.is_valid?
email = response.name_id
session[:nameid] = response.name_id
user = User.find_by("LOWER(email) = ?", email.strip.downcase)
if user
user.unlock_access!
else
password = [*'0'..'9', *'a'..'z', *'A'..'Z', *'!'..'?'].sample(16).join
user = User.create!(email:, password:, password_confirmation: password)
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
Dismissed Show dismissed Hide dismissed
Dismissed Show dismissed Hide dismissed
user.is_user = true
user.unlock_access!
user.save
end
sign_in(:user, user)

doorkeeper_app = Doorkeeper::Application.first
access_token = Doorkeeper::AccessToken.find_or_create_for(
application: doorkeeper_app,
resource_owner: user.id,
scopes: Doorkeeper::OAuth::Scopes.from_array(%w[public])
)

# redirect_to frontrnd
redirect_to "#{ENV['SSO_FRONTEND_REDIRECTION']}/#/?sso_token=#{access_token.token}", allow_other_host: true
else
logger.info "Response Invalid. Errors: #{response.errors}"
@errors = response.errors
redirect_to root_path
end
end

def logout
logout_request = OneLogin::RubySaml::Logoutrequest.new
session[:transaction_id] = logout_request.uuid

logger.info "New SP SLO for User ID: '#{session[:nameid]}', Transaction ID: '#{session[:transaction_id]}'"

settings.name_identifier_value = session[:nameid] if settings.name_identifier_value.nil?

redirect_to(logout_request.create(settings), allow_other_host: true)
end

# Handle the SLO response from the IdP
# GET /saml/slo
def slo
logout_response = if session.has_key? :transaction_id
OneLogin::RubySaml::Logoutresponse.new(params[:SAMLResponse], settings,
matches_request_id: session[:transaction_id])
else
OneLogin::RubySaml::Logoutresponse.new(params[:SAMLResponse], settings)
end
logger.info "LogoutResponse is: #{logout_response}"

# Validate the SAML Logout Response
if !logout_response.validate
logger.error 'The SAML Logout Response is invalid'
else
# Actually log out this session
logger.info "SLO completed for '#{session[:nameid]}'"
session[:nameid] = nil
session[:transaction_id] = nil

redirect_to ENV['SSO_FRONTEND_REDIRECTION'], allow_other_host: true
end
end

private

def settings
settings = OneLogin::RubySaml::Settings.new
url_base = "#{request.protocol}#{request.host_with_port}"

# settings.soft = true
settings.issuer = "#{url_base}/saml/metadata"
settings.assertion_consumer_service_url = "#{url_base}/saml/acs"
settings.assertion_consumer_logout_service_url = "#{url_base}/saml/slo"

# IdP section
settings.idp_entity_id = ENV['IDP_ENTITY_ID']
settings.idp_sso_target_url = ENV['IDP_SSO_TARGET_URL']
settings.idp_slo_target_url = ENV['IDP_SLO_TARGET_URL']
settings.idp_cert = ENV['IDP_CERT']
settings.name_identifier_format = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'

settings
end
end
2 changes: 1 addition & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class User < ApplicationRecord
dependent: :destroy

def validate_login_uniqueness
errors.add(:login, :taken) if User.where(login: login).where.not(id: id).exists?
errors.add(:login, :taken) if User.where(login:).where.not(id:).exists?
end

def check_ldap_email
Expand Down
6 changes: 4 additions & 2 deletions app/policies/pia_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@ def initialize(user, scope)

def resolve
if user.present? && user.is_functional_admin?
scope.all
scope.eager_load(:user_pias)
.all
else
scope.joins(:user_pias).merge(UserPia.where(user_id: user.id))
scope.eager_load(:user_pias)
.merge(UserPia.where(user_id: user.id))
end
end
end
Expand Down
7 changes: 4 additions & 3 deletions config/application.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
require_relative "boot"
require_relative 'boot'

require 'rails'
# Pick the frameworks you want:
Expand All @@ -18,7 +18,6 @@
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)


module PiaBack
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
Expand All @@ -38,7 +37,7 @@ class Application < Rails::Application
config.api_only = true

# set the default locale to French
config.i18n.default_locale = ENV.fetch("DEFAULT_LOCALE", :en)
config.i18n.default_locale = ENV.fetch('DEFAULT_LOCALE', :en)
# if a locale isn't found fall back to this default locale
config.i18n.fallbacks = true
# set the possible locales to English and Brazilian-Portuguese
Expand All @@ -50,5 +49,7 @@ class Application < Rails::Application
config.action_view.sanitized_allowed_tags = tags_allowed
attributes_allowed = ENV['SANITIZED_ALLOWED_ATTRIBUTES'] ? ENV['SANITIZED_ALLOWED_ATTRIBUTES'].split(' ') : []
config.action_view.sanitized_allowed_attributes = attributes_allowed

config.secret_key_base = Rails.application.credentials.secret_key_base
end
end
8 changes: 4 additions & 4 deletions config/environments/development.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
require "active_support/core_ext/integer/time"
require 'active_support/core_ext/integer/time'

Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
Expand All @@ -19,10 +19,10 @@

# Enable/disable caching. By default caching is disabled.
# Run rails dev:cache to toggle caching.
if Rails.root.join("tmp/caching-dev.txt").exist?
if Rails.root.join('tmp/caching-dev.txt').exist?
config.cache_store = :memory_store
config.public_file_server.headers = {
"Cache-Control" => "public, max-age=#{2.days.to_i}"
'Cache-Control' => "public, max-age=#{2.days.to_i}"
}
else
config.action_controller.perform_caching = false
Expand Down Expand Up @@ -53,7 +53,6 @@
# Highlight code that triggered database queries in logs.
config.active_record.verbose_query_logs = true


# Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true

Expand All @@ -62,4 +61,5 @@

# Uncomment if you wish to allow Action Cable access from any origin.
# config.action_cable.disable_request_forgery_protection = true
config.hosts << ENV.fetch('RAILS_CONFIG_HOSTS', 'localhost:3000')
end
Loading
Loading