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? %> + +<% 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