ShibRack
Shibboleth SP authentication plugin for Rack-based web applications. Contains Rails-specific extensions for consumption by Rails applications.
Installation
Add the shib-rack
dependency to your application's Gemfile
:
gem 'shib-rack'
Use Bundler to install the dependency:
bundle install
Then create a Receiver class which will receive the session attributes. In development mode, shib-rack
uses a development handler to provide example identities that you can use.
Optionally, you can also setup an error handler class.
You will need to customise this to suit your usage. Below is an example using only a limited number of attributes.
# frozen_string_literal: true
# Extends the Authentication Module from Shib-Rack
module Authentication
# Custom SubjectReceiver Class
class SubjectReceiver
include ShibRack::DefaultReceiver
include ShibRack::AttributeMapping
map_single_value shared_token: 'HTTP_AUEDUPERSONSHAREDTOKEN',
targeted_id: 'HTTP_TARGETED_ID',
principal_name: 'HTTP_PRINCIPALNAME'
def subject(_env, attrs)
Subject.transaction do
identifier = attrs.slice(:targeted_id)
subject = Subject.find_or_initialize_by(identifier)
subject.update!(attrs)
subject
end
end
def finish(_env)
redirect_to('/dashboard')
end
end
end
Multiple attributes
Some attributes are multi-valued, such as affiliation
and scoped_affiliation
.
You can use the map_multi_value
method for these attributes, but you will need to consider how to store them in your app. In the example below, a separate model has been used for affiliations
.
Multi-value attributes (as with other attributes) are appended to your HTTP headers after authentication occurs. Multi-value attributes are stored in your header as a single string with each value delimited by a ;
. This method splits the string into a ruby array based on this delimiting character.
# frozen_string_literal: true
# Extends the Authentication Module from Shib-Rack
module Authentication
# Custom SubjectReceiver Class
class SubjectReceiver
include ShibRack::DefaultReceiver
include ShibRack::AttributeMapping
map_single_value shared_token: 'HTTP_AUEDUPERSONSHAREDTOKEN',
targeted_id: 'HTTP_TARGETED_ID',
principal_name: 'HTTP_PRINCIPALNAME'
map_multi_value affiliation: 'HTTP_EDUPERSONAFFILIATION'
def subject(_env, attrs)
Subject.transaction do
identifier = attrs.slice(:targeted_id)
subject = Subject.find_or_initialize_by(identifier)
subject.update!(attrs.except(:affiliation))
update_affiliations(subject, attrs)
subject
end
end
def finish(_env)
redirect_to('/dashboard')
end
def update_affiliations(subject, attrs)
touched = []
attrs[:affiliation].each do |value|
touched << subject.affiliations.find_or_create_by!(value: value)
end
subject.affiliations.where.not(id: touched.map(&:id)).destroy_all
end
end
end
Integrating with a Rails application
Map the ShibRack::Engine
app to a path in your application. It is strongly recommended that you use the default path of /auth
. In config/routes.rb
Rails.application.routes.draw do
mount ShibRack::Engine => '/auth'
end
Configure the receiver, and optional error handler in config/application.rb
module MyApp
class Application < Rails::Application
# ...
config.autoload_paths << Rails.root.join('lib')
config.shib_rack.receiver = 'Authentication::SubjectReceiver'
config.shib_rack.error_handler = 'Authentication::ErrorHandler'
end
end
Ensure the gem runs in development mode in config/development.rb
config.shib_rack.development_mode = true
And likewise ensure the gem runs in test mode in config/test.rb
config.shib_rack.test_mode = true
Using with Capybara-style tests
Ensure that you have configured the gem to run in test mode in config/test.rb
(See above)
Once this has been setup, you can create a capybara-style test. See the example below which tests the login process using the test handler. This is an example only, and will vary based on your app implementation.
# frozen_string_literal: true
require 'rails_helper'
RSpec.feature 'Authentication Flow', type: :feature do
let(:idp_domain) { Faker::Internet.domain_name }
let(:idp) { "https://idp.#{idp_domain}/idp/shibboleth" }
let(:sp) { "https://sp.#{Faker::Internet.domain_name}/shibboleth" }
let(:name) { Faker::Name.name }
let(:attrs) do
{
'HTTP_AUEDUPERSONSHAREDTOKEN' => SecureRandom.urlsafe_base64(20),
'HTTP_TARGETED_ID' => "#{idp}!#{sp}!#{SecureRandom.uuid}",
'HTTP_PRINCIPALNAME' => "#{name}@#{idp_domain}",
'HTTP_EDUPERSONAFFILIATION' => 'staff;member'
}
end
before do
ShibRack::TestHandler.attributes = attrs
visit '/auth/login'
end
it 'User is redirected to dashboard with details displayed on initial login' do
expect(current_path).to eq('/dashboard')
expect(page).to have_content("You're logged in. Here are your details")
expect(page).to have_content(name)
expect(page).to have_content(idp_domain)
# Further checks for other attributes can be added here
end
end
Contributing
Refer to GitHub Flow for help contributing to this project.