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

initial saml implementation #612

Draft
wants to merge 26 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
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 .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ jobs:
HYRAX_VALKYRIE: true
RACK_ENV: test
RAILS_ENV: test
DATABASE_AUTH: true
SOLR_TEST_URL: http://127.0.0.1:8983/solr/dlp-selfdeposit-test
SPEC_OPTS: >-
--profile 10 --format RspecJunitFormatter --out /tmp/test-results/rspec.xml
Expand Down
3 changes: 2 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ gem 'coffee-rails', '~> 4.2'
gem 'dalli'
gem 'devise'
gem 'devise-guests', '~> 0.8'
gem "devise_saml_authenticatable", git: "https://github.com/apokalipto/devise_saml_authenticatable"
gem 'dotenv-rails'
gem 'hydra-role-management'
gem 'hyrax', git: 'https://github.com/samvera/hyrax', ref: '9c58751'
gem 'jbuilder', '~> 2.5'
gem 'jquery-rails'
gem 'mutex_m', '~> 0.2.0'
gem 'omniauth-rails_csrf_protection'
gem 'omniauth-saml'
gem 'pg', '~> 1.3'
gem 'puma'
gem 'rails', '~> 6.1'
Expand Down
21 changes: 12 additions & 9 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
GIT
remote: https://github.com/apokalipto/devise_saml_authenticatable
revision: f8a85c3f3ed069fd40701424e2eef075219ef17f
specs:
devise_saml_authenticatable (1.9.1)
devise (> 2.0.0)
ruby-saml (~> 1.7)

GIT
remote: https://github.com/brentd/xray-rails
revision: f121814718c9907b20058dc9357b80a53afab821
Expand Down Expand Up @@ -792,6 +784,16 @@ GEM
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 4)
omniauth (2.1.2)
hashie (>= 3.4.6)
rack (>= 2.2.3)
rack-protection
omniauth-rails_csrf_protection (1.0.2)
actionpack (>= 4.2)
omniauth (~> 2.0)
omniauth-saml (2.2.1)
omniauth (~> 2.1)
ruby-saml (~> 1.17)
openseadragon (0.7.0)
rails (> 6.1.0)
orm_adapter (0.5.0)
Expand Down Expand Up @@ -1264,7 +1266,6 @@ DEPENDENCIES
debug (>= 1.0.0)
devise
devise-guests (~> 0.8)
devise_saml_authenticatable!
dotenv-rails
ed25519 (>= 1.2, < 2.0)
factory_bot_rails
Expand All @@ -1274,6 +1275,8 @@ DEPENDENCIES
jquery-rails
listen (>= 3.0.5, < 3.2)
mutex_m (~> 0.2.0)
omniauth-rails_csrf_protection
omniauth-saml
pg (~> 1.3)
pry-doc
pry-rails
Expand Down
10 changes: 10 additions & 0 deletions app/controllers/omniauth_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true
class OmniauthController < Devise::SessionsController
def new
if Rails.env.production?
redirect_to user_saml_omniauth_authorize_path
else
super
end
end
end
79 changes: 0 additions & 79 deletions app/controllers/saml_sessions_controller.rb

This file was deleted.

20 changes: 20 additions & 0 deletions app/controllers/users/omniauth_callbacks_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def saml
@user = User.from_omniauth(request.env["omniauth.auth"])

if @user.persisted?
sign_in @user
redirect_to request.env["omniauth.origin"] || hyrax.dashboard_path
set_flash_message(:notice, :success, kind: "SAML")
else
redirect_to root_path
set_flash_message(:notice, :failure, kind: "SAML", reason: "you aren't authorized to use this application.")
end
end

def failure
redirect_to root_path
end
end
9 changes: 9 additions & 0 deletions app/models/auth_config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true
class AuthConfig
# In production, we use Shibboleth for user authentication,
# but in development mode, you may want to use local database
# authentication instead.
def self.use_database_auth?
ENV['DATABASE_AUTH'] == 'true'
end
end
83 changes: 38 additions & 45 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,68 +4,61 @@ class User < ApplicationRecord
include Hydra::User
# Connects this user object to Role-management behaviors.
include Hydra::RoleManagement::UserRoles
# Connects this user object to Hyrax behaviors.

include Hyrax::User
include Hyrax::UserUsageStats

class NilSamlUserError < RuntimeError
attr_accessor :auth

def initialize(message = nil, auth = nil)
super(message)
self.auth = auth
end
end

# Connects this user object to Blacklights Bookmarks.
include Blacklight::User
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :saml_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
# temporary fix for SAML login while we have a hybrid login
validates :password, presence: true, if: :password_required?
devise_modules = [:registerable, :recoverable, :rememberable, :validatable, :omniauthable, omniauth_providers: [:saml]]
devise_modules.prepend(:database_authenticatable, :registerable) # if AuthConfig.use_database_auth?
devise(*devise_modules)

def password_required?
return false if saml_authenticatable?
super
def to_s
email
end

def saml_authenticatable?
uid.present?
end
# Method added by Blacklight; Blacklight uses #to_s on your
# user class to get a user-displayable login/identifier for
# the account.
def self.from_omniauth(auth)
begin
user = find_by!(provider: auth.provider, uid: auth.info.net_id)
rescue ActiveRecord::RecordNotFound
log_omniauth_error(auth)
return User.new
end

def to_s
email
assign_user_attributes(user, auth)
user.save
user
end

def self.from_saml(auth)
return unless Rails.env.production?
user = find_or_initialize_by(email: auth.info.mail)
set_user_attributes(user, auth)
def self.assign_user_attributes(user, auth)
user.assign_attributes(
display_name: auth.info.display_name,
ppid: auth.uid,
provider: auth.provider,
uid: auth.info.net_id
)

if user.save
user
else
log_saml_error(auth)
User.new
end
user.email = "#{auth.info.net_id}@emory.edu" unless auth.info.net_id == 'tezprox'
end

def self.log_saml_error(auth)
if auth.info.mail.blank?
Rails.logger.error "Nil user detected: SAML didn't pass an email for #{auth.inspect}"
def self.log_omniauth_error(auth)
if auth.info.net_id.blank?
Rails.logger.error "Nil user detected: SAML didn't pass a net_id for #{auth.inspect}"
else
Rails.logger.error "Failed to create/update user: #{auth.inspect}"
# Log unauthorized logins to error
Rails.logger.error "Unauthorized user attempted login: #{auth.inspect}"
end
end
end

private

def set_user_attributes(user, auth)
password = SecureRandom.hex(16)
user.assign_attributes(
display_name: auth.info.displayName,
department: auth.info.ou,
title: auth.info.title,
uid: auth.uid,
# set a random password temporarily to allow a hybrid login
password:,
password_confirmation: password
)
end
2 changes: 1 addition & 1 deletion app/views/_user_util_links.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<% end %>
</li>
<li class="nav-item">
<%= link_to "Shib", '/users/saml/sign_in', method: :get, class: 'nav-link' %>
<%= link_to "Shib", main_app.user_saml_omniauth_authorize_path, method: :post, class: 'nav-link' %>
</li>
<% end %>
</ul>
20 changes: 20 additions & 0 deletions config/environments/production.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,24 @@

# Do not dump schema after migrations.
config.active_record.dump_schema_after_migration = false

# OmniAuth configuration settings
config.idp_slo_target_url = ENV['IDP_SLO_TARGET_URL']
config.assertion_consumer_service_url = ENV['ASSERTION_CS_URL']
config.assertion_consumer_logout_service_url = ENV['ASSERTION_LOGOUT_URL']
config.issuer = ENV['ISSUER']
config.idp_sso_target_url = ENV['IDP_SSO_TARGET_URL']
config.idp_cert = ENV['IDP_CERT']
config.certificate = ENV['SP_CERT']
config.private_key = ENV['SP_KEY']
config.attribute_statements = {
net_id: ["urn:oid:0.9.2342.19200300.100.1.1"],
display_name: ["urn:oid:1.3.6.1.4.1.5923.1.1.1.2"],
last_name: ["urn:oid:2.5.4.4"]
}
config.uid_attribute = "urn:oid:2.5.4.5"
config.security = {
want_assertions_encrypted: true, # Makes a 2nd KeyDescriptor, this one says use="encryption"
want_assertions_signed: true # Goes on md SPSSODescriptor tag
}
end
32 changes: 7 additions & 25 deletions config/initializers/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,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 = [:uid]

# 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 @@ -272,6 +272,12 @@
# Add a new OmniAuth provider. Check the wiki for more information on setting
# up on your models and hooks.
# config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo'
config.omniauth :saml,
idp_cert: ENV['IDP_CERT'],
certificate: ENV['SP_CERT'],
private_key: ENV['SP_KEY'],
idp_sso_target_url: 'https://login.emory.edu/idp/profile/SAML2/Redirect/SSO',
idp_sso_service_url: ENV['IDP_SSO_TARGET_URL']

# ==> Warden configuration
# If you want to use other strategies, that are not supported by Devise, or
Expand Down Expand Up @@ -308,28 +314,4 @@
# 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_route_helper_prefix = 'saml'

# Configure with your SAML settings (see ruby-saml's README for more information: https://github.com/onelogin/ruby-saml).
config.saml_configure do |settings|
base_url = ApplicationUrl.base_url
settings.assertion_consumer_service_url = "https://#{base_url}/users/saml/auth/"
settings.sp_entity_id = "https://#{base_url}/users/saml/metadata"
settings.assertion_consumer_service_binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
settings.name_identifier_format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
settings.authn_context = ""
settings.idp_slo_service_url = "https://shib.open.library.emory.edu/Shibboleth.sso/SLO/POST"
settings.idp_sso_service_url = "https://shib.open.library.emory.edu/Shibboleth.sso/Login"
# used the use="signing" key below
settings.idp_cert_fingerprint = ENV['IDP_CERT_FINGERPRINT']
settings.idp_cert_fingerprint_algorithm = "http://www.w3.org/2000/09/xmldsig#sha1"
end

config.saml_create_user = true
config.saml_update_user = true
config.saml_default_user_key = :email
config.saml_session_index_key = :session_index
config.saml_use_subject = true
config.saml_resource_locator = ->(model, saml_response) { model.from_saml(saml_response) }
end
Loading