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 6 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 @@ -64,3 +64,7 @@ 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
24 changes: 24 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ GEM
mime-types (~> 3.0)
mimemagic (~> 0.3.2)
concurrent-ruby (1.2.2)
connection_pool (2.4.1)
crass (1.0.6)
devise (4.8.1)
bcrypt (~> 3.0)
Expand All @@ -101,6 +102,9 @@ 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)
digest (3.1.0)
doorkeeper (5.6.6)
railties (>= 5)
Expand Down Expand Up @@ -212,6 +216,19 @@ GEM
zeitwerk (~> 2.5)
rainbow (3.1.1)
rake (13.0.6)
redis (5.0.8)
redis-client (>= 0.17.0)
redis-actionpack (5.3.0)
actionpack (>= 5, < 8)
redis-rack (>= 2.1.0, < 3)
redis-store (>= 1.1.0, < 2)
redis-client (0.18.0)
connection_pool
redis-rack (2.1.4)
rack (>= 2.0.8, < 3)
redis-store (>= 1.2, < 2)
redis-store (1.10.0)
redis (>= 4, < 6)
regexp_parser (2.5.0)
responders (3.0.1)
actionpack (>= 5.0)
Expand All @@ -234,6 +251,9 @@ GEM
rack (>= 1.1)
rubocop (>= 1.7.0, < 2.0)
ruby-progressbar (1.11.0)
ruby-saml (1.16.0)
nokogiri (>= 1.13.10)
rexml
ruby-vips (2.1.4)
ffi (~> 1.12)
ssrf_filter (1.0.8)
Expand Down Expand Up @@ -263,6 +283,7 @@ DEPENDENCIES
devise-i18n
devise-security
devise_ldap_authenticatable
devise_saml_authenticatable (~> 1.9)
doorkeeper
doorkeeper-i18n
dotenv-rails
Expand All @@ -275,7 +296,10 @@ DEPENDENCIES
rack-cors
rails (~> 7.0.3)
rails-i18n
redis (~> 5.0)
redis-actionpack (~> 5.3)
rubocop-rails
ruby-saml (~> 1.15)
tzinfo-data

RUBY VERSION
Expand Down
10 changes: 7 additions & 3 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@ class ApplicationController < ActionController::API
render text: exception, status: :internal_server_error
end

include Pundit if ENV['ENABLE_AUTHENTICATION'].present?
include Pundit::Authorization if ENV['ENABLE_AUTHENTICATION'].present?
if ENV['ENABLE_AUTHENTICATION'].present?
before_action :doorkeeper_authorize!,
except: %i[info check_uuid password_forgotten change_password]
end

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
101 changes: 101 additions & 0 deletions app/controllers/saml_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
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)
unless user
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.save
user.unlock_access!
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
9 changes: 5 additions & 4 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,11 +18,10 @@
# 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.
config.load_defaults 5.0
config.load_defaults 7.0
config.autoload_paths << "#{Rails.root}/lib"
# Configuration for the application, engines, and railties goes here.
#
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
19 changes: 18 additions & 1 deletion config/initializers/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
# session. If you need permissions, you should implement that in a before filter.
# You can also supply a hash where the value is a boolean determining whether
# or not authentication should be aborted when the value is not present.
# config.authentication_keys = [:email]
config.authentication_keys = [:email]

# Configure parameters from the request object used for authentication. Each entry
# given should be a request method and it will automatically be passed to the
Expand Down Expand Up @@ -320,4 +320,21 @@
# When set to false, does not sign a user in automatically after their password is
# changed. Defaults to true, so a user is signed in automatically after changing a password.
# config.sign_in_after_change_password = true

# # config.saml_default_user_key = :email # or whatever attribute you want to use as the user identifier
# config.saml_create_user = true # Automatically create users
# config.saml_update_user = true # Update user attributes after login
# config.saml_configure do |saml|
# url_base = "#{ENV['SAML_URL_BASE']}"
# saml.issuer = "#{url_base}/saml/metadata"
# saml.assertion_consumer_service_url = "#{url_base}/saml/acs"
# saml.assertion_consumer_logout_service_url = "#{url_base}/saml/logout"

# # IdP section
# saml.idp_entity_id = ENV['IDP_ENTITY_ID']
# saml.idp_sso_target_url = ENV['IDP_SSO_TARGET_URL']
# saml.idp_slo_target_url = ENV['IDP_SLO_TARGET_URL']
# saml.idp_cert = ENV['IDP_CERT']
# saml.name_identifier_format = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
# end
end
38 changes: 0 additions & 38 deletions config/initializers/new_framework_defaults_5_2.rb

This file was deleted.

Loading
Loading