Skip to content

Commit

Permalink
Initial support for SAML SSO #95
Browse files Browse the repository at this point in the history
  • Loading branch information
oneiros committed Feb 23, 2023
1 parent e0e4d63 commit dc45c50
Show file tree
Hide file tree
Showing 12 changed files with 164 additions and 18 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ gem 'hiera-eyaml'
gem 'net-ldap', require: "net/ldap"
gem 'breadcrumbs_on_rails'
gem 'cancancan'
gem 'ruby-saml'

# To use retry middleware with Faraday v2.0+
gem 'faraday-retry'
Expand Down
4 changes: 4 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,9 @@ GEM
rubocop-rake (0.6.0)
rubocop (~> 1.0)
ruby-progressbar (1.11.0)
ruby-saml (1.15.0)
nokogiri (>= 1.13.10)
rexml
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
sassc (2.4.0)
Expand Down Expand Up @@ -435,6 +438,7 @@ DEPENDENCIES
rubocop
rubocop-rails
rubocop-rake
ruby-saml
selenium-webdriver
simplecov
sprockets-rails
Expand Down
41 changes: 41 additions & 0 deletions app/controllers/saml_sessions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
class SamlSessionsController < ApplicationController
skip_before_action :authentication_required
skip_forgery_protection only: :create

def new
saml_request = OneLogin::RubySaml::Authrequest.new
redirect_to saml_request.create(saml_settings), allow_other_host: true
end

def create
saml_response = OneLogin::RubySaml::Response.new(params[:SAMLResponse])
saml_response.settings = saml_settings

if saml_response.is_valid?
user = find_or_create_user(saml_response)
session[:user_id] = user.id
if user.admin?
redirect_to users_path, notice: "Logged in!"
else
redirect_to root_url, notice: "Logged in!"
end
else
redirect_to new_session_path, alert: "Could not sign you in via SSO"
end
end

private

def find_or_create_user(saml_response)
User.find_or_create_by!(email: saml_response.nameid) do |user|
user.first_name = "SAML"
user.last_name = "User"
end
end

def saml_settings
settings = Saml.new.settings
settings.assertion_consumer_service_url = saml_session_url
settings
end
end
14 changes: 14 additions & 0 deletions app/models/saml.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class Saml
def self.configured?
Rails.configuration.hdm[:saml].present?
end

def initialize
@hdm_settings = Rails.configuration.hdm[:saml]
@hdm_settings[:name_identifier_format] ||= "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
end

def settings
OneLogin::RubySaml::Settings.new(@hdm_settings)
end
end
9 changes: 1 addition & 8 deletions app/views/ldap_sessions/new.html.erb
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
<h1>Login</h1>

<ul class="nav nav-tabs">
<li class="nav-item">
<%= link_to "Local login", login_path, class: "nav-link" %>
</li>
<li class="nav-item">
<%= link_to "LDAP login", new_ldap_session_path, class: "nav-link active" %>
</li>
</ul>
<%= render "shared/auth_navigation", active: :ldap %>
<%= form_tag ldap_session_path do |form| %>
<div class="form-group">
Expand Down
11 changes: 1 addition & 10 deletions app/views/sessions/new.html.erb
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
<h1>Login</h1>

<% if Ldap.configured? %>
<ul class="nav nav-tabs">
<li class="nav-item">
<%= link_to "Local login", login_path, class: "nav-link active" %>
</li>
<li class="nav-item">
<%= link_to "LDAP login", new_ldap_session_path, class: "nav-link" %>
</li>
</ul>
<% end %>
<%= render "shared/auth_navigation", active: :local %>
<%= form_tag sessions_path do |form| %>
<div class="form-group">
Expand Down
20 changes: 20 additions & 0 deletions app/views/shared/_auth_navigation.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<% if Ldap.configured? || Saml.configured? %>
<ul class="nav nav-tabs">
<li class="nav-item">
<%= link_to "Local login", login_path, class: "nav-link #{"active" if local_assigns[:active] == :local}" %>
</li>
<% if Ldap.configured? %>
<li class="nav-item">
<%= link_to "LDAP login", new_ldap_session_path, class: "nav-link #{"active" if local_assigns[:active] == :ldap}" %>
</li>
<% end %>
<% if Saml.configured? %>
<li class="nav-item">
<%= link_to new_saml_session_path, class: "nav-link" do %>
<%= icon("box-arrow-up-right") %>
SAML SSO login
<% end %>
</li>
<% end %>
</ul>
<% end %>
14 changes: 14 additions & 0 deletions config/hdm.yml.template
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,17 @@ production:
# base_dn: "ou=hdm,dc=nodomain"
# bind_dn: "cn=admin,dc=nodomain"
# bind_dn_password: "openldap"

# Example for SAML SSO authentication
# production:
# read_only: false
# allow_encryption: true
# puppet_db:
# server: "https://localhost:8081"
# config_dir: "/etc/puppetlabs/code"
# saml:
# sp_entity_id: "my-id"
# idp_sso_service_url: "https://my_idp/saml_endpoint"
# idp_cert_fingerprint: "aaa"
# idp_cert: "cert" # use either fingerprint _or_ cert but not both

1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
resources :users

resource :ldap_session, only: [:new, :create]
resource :saml_session, only: [:new, :create]

get 'page/index'

Expand Down
41 changes: 41 additions & 0 deletions test/controllers/saml_sessions_controller_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
require "test_helper"

class SamlSessionsControllerTest < ActionDispatch::IntegrationTest
setup do
Rails.configuration.hdm[:saml] = SAML_TEST_CONFIG.dup
end

teardown do
Rails.configuration.hdm.delete(:saml)
end

test "#new redirects to SSO" do
get new_saml_session_path

assert_redirected_to %r{\Ahttps://testsso}
end

test "#create with successful SSO redirects to root_path" do
stubbed_saml_response(valid: true) do
post saml_session_path
assert_redirected_to root_path
end
end

test "#create with failed SSO redirects to login page" do
stubbed_saml_response(valid: false) do
post saml_session_path
assert_redirected_to new_session_path
end
end

private

def stubbed_saml_response(valid: true, &block)
saml_response = Minitest::Mock.new
saml_response.expect(:settings=, true, [OneLogin::RubySaml::Settings])
saml_response.expect(:is_valid?, valid)
saml_response.expect(:nameid, "testuser@example.com")
OneLogin::RubySaml::Response.stub(:new, saml_response, &block)
end
end
20 changes: 20 additions & 0 deletions test/models/saml_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
require 'test_helper'

class SamlTest < ActiveSupport::TestCase
test "::configured? checks if configuration exists" do
Rails.configuration.hdm[:saml] = SAML_TEST_CONFIG.dup
assert Saml.configured?
Rails.configuration.hdm.delete(:saml)
assert_not Saml.configured?
end

test "#settings correctly configures ruby-saml" do
Rails.configuration.hdm[:saml] = SAML_TEST_CONFIG.dup
settings = Saml.new.settings
assert_equal "hdm-test", settings.sp_entity_id
assert_equal "https://testsso", settings.idp_sso_service_url
assert_equal "test", settings.idp_cert_fingerprint
assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", settings.name_identifier_format
Rails.configuration.hdm.delete(:saml)
end
end
6 changes: 6 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
end
server_thread.join(1)

SAML_TEST_CONFIG = {
sp_entity_id: "hdm-test",
idp_sso_service_url: "https://testsso",
idp_cert_fingerprint: "test"
}.freeze

class ActiveSupport::TestCase
# Run tests in parallel with specified workers
parallelize(workers: :number_of_processors) unless ENV["COVERAGE"]
Expand Down

0 comments on commit dc45c50

Please sign in to comment.