ShibRack

Gem Version

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.