RapidRack
AAF Rapid Connect authentication plugin for Rack-based web applications. Contains Rails-specific extensions for consumption by Rails applications.
Author: Shaun Mangelsdorf
Copyright 2014, Australian Access Federation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Installation
Add the rapid-rack
dependency to your application's Gemfile
:
gem 'rapid-rack'
Use Bundler to install the dependency:
bundle install
Create a Receiver class, which will receive the validated claim from Rapid Connect and establish a session for the authenticated subject.
module MyApplication
class MyReceiver
# Helper mixin which provides default behaviour for the Receiver class
include RapidRack::DefaultReceiver
# Default implementation of Redis-backed replay detection
include RapidRack::RedisRegistry
# Receives the contents of the 'https://aaf.edu.au/attributes' claim from
# Rapid Connect, and returns a set of attributes appropriate for passing in
# to the `subject` method.
def map_attributes(_env, attrs)
{
targeted_id: attrs['edupersontargetedid'],
name: attrs['displayname'],
email: attrs['mail']
}
end
# Receives a set of attributes returned by `map_attributes`, and is
# responsible for either creating a new user record, or updating an existing
# user record to ensure attributes are current.
#
# Must return the subject, and the subject must have an `id` method to work
# with the DefaultReceiver mixin.
def subject(_env, attrs)
identifier = attrs.slice(:targeted_id)
MyOwnUserClass.find_or_initialize_by(identifier).tap do |subject|
subject.update_attributes!(attrs)
end
end
end
end
Integrating with a Rack application
Map the RapidRack::Authenticator
app to a path in your application. The
strongly suggested default of /auth
will result in a callback URL ending in
/auth/jwt
, which is given to Rapid Connect during registration:
Rack::Builder.new do
use Rack::Lint
map '/auth' do
opts = { ... }
run RapidRack::Authenticator.new(opts)
end
run MyApplication
end
opts
must be the same arguments derived from the Rails configuration below,
that is:
url
— The URL provided during registration with Rapid Connectsecret
— Your extremely secure secretissuer
— Theiss
claim to expect. This is the identifier of the Rapid Connect server you're authenticating againstaudience
— Theaud
claim to expect. This is your own service's identifierreceiver
— String representing the fully qualified class name of your receiver classerror_handler
(optional) — String representing the fully qualified class name of your error handler class
In the opts
hash, all keys must be symbols.
Integrating with a Rails application
Add a config/rapidconnect.yml
file to your application, with the
deployment-specific options described above:
---
url: https://rapid.example.com/url/provided/by/registration
secret: very_secure_secret_you_generated_yourself
issuer: https://rapid.example.com
audience: https://yourapp.example.org
Configure the receiver, and optional error handler in config/application.rb
:
module MyApplication
class Application < Rails::Application
# ...
config.rapid_rack.receiver = 'MyApplication::MyReceiver'
config.rapid_rack.error_handler = 'MyApplication::MyErrorHandler'
end
end
Mount the RapidRack::Engine
engine in your Rails app. The strongly suggested
default of /auth
will result in a callback URL ending in /auth/jwt
, which is
given to Rapid Connect during registration. In config/routes.rb
:
Rails.application.routes.draw do
mount RapidRack::Engine => '/auth'
# ...
end
Redirect to /auth/login
to force authentication. By default, the subject id is
available as session[:subject_id]
. For example:
class WelcomeController < ApplicationController
before_action do
@subject = session[:subject_id] && MyOwnUserClass.find(session[:subject_id])
redirect_to('/auth/login') if @subject.nil?
end
def index
end
end
Using with Capybara-style tests
Configure rapid_rack
to run in test mode. In a Rails application this can be
set in config/environments/test.rb
:
Rails.application.configure do
# ...
config.rapid_rack.test_mode = true
end
Set the JWT in your test code. In this example factory_girl has a jwt
factory
registered which creates a valid JWT.
RSpec.feature 'First visit', type: :feature do
given(:user_attrs) { attributes_for(:user) }
background do
attrs = create(:aaf_attributes, displayname: user_attrs[:name],
mail: user_attrs[:email])
RapidRack::TestAuthenticator.jwt = create(:jwt, aaf_attributes: attrs)
end
# ...
end
Once this is in place, your example will be presented with a plain page with a
'Login' button when it is required to log in. The button will submit the form
to the /auth/jwt
endpoint, which works as normal and will invoke your
receiver.
RSpec.feature 'First visit', type: :feature do
# ... code from above ...
scenario 'initial login' do
visit '/'
'Sign in via AAF'
# At this point, your Capybara test is sitting in the TestAuthenticator page
# which has a 'Login' button and no other content.
expect(current_path).to eq('/auth/login')
'Login'
expect(current_path).to match(%r{/users/\d+})
expect(page).to have_content("Logged in as: #{user_attrs[:name]}")
end
end
Contributing
Refer to GitHub Flow for help contributing to this project.