diff --git a/app/assets/stylesheets/casino.scss b/app/assets/stylesheets/casino.scss
index 490637f5..5248c18a 100644
--- a/app/assets/stylesheets/casino.scss
+++ b/app/assets/stylesheets/casino.scss
@@ -270,6 +270,18 @@ table {
}
}
+/// LOGIN EXTERNAL INLINE LIST ///
+#external_list
+{
+ margin: 0;
+ padding: 0;
+ margin-right: 10px;
+ list-style-type: none;
+ text-align: left;
+
+ li { display: inline-block; }
+}
+
/// SESSIONS ///
.sessions, .logout {
width: 800px;
diff --git a/app/authenticators/casino/static_external_authenticator.rb b/app/authenticators/casino/static_external_authenticator.rb
new file mode 100644
index 00000000..c410d4f3
--- /dev/null
+++ b/app/authenticators/casino/static_external_authenticator.rb
@@ -0,0 +1,28 @@
+require 'casino/external_authenticator'
+
+# The external static authenticator is just a simple example.
+# Never use this authenticator in a production environment!
+class CASino::StaticExternalAuthenticator < CASino::Authenticator
+
+ # @param [Hash] options
+ def initialize(options)
+ @users = options[:users] || {}
+ end
+
+ def validate(params, cookies)
+ token = :"#{cookies[:token]}"
+ if @users.include?(token)
+ {
+ username: @users[token][:username],
+ extra_attributes: @users[token].except(:token)
+ }
+ else
+ false
+ end
+ end
+
+ def view
+ return nil
+ end
+
+end
diff --git a/app/controllers/casino/sessions_controller.rb b/app/controllers/casino/sessions_controller.rb
index b5d774e0..da534fac 100644
--- a/app/controllers/casino/sessions_controller.rb
+++ b/app/controllers/casino/sessions_controller.rb
@@ -11,7 +11,7 @@ def new
end
def create
- processor(:LoginCredentialAcceptor).process(params, request.user_agent)
+ processor(:LoginCredentialAcceptor).process(params, cookies, request.user_agent)
end
def destroy
diff --git a/app/listeners/casino/login_credential_acceptor_listener.rb b/app/listeners/casino/login_credential_acceptor_listener.rb
index c5819a46..502266c1 100644
--- a/app/listeners/casino/login_credential_acceptor_listener.rb
+++ b/app/listeners/casino/login_credential_acceptor_listener.rb
@@ -15,14 +15,14 @@ def two_factor_authentication_pending(ticket_granting_ticket)
@controller.render 'validate_otp'
end
- def invalid_login_credentials(login_ticket)
+ def invalid_login_credentials(login_ticket, external_authenticators)
@controller.flash.now[:error] = I18n.t('login_credential_acceptor.invalid_login_credentials')
- rerender_login_page(login_ticket)
+ rerender_login_page(login_ticket, external_authenticators)
end
- def invalid_login_ticket(login_ticket)
+ def invalid_login_ticket(login_ticket, external_authenticators)
@controller.flash.now[:error] = I18n.t('login_credential_acceptor.invalid_login_ticket')
- rerender_login_page(login_ticket)
+ rerender_login_page(login_ticket, external_authenticators)
end
def service_not_allowed(service)
@@ -31,8 +31,9 @@ def service_not_allowed(service)
end
private
- def rerender_login_page(login_ticket)
+ def rerender_login_page(login_ticket, external_authenticators)
assign(:login_ticket, login_ticket)
+ assign(:external_authenticators, external_authenticators)
@controller.render 'new', status: 403
end
end
diff --git a/app/listeners/casino/login_credential_requestor_listener.rb b/app/listeners/casino/login_credential_requestor_listener.rb
index 019e27c0..4fa44156 100644
--- a/app/listeners/casino/login_credential_requestor_listener.rb
+++ b/app/listeners/casino/login_credential_requestor_listener.rb
@@ -1,8 +1,9 @@
require_relative 'listener'
class CASino::LoginCredentialRequestorListener < CASino::Listener
- def user_not_logged_in(login_ticket)
+ def user_not_logged_in(login_ticket, external_authenticators)
assign(:login_ticket, login_ticket)
+ assign(:external_authenticators, external_authenticators)
@controller.cookies.delete :tgt
end
diff --git a/app/processors/casino/login_credential_acceptor_processor.rb b/app/processors/casino/login_credential_acceptor_processor.rb
index 3cd5164a..2bcf8944 100644
--- a/app/processors/casino/login_credential_acceptor_processor.rb
+++ b/app/processors/casino/login_credential_acceptor_processor.rb
@@ -13,30 +13,43 @@ class CASino::LoginCredentialAcceptorProcessor < CASino::Processor
# The second argument (String) is the ticket-granting ticket. It should be stored in a cookie named "tgt".
# The third argument (Time, optional, default = nil) is for "Remember Me" functionality.
# This is the cookies expiration date. If it is `nil`, the cookie should be a session cookie.
- # * `#invalid_login_ticket` and `#invalid_login_credentials`: The first argument is a LoginTicket.
+ # * `#invalid_login_ticket` and `#invalid_login_credentials`: The first argument is a LoginTicket and the second argument is the collection of external authenticators
# See {CASino::LoginCredentialRequestorProcessor} for details.
# * `#service_not_allowed`: The user tried to access a service that this CAS server is not allowed to serve.
# * `#two_factor_authentication_pending`: The user should be asked to enter his OTP. The first argument (String) is the ticket-granting ticket. The ticket-granting ticket is not active yet. Use SecondFactorAuthenticatonAcceptor to activate it.
#
# @param [Hash] params parameters supplied by user
+ # @param [Hash] cookies cookies supplied by user
# @param [String] user_agent user-agent delivered by the client
- def process(params = nil, user_agent = nil)
+ def process(params = nil, cookies = nil, user_agent = nil)
@params = params || {}
+ @cookies = cookies || {}
@user_agent = user_agent
if login_ticket_valid?(@params[:lt])
authenticate_user
else
- @listener.invalid_login_ticket(acquire_login_ticket)
+ external_authenticators = authenticators(:external_authenticators)
+ @listener.invalid_login_ticket(acquire_login_ticket, external_authenticators)
+ end
+ end
+
+ protected
+ def validate_credentials
+ if @params[:external]
+ validate_external_credentials(@params, @cookies)
+ else
+ validate_login_credentials(@params[:username], @params[:password])
end
end
private
def authenticate_user
- authentication_result = validate_login_credentials(@params[:username], @params[:password])
+ authentication_result = validate_credentials
if !authentication_result.nil?
user_logged_in(authentication_result)
else
- @listener.invalid_login_credentials(acquire_login_ticket)
+ external_authenticators = authenticators(:external_authenticators)
+ @listener.invalid_login_credentials(acquire_login_ticket, external_authenticators)
end
end
diff --git a/app/processors/casino/login_credential_requestor_processor.rb b/app/processors/casino/login_credential_requestor_processor.rb
index cec31845..5dedce94 100644
--- a/app/processors/casino/login_credential_requestor_processor.rb
+++ b/app/processors/casino/login_credential_requestor_processor.rb
@@ -4,6 +4,7 @@ class CASino::LoginCredentialRequestorProcessor < CASino::Processor
include CASino::ProcessorConcern::LoginTickets
include CASino::ProcessorConcern::ServiceTickets
include CASino::ProcessorConcern::TicketGrantingTickets
+ include CASino::ProcessorConcern::Authentication
# Use this method to process the request.
#
@@ -51,7 +52,8 @@ def handle_not_logged_in
@listener.user_logged_in(@service_url)
else
login_ticket = acquire_login_ticket
- @listener.user_not_logged_in(login_ticket)
+ external_authenticators = authenticators(:external_authenticators)
+ @listener.user_not_logged_in(login_ticket, external_authenticators)
end
end
diff --git a/app/processors/casino/processor_concern/authentication.rb b/app/processors/casino/processor_concern/authentication.rb
index 615b80a6..8d4b5679 100644
--- a/app/processors/casino/processor_concern/authentication.rb
+++ b/app/processors/casino/processor_concern/authentication.rb
@@ -5,10 +5,24 @@ module ProcessorConcern
module Authentication
def validate_login_credentials(username, password)
+ validate :authenticators do |authenticator_name, authenticator|
+ authenticator.validate(username, password)
+ end
+ end
+
+ def validate_external_credentials(params, cookies)
+ validate :external_authenticators do |authenticator_name, authenticator|
+ if authenticator_name == params[:external]
+ authenticator.validate(params, cookies)
+ end
+ end
+ end
+
+ def validate(type, &validator)
authentication_result = nil
- authenticators.each do |authenticator_name, authenticator|
+ authenticators(type).each do |authenticator_name, authenticator|
begin
- data = authenticator.validate(username, password)
+ data = validator.call(authenticator_name, authenticator)
rescue CASino::Authenticator::AuthenticatorError => e
Rails.logger.error "Authenticator '#{authenticator_name}' (#{authenticator.class}) raised an error: #{e}"
end
@@ -21,9 +35,11 @@ def validate_login_credentials(username, password)
authentication_result
end
- def authenticators
- @authenticators ||= begin
- CASino.config[:authenticators].each do |name, auth|
+ def authenticators(type)
+ @authenticators ||= {}
+ return @authenticators[type] if @authenticators.has_key?(type)
+ @authenticators[type] = begin
+ CASino.config[type].each do |name, auth|
next unless auth.is_a?(Hash)
authenticator = if auth[:class]
@@ -32,7 +48,7 @@ def authenticators
load_authenticator(auth[:authenticator])
end
- CASino.config[:authenticators][name] = authenticator.new(auth[:options])
+ CASino.config[type][name] = authenticator.new(auth[:options])
end
end
end
diff --git a/app/views/casino/sessions/_external.html.erb b/app/views/casino/sessions/_external.html.erb
new file mode 100644
index 00000000..b42f8be0
--- /dev/null
+++ b/app/views/casino/sessions/_external.html.erb
@@ -0,0 +1,15 @@
+<% if @external_authenticators.any? %>
+
+ <% @external_authenticators.each do |authenticator_name, authenticator| %>
+ <% unless authenticator.view.nil? %>
+ -
+ <%= form_tag(login_path, method: :post) do %>
+ <%= hidden_field_tag :lt, @login_ticket.ticket %>
+ <%= hidden_field_tag :external, authenticator_name %>
+ <%= render authenticator.view, :authenticator => authenticator %>
+ <% end %>
+
+ <% end %>
+ <% end %>
+
+<% end%>
diff --git a/app/views/casino/sessions/new.html.erb b/app/views/casino/sessions/new.html.erb
index 610d99ba..81505fcc 100644
--- a/app/views/casino/sessions/new.html.erb
+++ b/app/views/casino/sessions/new.html.erb
@@ -24,6 +24,7 @@
<% end %>
<%= button_tag t('login.label_button'), :class => 'button' %>
<% end %>
+ <%= render 'external' %>
<%= render 'footer' %>
diff --git a/lib/casino.rb b/lib/casino.rb
index f83857c4..3a732ab7 100644
--- a/lib/casino.rb
+++ b/lib/casino.rb
@@ -6,6 +6,7 @@ module CASino
defaults = {
authenticators: HashWithIndifferentAccess.new,
+ external_authenticators: HashWithIndifferentAccess.new,
logger: Rails.logger,
frontend: HashWithIndifferentAccess.new(
sso_name: 'CASino',
@@ -47,4 +48,4 @@ module CASino
}
self.config.merge! defaults.deep_dup
-end
\ No newline at end of file
+end
diff --git a/lib/casino/external_authenticator.rb b/lib/casino/external_authenticator.rb
new file mode 100644
index 00000000..be33643d
--- /dev/null
+++ b/lib/casino/external_authenticator.rb
@@ -0,0 +1,14 @@
+module CASino
+ class ExternalAuthenticator
+ class ExternalAuthenticatorError < StandardError; end
+
+ def validate(params, cookies)
+ raise NotImplementedError, "This method must be implemented by a class extending #{self.class}"
+ end
+
+ def view
+ raise NotImplementedError, "This method must be implemented by a class extending #{self.class}"
+ end
+
+ end
+end
diff --git a/spec/authenticator/base_spec.rb b/spec/authenticator/base_spec.rb
index 0b3ade3a..3377a7c2 100644
--- a/spec/authenticator/base_spec.rb
+++ b/spec/authenticator/base_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
require 'casino/authenticator'
+require 'casino/external_authenticator'
describe CASino::Authenticator do
subject {
@@ -13,3 +14,21 @@
end
end
end
+
+describe CASino::ExternalAuthenticator do
+ subject {
+ CASino::ExternalAuthenticator.new
+ }
+
+ context '#validate' do
+ it 'raises an error' do
+ expect { subject.validate(nil, nil) }.to raise_error(NotImplementedError)
+ end
+ end
+
+ context '#view' do
+ it 'raises an error' do
+ expect { subject.view }.to raise_error(NotImplementedError)
+ end
+ end
+end
diff --git a/spec/controllers/listener/login_credential_acceptor_spec.rb b/spec/controllers/listener/login_credential_acceptor_spec.rb
index 29f7cb71..7f321e0d 100644
--- a/spec/controllers/listener/login_credential_acceptor_spec.rb
+++ b/spec/controllers/listener/login_credential_acceptor_spec.rb
@@ -47,6 +47,7 @@
context "##{method}" do
let(:login_ticket) { Object.new }
let(:flash) { ActionDispatch::Flash::FlashHash.new }
+ let(:external_authenticators) { { :static => {} } }
before(:each) do
controller.stub(:render)
@@ -55,16 +56,21 @@
it 'tells the controller to render the new template' do
controller.should_receive(:render).with('new', status: 403)
- listener.send(method, login_ticket)
+ listener.send(method, login_ticket, external_authenticators)
end
it 'assigns a new login ticket' do
- listener.send(method, login_ticket)
+ listener.send(method, login_ticket, external_authenticators)
controller.instance_variable_get(:@login_ticket).should == login_ticket
end
+ it 'receives external authenticators' do
+ listener.send(method, login_ticket, external_authenticators)
+ controller.instance_variable_get(:@external_authenticators).should == external_authenticators
+ end
+
it 'should add an error message' do
- listener.send(method, login_ticket)
+ listener.send(method, login_ticket, external_authenticators)
flash[:error].should == I18n.t("login_credential_acceptor.#{method}")
end
end
diff --git a/spec/controllers/listener/login_credential_requestor_spec.rb b/spec/controllers/listener/login_credential_requestor_spec.rb
index 3fe67518..ff5297e2 100644
--- a/spec/controllers/listener/login_credential_requestor_spec.rb
+++ b/spec/controllers/listener/login_credential_requestor_spec.rb
@@ -4,17 +4,23 @@
include CASino::Engine.routes.url_helpers
let(:controller) { Struct.new(:cookies).new(cookies: {}) }
let(:listener) { described_class.new(controller) }
+ let(:external_authenticators) { { :static => {} } }
describe '#user_not_logged_in' do
let(:login_ticket) { Object.new }
it 'assigns the login ticket' do
- listener.user_not_logged_in(login_ticket)
+ listener.user_not_logged_in(login_ticket, external_authenticators)
controller.instance_variable_get(:@login_ticket).should == login_ticket
end
+ it 'receives external authenticators' do
+ listener.user_not_logged_in(login_ticket, external_authenticators)
+ controller.instance_variable_get(:@external_authenticators).should == external_authenticators
+ end
+
it 'deletes an existing ticket-granting ticket cookie' do
controller.cookies = { tgt: 'TGT-12345' }
- listener.user_not_logged_in(login_ticket)
+ listener.user_not_logged_in(login_ticket, external_authenticators)
controller.cookies[:tgt].should be_nil
end
end
diff --git a/spec/dummy/config/cas.yml b/spec/dummy/config/cas.yml
index c0c27f23..46da5e11 100644
--- a/spec/dummy/config/cas.yml
+++ b/spec/dummy/config/cas.yml
@@ -18,7 +18,14 @@ defaults: &defaults
testuser:
password: "foobar123"
name: "Test User"
-
+ external_authenticators:
+ external_static:
+ class: "CASino::StaticExternalAuthenticator"
+ options:
+ users:
+ foobar123:
+ username: "foobar"
+ name: "Test User"
development:
<<: *defaults
diff --git a/spec/processor/login_credential_acceptor_spec.rb b/spec/processor/login_credential_acceptor_spec.rb
index 56c845c5..8c2b683e 100644
--- a/spec/processor/login_credential_acceptor_spec.rb
+++ b/spec/processor/login_credential_acceptor_spec.rb
@@ -7,7 +7,7 @@
context 'without a valid login ticket' do
it 'calls the #invalid_login_ticket method on the listener' do
- listener.should_receive(:invalid_login_ticket).with(kind_of(CASino::LoginTicket))
+ listener.should_receive(:invalid_login_ticket).with(kind_of(CASino::LoginTicket), kind_of(Hash))
processor.process
end
end
@@ -16,17 +16,41 @@
let(:expired_login_ticket) { FactoryGirl.create :login_ticket, :expired }
it 'calls the #invalid_login_ticket method on the listener' do
- listener.should_receive(:invalid_login_ticket).with(kind_of(CASino::LoginTicket))
+ listener.should_receive(:invalid_login_ticket).with(kind_of(CASino::LoginTicket), kind_of(Hash))
processor.process(lt: expired_login_ticket.ticket)
end
end
+ context 'with an external param' do
+ let(:login_ticket) { FactoryGirl.create :login_ticket }
+ let(:params) { { lt: login_ticket.ticket, external: 'static' } }
+ let(:cookies) { { token: 'foo123' } }
+
+ it 'calls the #validate_external_credentials method on the processor' do
+ listener.should_receive(:invalid_login_credentials).with(kind_of(CASino::LoginTicket), kind_of(Hash))
+ processor.should_receive(:validate_external_credentials).with(kind_of(Hash), kind_of(Hash))
+ processor.process(params, cookies)
+ end
+ end
+
+ context 'without an external param' do
+ let(:login_ticket) { FactoryGirl.create :login_ticket }
+ let(:params) { { lt: login_ticket.ticket, username: 'testuser', password: 'foo123' } }
+ let(:cookies) { {} }
+
+ it 'calls the #validate_login_credentials method on the processor' do
+ listener.should_receive(:invalid_login_credentials).with(kind_of(CASino::LoginTicket), kind_of(Hash))
+ processor.should_receive(:validate_login_credentials).with(kind_of(String), kind_of(String))
+ processor.process(params, cookies)
+ end
+ end
+
context 'with a valid login ticket' do
let(:login_ticket) { FactoryGirl.create :login_ticket }
context 'with invalid credentials' do
it 'calls the #invalid_login_credentials method on the listener' do
- listener.should_receive(:invalid_login_credentials).with(kind_of(CASino::LoginTicket))
+ listener.should_receive(:invalid_login_credentials).with(kind_of(CASino::LoginTicket), kind_of(Hash))
processor.process(lt: login_ticket.ticket)
end
end
@@ -86,7 +110,7 @@
end
it 'calls the #invalid_login_credentials method on the listener' do
- listener.should_receive(:invalid_login_credentials).with(kind_of(CASino::LoginTicket))
+ listener.should_receive(:invalid_login_credentials).with(kind_of(CASino::LoginTicket), kind_of(Hash))
processor.process(login_data)
end
end
diff --git a/spec/processor/login_credential_requestor_spec.rb b/spec/processor/login_credential_requestor_spec.rb
index 379d6afe..3c5ce20b 100644
--- a/spec/processor/login_credential_requestor_spec.rb
+++ b/spec/processor/login_credential_requestor_spec.rb
@@ -20,7 +20,7 @@
context 'when logged out' do
it 'calls the #user_not_logged_in method on the listener' do
- listener.should_receive(:user_not_logged_in).with(kind_of(CASino::LoginTicket))
+ listener.should_receive(:user_not_logged_in).with(kind_of(CASino::LoginTicket), kind_of(Hash))
processor.process
end
@@ -39,7 +39,7 @@
let(:params) { { gateway: 'true' } }
it 'calls the #user_not_logged_in method on the listener' do
- listener.should_receive(:user_not_logged_in).with(kind_of(CASino::LoginTicket))
+ listener.should_receive(:user_not_logged_in).with(kind_of(CASino::LoginTicket), kind_of(Hash))
processor.process
end
end
@@ -59,7 +59,7 @@
let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket, :awaiting_two_factor_authentication }
it 'calls the #user_not_logged_in method on the listener' do
- listener.should_receive(:user_not_logged_in).with(kind_of(CASino::LoginTicket))
+ listener.should_receive(:user_not_logged_in).with(kind_of(CASino::LoginTicket), kind_of(Hash))
processor.process(nil, cookies, user_agent)
end
end
@@ -71,7 +71,7 @@
end
it 'calls the #user_not_logged_in method on the listener' do
- listener.should_receive(:user_not_logged_in).with(kind_of(CASino::LoginTicket))
+ listener.should_receive(:user_not_logged_in).with(kind_of(CASino::LoginTicket), kind_of(Hash))
processor.process(nil, cookies, user_agent)
end
end
@@ -93,7 +93,7 @@
context 'with renew parameter' do
it 'calls the #user_not_logged_in method on the listener' do
- listener.should_receive(:user_not_logged_in).with(kind_of(CASino::LoginTicket))
+ listener.should_receive(:user_not_logged_in).with(kind_of(CASino::LoginTicket), kind_of(Hash))
processor.process(params.merge({ renew: 'true' }), cookies)
end
end
@@ -135,7 +135,7 @@
let(:user_agent) { 'FooBar 1.0' }
it 'calls the #user_not_logged_in method on the listener' do
- listener.should_receive(:user_not_logged_in).with(kind_of(CASino::LoginTicket))
+ listener.should_receive(:user_not_logged_in).with(kind_of(CASino::LoginTicket), kind_of(Hash))
processor.process(nil, cookies, user_agent)
end
end