diff --git a/app/controllers/casino/application_controller.rb b/app/controllers/casino/application_controller.rb index 8a207864..de94b0f7 100644 --- a/app/controllers/casino/application_controller.rb +++ b/app/controllers/casino/application_controller.rb @@ -2,22 +2,12 @@ require 'http_accept_language' class CASino::ApplicationController < ::ApplicationController - include ApplicationHelper + include CASino::ApplicationHelper layout 'application' before_filter :set_locale - def cookies - super - end - protected - def processor(processor_name, listener_name = nil) - listener_name ||= processor_name - listener = CASino.const_get(:"#{listener_name}Listener").new(self) - @processor = CASino.const_get(:"#{processor_name}Processor").new(listener) - end - def set_locale I18n.locale = extract_locale_from_accept_language_header || I18n.default_locale end diff --git a/app/controllers/casino/sessions_controller.rb b/app/controllers/casino/sessions_controller.rb index b5d774e0..a673dff7 100644 --- a/app/controllers/casino/sessions_controller.rb +++ b/app/controllers/casino/sessions_controller.rb @@ -1,13 +1,15 @@ class CASino::SessionsController < CASino::ApplicationController include CASino::SessionsHelper + skip_before_filter :set_current_user, only:[:create, :validate_otp] + def index - processor(:TwoFactorAuthenticatorOverview).process(cookies, request.user_agent) - processor(:SessionOverview).process(cookies, request.user_agent) + processor(:TwoFactorAuthenticatorOverview).process(current_user, request.user_agent) + processor(:SessionOverview).process(current_user, request.user_agent) end def new - processor(:LoginCredentialRequestor).process(params, cookies, request.user_agent) + processor(:LoginCredentialRequestor).process(params, current_user, request.user_agent) end def create @@ -15,18 +17,19 @@ def create end def destroy - processor(:SessionDestroyer).process(params, cookies, request.user_agent) + processor(:SessionDestroyer).process(params, current_user, request.user_agent) end def destroy_others - processor(:OtherSessionsDestroyer).process(params, cookies, request.user_agent) + processor(:OtherSessionsDestroyer).process(params, current_user, request.user_agent) end def logout - processor(:Logout).process(params, cookies, request.user_agent) + processor(:Logout).process(params, current_user, request.user_agent) end def validate_otp - processor(:SecondFactorAuthenticationAcceptor).process(params, request.user_agent) + processor(:CurrentUser).process(params, request.user_agent, ignore_two_factor:true) + processor(:SecondFactorAuthenticationAcceptor).process(params, current_user, request.user_agent) end end diff --git a/app/controllers/casino/two_factor_authenticators_controller.rb b/app/controllers/casino/two_factor_authenticators_controller.rb index 8d5271ff..97dbdc47 100644 --- a/app/controllers/casino/two_factor_authenticators_controller.rb +++ b/app/controllers/casino/two_factor_authenticators_controller.rb @@ -2,14 +2,14 @@ class CASino::TwoFactorAuthenticatorsController < CASino::ApplicationController include CASino::SessionsHelper def new - processor(:TwoFactorAuthenticatorRegistrator).process(cookies, request.user_agent) + processor(:TwoFactorAuthenticatorRegistrator).process(current_user, request.user_agent) end def create - processor(:TwoFactorAuthenticatorActivator).process(params, cookies, request.user_agent) + processor(:TwoFactorAuthenticatorActivator).process(params, current_user, request.user_agent) end def destroy - processor(:TwoFactorAuthenticatorDestroyer).process(params, cookies, request.user_agent) + processor(:TwoFactorAuthenticatorDestroyer).process(params, current_user, request.user_agent) end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb deleted file mode 100644 index de6be794..00000000 --- a/app/helpers/application_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module ApplicationHelper -end diff --git a/app/helpers/casino/application_helper.rb b/app/helpers/casino/application_helper.rb new file mode 100644 index 00000000..fd86ee7a --- /dev/null +++ b/app/helpers/casino/application_helper.rb @@ -0,0 +1,9 @@ +module CASino + module ApplicationHelper + extend ActiveSupport::Concern + + included do + include CASino::CurrentUserHelper + end + end +end diff --git a/app/helpers/casino/current_user_helper.rb b/app/helpers/casino/current_user_helper.rb new file mode 100644 index 00000000..d542f2c1 --- /dev/null +++ b/app/helpers/casino/current_user_helper.rb @@ -0,0 +1,23 @@ +module CASino::CurrentUserHelper + include CASino::ProcessorHelper + + def self.included(base) + return unless base.ancestors.include? ActionController::Base + + base.helper_method :current_user, :user_signed_in? + base.before_filter :set_current_user + end + + def set_current_user + params[:tgt] ||= cookies[:tgt] + processor(:CurrentUser).process(params, request.user_agent) + end + + def current_user + @current_user + end + + def user_signed_in? + !!current_user + end +end diff --git a/app/helpers/casino/processor_helper.rb b/app/helpers/casino/processor_helper.rb new file mode 100644 index 00000000..27696a3a --- /dev/null +++ b/app/helpers/casino/processor_helper.rb @@ -0,0 +1,8 @@ +module CASino::ProcessorHelper + protected + def processor(processor_name, listener_name = nil) + listener_name ||= processor_name + listener = CASino.const_get(:"#{listener_name}Listener").new(self) + @processor = CASino.const_get(:"#{processor_name}Processor").new(listener) + end +end \ No newline at end of file diff --git a/app/listeners/casino/current_user_listener.rb b/app/listeners/casino/current_user_listener.rb new file mode 100644 index 00000000..fd14fef0 --- /dev/null +++ b/app/listeners/casino/current_user_listener.rb @@ -0,0 +1,13 @@ +class CASino::CurrentUserListener < CASino::Listener + def user_not_logged_in + # NO-OP + end + + def user_not_logged_in! + @controller.redirect_to login_path + end + + def current_user_found(user) + assign(:current_user, user) + end +end diff --git a/app/listeners/casino/listener.rb b/app/listeners/casino/listener.rb index a05b7d1a..6af03269 100644 --- a/app/listeners/casino/listener.rb +++ b/app/listeners/casino/listener.rb @@ -1,6 +1,5 @@ module CASino class Listener - # include helpers to have the route path methods (like sessions_path) include CASino::Engine.routes.url_helpers @@ -8,9 +7,17 @@ def initialize(controller) @controller = controller end - protected def assign(name, value) @controller.instance_variable_set("@#{name}", value) end + + def assigned(name) + @controller.instance_variable_get("@#{name}") + end + + protected + def cookies + @controller.send(:cookies) + end end end diff --git a/app/listeners/casino/login_credential_acceptor_listener.rb b/app/listeners/casino/login_credential_acceptor_listener.rb index c5819a46..e1c84908 100644 --- a/app/listeners/casino/login_credential_acceptor_listener.rb +++ b/app/listeners/casino/login_credential_acceptor_listener.rb @@ -2,7 +2,7 @@ class CASino::LoginCredentialAcceptorListener < CASino::Listener def user_logged_in(url, ticket_granting_ticket, cookie_expiry_time = nil) - @controller.cookies[:tgt] = { value: ticket_granting_ticket, expires: cookie_expiry_time } + cookies[:tgt] = { value: ticket_granting_ticket, expires: cookie_expiry_time } if url.nil? @controller.redirect_to sessions_path, status: :see_other else diff --git a/app/listeners/casino/login_credential_requestor_listener.rb b/app/listeners/casino/login_credential_requestor_listener.rb index 019e27c0..c723cd7c 100644 --- a/app/listeners/casino/login_credential_requestor_listener.rb +++ b/app/listeners/casino/login_credential_requestor_listener.rb @@ -3,7 +3,7 @@ class CASino::LoginCredentialRequestorListener < CASino::Listener def user_not_logged_in(login_ticket) assign(:login_ticket, login_ticket) - @controller.cookies.delete :tgt + cookies.delete :tgt end def service_not_allowed(service) diff --git a/app/listeners/casino/logout_listener.rb b/app/listeners/casino/logout_listener.rb index bc66fa90..7fe78828 100644 --- a/app/listeners/casino/logout_listener.rb +++ b/app/listeners/casino/logout_listener.rb @@ -2,11 +2,12 @@ class CASino::LogoutListener < CASino::Listener def user_logged_out(url, redirect_immediately = false) + assign(:current_user, nil) if redirect_immediately @controller.redirect_to url, status: :see_other else assign(:url, url) end - @controller.cookies.delete :tgt + cookies.delete :tgt end end diff --git a/app/listeners/casino/second_factor_authentication_acceptor_listener.rb b/app/listeners/casino/second_factor_authentication_acceptor_listener.rb index 0248c604..81fa958f 100644 --- a/app/listeners/casino/second_factor_authentication_acceptor_listener.rb +++ b/app/listeners/casino/second_factor_authentication_acceptor_listener.rb @@ -7,7 +7,7 @@ def user_not_logged_in end def user_logged_in(url, ticket_granting_ticket, cookie_expiry_time = nil) - @controller.cookies[:tgt] = { value: ticket_granting_ticket, expires: cookie_expiry_time } + cookies[:tgt] = { value: ticket_granting_ticket, expires: cookie_expiry_time } if url.nil? @controller.redirect_to sessions_path, status: :see_other else diff --git a/app/models/casino/ticket_granting_ticket.rb b/app/models/casino/ticket_granting_ticket.rb index 10c9ddaf..7f607718 100644 --- a/app/models/casino/ticket_granting_ticket.rb +++ b/app/models/casino/ticket_granting_ticket.rb @@ -7,6 +7,13 @@ class CASino::TicketGrantingTicket < ActiveRecord::Base belongs_to :user has_many :service_tickets, dependent: :destroy + # Backport Rails 4's ActiveRecord::NullRelation + # From: http://stackoverflow.com/questions/4877931/how-to-return-an-empty-activerecord-relation + # See also: http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-none + if Rails.version.to_i < 4 + scope :none, where('1=0') + end + def self.cleanup(user = nil) if user.nil? base = self diff --git a/app/models/casino/user.rb b/app/models/casino/user.rb index 6c1a3aa4..1352afc5 100644 --- a/app/models/casino/user.rb +++ b/app/models/casino/user.rb @@ -9,4 +9,23 @@ class CASino::User < ActiveRecord::Base def active_two_factor_authenticator self.two_factor_authenticators.where(active: true).first end + + def ticket(params = {}) + query = if params + params = params.delete_if{ |k,v| v.nil? } + ticket_granting_tickets.where(params) + else + ticket_granting_tickets + end + + query.first + end + + def other_tickets(user_agent = nil) + if primary = ticket(user_agent:user_agent) + ticket_granting_tickets.where('id != ?', primary.id) + else + ticket_granting_tickets.none + end + end end diff --git a/app/processors/casino/current_user_processor.rb b/app/processors/casino/current_user_processor.rb new file mode 100644 index 00000000..7170462b --- /dev/null +++ b/app/processors/casino/current_user_processor.rb @@ -0,0 +1,52 @@ +class CASino::CurrentUserProcessor < CASino::Processor + include CASino::ProcessorConcern::LoginTickets + include CASino::ProcessorConcern::TicketGrantingTickets + + # This method will call `#user_not_logged_in` or `#current_user_found(User)` on the listener. + # @param [Hash] params A Hash delivered by the client used to located the User's Ticket Granting Ticket + # @param [String] user_agent user-agent delivered by the client + def process(params = nil, user_agent = nil, options = {}) + options ||= {} + if user = handle_process(params, user_agent, options) + @listener.current_user_found(user) + else + @listener.user_not_logged_in + end + end + + # This method will call `#user_not_logged_in!` or `#current_user_found(User)` on the listener. + # @param [Hash] params A Hash delivered by the client used to located the User's Ticket Granting Ticket + # @param [String] user_agent user-agent delivered by the client + def process!(params = nil, user_agent = nil, options = {}) + options ||= {} + if handle_process(params, user_agent, options) + @listener.current_user_found + else + @listener.user_not_logged_in! + end + end + + private + def handle_process(params, user_agent, options) + return current_user if user_signed_in? + + @params, @user_agent = (params || {}), user_agent + + ticket_granting_ticket(options).try(:user) + end + + def current_user + @listener.assigned :current_user + end + + def user_signed_in? + current_user + end + + def ticket_granting_ticket(options) + @ticket_granting_ticket ||= begin + find_valid_ticket_granting_ticket(@params[:tgt], @user_agent, options[:ignore_two_factor]) + end + end + +end \ No newline at end of file diff --git a/app/processors/casino/login_credential_requestor_processor.rb b/app/processors/casino/login_credential_requestor_processor.rb index fcc86c16..bba5ff42 100644 --- a/app/processors/casino/login_credential_requestor_processor.rb +++ b/app/processors/casino/login_credential_requestor_processor.rb @@ -3,7 +3,7 @@ class CASino::LoginCredentialRequestorProcessor < CASino::Processor include CASino::ProcessorConcern::Browser include CASino::ProcessorConcern::LoginTickets include CASino::ProcessorConcern::ServiceTickets - include CASino::ProcessorConcern::TicketGrantingTickets + include CASino::ProcessorConcern::CurrentUser # Use this method to process the request. # @@ -13,12 +13,13 @@ class CASino::LoginCredentialRequestorProcessor < CASino::Processor # * `#service_not_allowed`: The user tried to access a service that this CAS server is not allowed to serve. # # @param [Hash] params parameters supplied by user - # @param [Hash] cookies cookies supplied by user + # @param [Object] user A previously initializer User instance # @param [String] user_agent user-agent delivered by the client - def process(params = nil, cookies = nil, user_agent = nil) + def process(params = nil, user = nil, user_agent = nil) @params = params || {} - @cookies = cookies || {} - @user_agent = user_agent || {} + @user = user || current_user + @user_agent = user_agent + if check_service_allowed handle_allowed_service end @@ -26,7 +27,7 @@ def process(params = nil, cookies = nil, user_agent = nil) private def handle_allowed_service - if !@params[:renew] && (@ticket_granting_ticket = find_valid_ticket_granting_ticket(@cookies[:tgt], @user_agent)) + if !@params[:renew] && ticket_granting_ticket handle_logged_in else handle_not_logged_in @@ -63,4 +64,8 @@ def check_service_allowed def gateway_request? @params[:gateway] == 'true' && @params[:service] end + + def ticket_granting_ticket + @ticket_granting_ticket ||= @user.ticket(user_agent:@user_agent) + end end diff --git a/app/processors/casino/logout_processor.rb b/app/processors/casino/logout_processor.rb index 6ac8a523..a8320cac 100644 --- a/app/processors/casino/logout_processor.rb +++ b/app/processors/casino/logout_processor.rb @@ -1,6 +1,6 @@ # The Logout processor should be used to process GET requests to /logout. class CASino::LogoutProcessor < CASino::Processor - include CASino::ProcessorConcern::TicketGrantingTickets + include CASino::ProcessorConcern::CurrentUser # This method will call `#user_logged_out` and may supply an URL that should be presented to the user. # As per specification, the URL specified by "url" SHOULD be on the logout page with descriptive text. @@ -8,12 +8,12 @@ class CASino::LogoutProcessor < CASino::Processor # Please click here to access http://www.go-back.edu/." # # @param [Hash] params parameters supplied by user - # @param [Hash] cookies cookies supplied by user + # @param [Object] user A previously initializer User instance # @param [String] user_agent user-agent delivered by the client - def process(params = nil, cookies = nil, user_agent = nil) + def process(params = nil, user = nil, user_agent = nil) params ||= {} - cookies ||= {} - remove_ticket_granting_ticket(cookies[:tgt], user_agent) + user ||= current_user + user.ticket(user_agent:user_agent).try(:destroy) if params[:service] && CASino::ServiceRule.allowed?(params[:service]) @listener.user_logged_out(params[:service], true) else diff --git a/app/processors/casino/other_sessions_destroyer_processor.rb b/app/processors/casino/other_sessions_destroyer_processor.rb index 808b234e..f6dcab08 100644 --- a/app/processors/casino/other_sessions_destroyer_processor.rb +++ b/app/processors/casino/other_sessions_destroyer_processor.rb @@ -5,22 +5,20 @@ # This feature is not described in the CAS specification so it's completly optional # to implement this on the web application side. class CASino::OtherSessionsDestroyerProcessor < CASino::Processor - include CASino::ProcessorConcern::TicketGrantingTickets + include CASino::ProcessorConcern::CurrentUser # This method will call `#other_sessions_destroyed` and may supply an URL that should be presented to the user. # The user should be redirected to this URL immediately. # # @param [Hash] params parameters supplied by user - # @param [Hash] cookies cookies supplied by user + # @param [Object] user A previously initializer User instance # @param [String] user_agent user-agent delivered by the client - def process(params = nil, cookies = nil, user_agent = nil) + def process(params = nil, user = nil, user_agent = nil) params ||= {} - cookies ||= {} - tgt = find_valid_ticket_granting_ticket(cookies[:tgt], user_agent) - unless tgt.nil? - other_ticket_granting_tickets = tgt.user.ticket_granting_tickets.where('id != ?', tgt.id) - other_ticket_granting_tickets.destroy_all - end + user ||= current_user + + user.other_tickets(user_agent).destroy_all + @listener.other_sessions_destroyed(params[:service]) end end diff --git a/app/processors/casino/processor_concern/current_user.rb b/app/processors/casino/processor_concern/current_user.rb new file mode 100644 index 00000000..0cd7daa7 --- /dev/null +++ b/app/processors/casino/processor_concern/current_user.rb @@ -0,0 +1,18 @@ +module CASino + module ProcessorConcern + module CurrentUser + def user_signed_in? + !!assigned_user + end + + def current_user + assigned_user || CASino::User.new + end + + private + def assigned_user + @listener.assigned :current_user + end + end + end +end diff --git a/app/processors/casino/processor_concern/ticket_granting_tickets.rb b/app/processors/casino/processor_concern/ticket_granting_tickets.rb index 45ffbd07..c1ef8ac5 100644 --- a/app/processors/casino/processor_concern/ticket_granting_tickets.rb +++ b/app/processors/casino/processor_concern/ticket_granting_tickets.rb @@ -3,8 +3,7 @@ module CASino module ProcessorConcern module TicketGrantingTickets - - include CASino::ProcessorConcern::Browser + include Browser def find_valid_ticket_granting_ticket(tgt, user_agent, ignore_two_factor = false) ticket_granting_ticket = CASino::TicketGrantingTicket.where(ticket: tgt).first @@ -50,10 +49,13 @@ def load_or_initialize_user(authenticator, username, extra_attributes) end def remove_ticket_granting_ticket(ticket_granting_ticket, user_agent = nil) - tgt = find_valid_ticket_granting_ticket(ticket_granting_ticket, user_agent) - unless tgt.nil? - tgt.destroy + tgt = if ticket_granting_ticket.is_a? CASino::TicketGrantingTicket + ticket_granting_ticket + else + find_valid_ticket_granting_ticket(ticket_granting_ticket, user_agent) end + + tgt.destroy if tgt end def cleanup_expired_ticket_granting_tickets(user) diff --git a/app/processors/casino/second_factor_authentication_acceptor_processor.rb b/app/processors/casino/second_factor_authentication_acceptor_processor.rb index 9f0856be..102d674e 100644 --- a/app/processors/casino/second_factor_authentication_acceptor_processor.rb +++ b/app/processors/casino/second_factor_authentication_acceptor_processor.rb @@ -4,7 +4,7 @@ # to implement this on the web application side. class CASino::SecondFactorAuthenticationAcceptorProcessor < CASino::Processor include CASino::ProcessorConcern::ServiceTickets - include CASino::ProcessorConcern::TicketGrantingTickets + include CASino::ProcessorConcern::CurrentUser include CASino::ProcessorConcern::TwoFactorAuthenticators # The method will call one of the following methods on the listener: @@ -14,32 +14,49 @@ class CASino::SecondFactorAuthenticationAcceptorProcessor < CASino::Processor # * `#invalid_one_time_password`: The user should be asked for a new OTP. # # @param [Hash] params parameters supplied by user. The processor will look for keys :otp and :service. + # @param [Object] user A previously initializer User instance # @param [String] user_agent user-agent delivered by the client - def process(params = nil, user_agent = nil) - cookies ||= {} - tgt = find_valid_ticket_granting_ticket(params[:tgt], user_agent, true) - if tgt.nil? - @listener.user_not_logged_in - else - validation_result = validate_one_time_password(params[:otp], tgt.user.active_two_factor_authenticator) + def process(params = nil, user = nil, user_agent = nil) + @user = user || current_user + @params = params || {} + + if tgt = @user.ticket({ user_agent:user_agent, ticket:@params[:tgt] }) if validation_result.success? - tgt.awaiting_two_factor_authentication = false - tgt.save! + tgt.update_attributes!(awaiting_two_factor_authentication:false) begin - url = unless params[:service].blank? - acquire_service_ticket(tgt, params[:service], true).service_with_ticket_url - end - if tgt.long_term? - @listener.user_logged_in(url, tgt.ticket, CASino.config.ticket_granting_ticket[:lifetime_long_term].seconds.from_now) - else - @listener.user_logged_in(url, tgt.ticket) - end - rescue ServiceNotAllowedError => e - @listener.service_not_allowed(clean_service_url params[:service]) + handle_logged_in_user(tgt) + rescue ServiceNotAllowedError + @listener.service_not_allowed(clean_service_url @params[:service]) end else @listener.invalid_one_time_password end + else + @listener.user_not_logged_in end end + + private + def validation_result + @validation_result ||= begin + validate_one_time_password(@params[:otp], @user.active_two_factor_authenticator) + end + end + + def service_url(tgt) + return if @params[:service].blank? + + acquire_service_ticket(tgt, @params[:service], true).service_with_ticket_url + end + + def expiry_time(tgt) + return unless tgt.long_term? + + CASino.config.ticket_granting_ticket[:lifetime_long_term].seconds.from_now + end + + def handle_logged_in_user(tgt) + @listener.user_logged_in(service_url(tgt), tgt.ticket, expiry_time(tgt)) + end + end diff --git a/app/processors/casino/session_destroyer_processor.rb b/app/processors/casino/session_destroyer_processor.rb index 82ffe750..833398fd 100644 --- a/app/processors/casino/session_destroyer_processor.rb +++ b/app/processors/casino/session_destroyer_processor.rb @@ -4,22 +4,23 @@ # to implement this on the web application side. It is especially useful in # combination with the {CASino::SessionOverviewProcessor} processor. class CASino::SessionDestroyerProcessor < CASino::Processor + include CASino::ProcessorConcern::CurrentUser # This method will call `#ticket_not_found` or `#ticket_deleted` on the listener. # @param [Hash] params parameters supplied by user (ID of ticket-granting ticket to delete should by in params[:id]) - # @param [Hash] cookies cookies supplied by user + # @param [Object] user A previously initializer User instance # @param [String] user_agent user-agent delivered by the client - def process(params = nil, cookies = nil, user_agent = nil) + def process(params = nil, user = nil, user_agent = nil) params ||= {} - cookies ||= {} + user ||= current_user ticket = CASino::TicketGrantingTicket.where(id: params[:id]).first - owner_ticket = CASino::TicketGrantingTicket.where(ticket: cookies[:tgt]).first - if ticket.nil? || !ticket.same_user?(owner_ticket) - @listener.ticket_not_found - else + owner_ticket = user.ticket(user_agent:user_agent) + if ticket && ticket.same_user?(owner_ticket) Rails.logger.info "Destroying ticket-granting ticket '#{ticket.ticket}'" ticket.destroy @listener.ticket_deleted + else + @listener.ticket_not_found end end end diff --git a/app/processors/casino/session_overview_processor.rb b/app/processors/casino/session_overview_processor.rb index b464c4ff..def41333 100644 --- a/app/processors/casino/session_overview_processor.rb +++ b/app/processors/casino/session_overview_processor.rb @@ -3,19 +3,24 @@ # This feature is not described in the CAS specification so it's completly optional # to implement this on the web application side. class CASino::SessionOverviewProcessor < CASino::Processor - include CASino::ProcessorConcern::TicketGrantingTickets + include CASino::ProcessorConcern::CurrentUser # This method will call `#user_not_logged_in` or `#ticket_granting_tickets_found(Enumerable)` on the listener. - # @param [Hash] cookies cookies delivered by the client + # @param [Object] user A previously initializer User instance # @param [String] user_agent user-agent delivered by the client - def process(cookies = nil, user_agent = nil) - cookies ||= {} - tgt = find_valid_ticket_granting_ticket(cookies[:tgt], user_agent) - if tgt.nil? - @listener.user_not_logged_in - else - ticket_granting_tickets = tgt.user.ticket_granting_tickets.where(awaiting_two_factor_authentication: false).order('updated_at DESC') + def process(user = nil, user_agent = nil) + @user ||= current_user + + if @user.ticket(user_agent:user_agent) @listener.ticket_granting_tickets_found(ticket_granting_tickets) + else + @listener.user_not_logged_in end end + + private + def ticket_granting_tickets + @user.ticket_granting_tickets.where(awaiting_two_factor_authentication: false).order('updated_at DESC') + end + end diff --git a/app/processors/casino/two_factor_authenticator_activator_processor.rb b/app/processors/casino/two_factor_authenticator_activator_processor.rb index 8f38596b..6bd34d62 100644 --- a/app/processors/casino/two_factor_authenticator_activator_processor.rb +++ b/app/processors/casino/two_factor_authenticator_activator_processor.rb @@ -3,7 +3,7 @@ # This feature is not described in the CAS specification so it's completly optional # to implement this on the web application side. class CASino::TwoFactorAuthenticatorActivatorProcessor < CASino::Processor - include CASino::ProcessorConcern::TicketGrantingTickets + include CASino::ProcessorConcern::CurrentUser include CASino::ProcessorConcern::TwoFactorAuthenticators # The method will call one of the following methods on the listener: @@ -13,19 +13,15 @@ class CASino::TwoFactorAuthenticatorActivatorProcessor < CASino::Processor # * `#invalid_one_time_password`: The user should be asked for a new OTP. # # @param [Hash] params parameters supplied by user. The processor will look for keys :otp and :id. - # @param [Hash] cookies cookies delivered by the client + # @param [Object] user A previously initializer User instance # @param [String] user_agent user-agent delivered by the client - def process(params = nil, cookies = nil, user_agent = nil) - cookies ||= {} - params ||= {} - tgt = find_valid_ticket_granting_ticket(cookies[:tgt], user_agent) - if tgt.nil? - @listener.user_not_logged_in - else - authenticator = tgt.user.two_factor_authenticators.where(id: params[:id]).first - validation_result = validate_one_time_password(params[:otp], authenticator) + def process(params = nil, user = nil, user_agent = nil) + @user = user || current_user + @params = params || {} + + if tgt = @user.ticket(user_agent:user_agent) if validation_result.success? - tgt.user.two_factor_authenticators.where(active: true).delete_all + @user.two_factor_authenticators.where(active: true).delete_all authenticator.active = true authenticator.save! @listener.two_factor_authenticator_activated @@ -36,6 +32,17 @@ def process(params = nil, cookies = nil, user_agent = nil) @listener.invalid_two_factor_authenticator end end + else + @listener.user_not_logged_in end end + + private + def authenticator + @authenticator ||= @user.two_factor_authenticators.where(id: @params[:id]).first + end + + def validation_result + @validation_result ||= validate_one_time_password(@params[:otp], authenticator) + end end diff --git a/app/processors/casino/two_factor_authenticator_destroyer_processor.rb b/app/processors/casino/two_factor_authenticator_destroyer_processor.rb index 16396517..21367153 100644 --- a/app/processors/casino/two_factor_authenticator_destroyer_processor.rb +++ b/app/processors/casino/two_factor_authenticator_destroyer_processor.rb @@ -3,7 +3,7 @@ # This feature is not described in the CAS specification so it's completly optional # to implement this on the web application side. class CASino::TwoFactorAuthenticatorDestroyerProcessor < CASino::Processor - include CASino::ProcessorConcern::TicketGrantingTickets + include CASino::ProcessorConcern::CurrentUser include CASino::ProcessorConcern::TwoFactorAuthenticators # The method will call one of the following methods on the listener: @@ -12,22 +12,26 @@ class CASino::TwoFactorAuthenticatorDestroyerProcessor < CASino::Processor # * `#invalid_two_factor_authenticator`: The two-factor authenticator is not valid. # # @param [Hash] params parameters supplied by user. The processor will look for key :id. - # @param [Hash] cookies cookies delivered by the client + # @param [Object] user A previously initializer User instance # @param [String] user_agent user-agent delivered by the client - def process(params = nil, cookies = nil, user_agent = nil) - cookies ||= {} - params ||= {} - tgt = find_valid_ticket_granting_ticket(cookies[:tgt], user_agent) - if tgt.nil? - @listener.user_not_logged_in - else - authenticator = tgt.user.two_factor_authenticators.where(id: params[:id]).first + def process(params = nil, user = nil, user_agent = nil) + @params = params || {} + @user = user || current_user + + if @user.ticket(user_agent:user_agent) if authenticator authenticator.destroy @listener.two_factor_authenticator_destroyed else @listener.invalid_two_factor_authenticator end + else + @listener.user_not_logged_in end end + + private + def authenticator + authenticator ||= @user.two_factor_authenticators.where(id: @params[:id]).first + end end diff --git a/app/processors/casino/two_factor_authenticator_overview_processor.rb b/app/processors/casino/two_factor_authenticator_overview_processor.rb index 42ffd6d8..81b1dead 100644 --- a/app/processors/casino/two_factor_authenticator_overview_processor.rb +++ b/app/processors/casino/two_factor_authenticator_overview_processor.rb @@ -3,18 +3,24 @@ # This feature is not described in the CAS specification so it's completly optional # to implement this on the web application side. class CASino::TwoFactorAuthenticatorOverviewProcessor < CASino::Processor - include CASino::ProcessorConcern::TicketGrantingTickets + include CASino::ProcessorConcern::CurrentUser # This method will call `#user_not_logged_in` or `#two_factor_authenticators_found(Enumerable)` on the listener. - # @param [Hash] cookies cookies delivered by the client + # @param [Object] user A previously initializer User instance # @param [String] user_agent user-agent delivered by the client - def process(cookies = nil, user_agent = nil) - cookies ||= {} - tgt = find_valid_ticket_granting_ticket(cookies[:tgt], user_agent) - if tgt.nil? - @listener.user_not_logged_in + def process(user = nil, user_agent = nil) + @user = user || current_user + + if @user.ticket(user_agent:user_agent) + @listener.two_factor_authenticators_found(authenticators) else - @listener.two_factor_authenticators_found(tgt.user.two_factor_authenticators.where(active: true)) + @listener.user_not_logged_in end end + + private + def authenticators + @user.two_factor_authenticators.where(active: true) + end + end diff --git a/app/processors/casino/two_factor_authenticator_registrator_processor.rb b/app/processors/casino/two_factor_authenticator_registrator_processor.rb index b50e0a54..e3312f00 100644 --- a/app/processors/casino/two_factor_authenticator_registrator_processor.rb +++ b/app/processors/casino/two_factor_authenticator_registrator_processor.rb @@ -6,19 +6,24 @@ # This feature is not described in the CAS specification so it's completly optional # to implement this on the web application side. class CASino::TwoFactorAuthenticatorRegistratorProcessor < CASino::Processor - include CASino::ProcessorConcern::TicketGrantingTickets + include CASino::ProcessorConcern::CurrentUser # This method will call `#user_not_logged_in` or `#two_factor_authenticator_registered(two_factor_authenticator)` on the listener. - # @param [Hash] cookies cookies delivered by the client + # @param [Object] user A previously initializer User instance # @param [String] user_agent user-agent delivered by the client - def process(cookies = nil, user_agent = nil) - cookies ||= {} - tgt = find_valid_ticket_granting_ticket(cookies[:tgt], user_agent) - if tgt.nil? - @listener.user_not_logged_in - else - two_factor_authenticator = tgt.user.two_factor_authenticators.create! secret: ROTP::Base32.random_base32 + def process(user = nil, user_agent = nil) + @user = user || current_user + + if @user.ticket(user_agent:user_agent) @listener.two_factor_authenticator_registered(two_factor_authenticator) + else + @listener.user_not_logged_in end end + + private + def two_factor_authenticator + @two_factor_authenticator ||= @user.two_factor_authenticators.create! secret: ROTP::Base32.random_base32 + end + end diff --git a/app/views/casino/sessions/index.html.erb b/app/views/casino/sessions/index.html.erb index 036dadf0..adaf25c3 100644 --- a/app/views/casino/sessions/index.html.erb +++ b/app/views/casino/sessions/index.html.erb @@ -4,7 +4,7 @@
- <%= raw t('sessions.currently_logged_in_as', :username => @ticket_granting_tickets[0].user.username) %> + <%= raw t('sessions.currently_logged_in_as', :username => current_user.username) %>
<%= link_to t('sessions.label_logout_button'), logout_path, :class => 'button' %> diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 7f70953c..8e18ed48 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -28,9 +28,10 @@ describe 'GET "logout"' do it 'calls the process method of the Logout processor' do - CASino::LogoutProcessor.any_instance.should_receive(:process) do |params, cookies, user_agent| + CASino::CurrentUserProcessor.any_instance.should_receive(:process) + CASino::LogoutProcessor.any_instance.should_receive(:process) do |params, user, user_agent| params.should == controller.params - cookies.should == controller.cookies + user.should == controller.current_user user_agent.should == request.user_agent end get :logout, use_route: :casino @@ -39,6 +40,7 @@ describe 'GET "index"' do it 'calls the process method of the SessionOverview processor' do + CASino::CurrentUserProcessor.any_instance.should_receive(:process) CASino::TwoFactorAuthenticatorOverviewProcessor.any_instance.should_receive(:process) CASino::SessionOverviewProcessor.any_instance.should_receive(:process) get :index, use_route: :casino @@ -47,12 +49,14 @@ describe 'DELETE "destroy"' do let(:id) { '123' } - let(:tgt) { 'TGT-foobar' } + let(:user) { double('user') } it 'calls the process method of the SessionOverview processor' do - request.cookies[:tgt] = tgt - CASino::SessionDestroyerProcessor.any_instance.should_receive(:process) do |params, cookies, user_agent| + controller.stub(:current_user).and_return user + + CASino::CurrentUserProcessor.any_instance.should_receive(:process) + CASino::SessionDestroyerProcessor.any_instance.should_receive(:process) do |params, user, user_agent| params[:id].should == id - cookies[:tgt].should == tgt + user == controller.current_user user_agent.should == request.user_agent @controller.render nothing: true end @@ -62,6 +66,7 @@ describe 'GET "destroy_others"' do it 'calls the process method of the OtherSessionsDestroyer' do + CASino::CurrentUserProcessor.any_instance.should_receive(:process) CASino::OtherSessionsDestroyerProcessor.any_instance.should_receive(:process) do @controller.render nothing: true end diff --git a/spec/controllers/two_factor_authenticators_controller_spec.rb b/spec/controllers/two_factor_authenticators_controller_spec.rb index a6a49047..672d7e53 100644 --- a/spec/controllers/two_factor_authenticators_controller_spec.rb +++ b/spec/controllers/two_factor_authenticators_controller_spec.rb @@ -22,9 +22,9 @@ let(:tgt) { 'TGT-foobar' } it 'calls the process method of the TwoFactorAuthenticatorDestroyer processor' do request.cookies[:tgt] = tgt - CASino::TwoFactorAuthenticatorDestroyerProcessor.any_instance.should_receive(:process) do |params, cookies, user_agent| + CASino::TwoFactorAuthenticatorDestroyerProcessor.any_instance.should_receive(:process) do |params, user, user_agent| params[:id].should == id - cookies[:tgt].should == tgt + user.should == @controller.current_user user_agent.should == request.user_agent @controller.render nothing: true end diff --git a/spec/model/user_spec.rb b/spec/model/user_spec.rb new file mode 100644 index 00000000..10ba696c --- /dev/null +++ b/spec/model/user_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' + +describe CASino::User do + describe '#active_two_factor_authenticator' do + let(:user) { FactoryGirl.create :user } + + let!(:active_auth) { FactoryGirl.create :two_factor_authenticator, user:user } + let!(:inactive_auth) { FactoryGirl.create :two_factor_authenticator, :inactive, user:user } + + subject { user.active_two_factor_authenticator } + + it 'returns the active authenticator' do + expect(subject).to eq active_auth + end + end + + describe '#ticket' do + let(:user) { FactoryGirl.create :user } + + let!(:primary_ticket) { FactoryGirl.create :ticket_granting_ticket, user:user, user_agent:'Chrome' } + let!(:other_ticket) { FactoryGirl.create :ticket_granting_ticket, user:user, user_agent:'Firefox' } + let!(:another_ticket) { FactoryGirl.create :ticket_granting_ticket, user:user, user_agent:'Firefox' } + let!(:no_ua_ticket) { FactoryGirl.create :ticket_granting_ticket, user:user, user_agent:nil } + + let(:params) { nil } + + subject { user.ticket params } + + it 'returns the first ticket found' do + expect(subject).to eq primary_ticket + end + + context 'with search parameters' do + let(:params) { { user_agent:other_ticket.user_agent } } + + it 'returns the first matching ticket' do + expect(subject).to eq other_ticket + end + + context 'that contain nil values' do + let(:params) { { user_agent:nil } } + + it 'does not include those parameters in the search' do + expect(subject).to eq primary_ticket + end + end + end + end + + describe '#other_tickets' do + let(:user) { FactoryGirl.create :user } + + let!(:primary_ticket) { FactoryGirl.create :ticket_granting_ticket, user:user, user_agent:'Chrome' } + let!(:other_ticket) { FactoryGirl.create :ticket_granting_ticket, user:user, user_agent:'Firefox' } + + let(:user_agent) { nil } + + subject { user.other_tickets user_agent } + + it 'returns an empty query' do + expect(subject).to be_an ActiveRecord::Relation + end + + context 'with a :user_agent specified' do + context 'that has no matching Ticket Granting Ticket' do + let(:user_agent) { 'Safari' } + + it 'returns an empty query' do + expect(subject).to be_an ActiveRecord::Relation + end + end + + context 'that matches a Ticket Granting Ticket' do + let(:user_agent) { primary_ticket.user_agent } + + it 'returns all other non-matching Ticket Granting Tickets' do + expect(subject).to eq [other_ticket] + end + end + end + end +end diff --git a/spec/processor/current_user_spec.rb b/spec/processor/current_user_spec.rb new file mode 100644 index 00000000..e4684b02 --- /dev/null +++ b/spec/processor/current_user_spec.rb @@ -0,0 +1,104 @@ +require 'spec_helper' + +describe CASino::CurrentUserProcessor do + let(:user) { FactoryGirl.create :user } + let(:current_user) { nil } + let(:cookies) { nil } + let(:user_agent) { 'TestBrowser 1.0' } + let(:listener) do + double('listener').tap do |l| + l.stub('assigned').with(:current_user).and_return(current_user) + end + end + let(:processor) { described_class.new(listener) } + + describe '#process' do + subject { processor.process(cookies, user_agent) } + + context 'with a previously set :current_user value' do + let(:current_user) { user } + + it 'calls the :current_user_found callback' do + listener.should_receive(:current_user_found) + subject + end + end + + context 'with an existing Ticket Granting Ticket' do + let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket, user: user } + let(:cookies) { { tgt:ticket_granting_ticket.ticket } } + + it 'calls the :current_user_found callback' do + listener.should_receive(:current_user_found) + subject + end + + context 'that has expired' do + let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket, :expired, user:user } + + it 'calls the #user_not_logged_in method on the listener' do + listener.should_receive(:user_not_logged_in) + subject + end + end + + context 'with a different browser' do + 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) + subject + end + end + end + + context 'when two-factor authentication is pending' do + let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket, :awaiting_two_factor_authentication, user: user } + let(:cookies) { { tgt:ticket_granting_ticket.ticket } } + + it 'calls the #user_not_logged_in method on the listener' do + listener.should_receive(:user_not_logged_in) + processor.process(cookies, user_agent) + end + + context 'and we opt ignore that' do + it 'calls the #current_user_found method on the listener' do + listener.should_receive(:current_user_found) + processor.process(cookies, user_agent, ignore_two_factor:true) + end + end + end + + context 'with an invalid Ticket Granting Ticket' do + let(:cookies) { { tgt:'TGT-INVALID' } } + + it 'calls the :user_not_logged_in callback' do + listener.should_receive(:user_not_logged_in) + subject + end + end + end + + describe '#process!' do + subject { processor.process!(cookies, user_agent) } + + context 'with an existing Ticket Granting Ticket' do + let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket, user: user } + let(:cookies) { { tgt:ticket_granting_ticket.ticket } } + + it 'calls the :current_user_found callback' do + listener.should_receive(:current_user_found) + subject + end + end + + context 'with an invalid Ticket Granting Ticket' do + let(:cookies) { { tgt:'TGT-INVALID' } } + + it 'calls the :user_not_logged_in! callback' do + listener.should_receive(:user_not_logged_in!) + subject + end + end + end +end diff --git a/spec/processor/login_credential_requestor_spec.rb b/spec/processor/login_credential_requestor_spec.rb index b5654767..b8363564 100644 --- a/spec/processor/login_credential_requestor_spec.rb +++ b/spec/processor/login_credential_requestor_spec.rb @@ -2,7 +2,8 @@ describe CASino::LoginCredentialRequestorProcessor do describe '#process' do - let(:listener) { Object.new } + let(:user) { nil } + let(:listener) { double('listener', assigned:user) } let(:processor) { described_class.new(listener) } context 'with a not allowed service' do @@ -47,33 +48,12 @@ end context 'when logged in' do - let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket } + let(:user) { FactoryGirl.create :user } + let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket, user: user } let(:user_agent) { ticket_granting_ticket.user_agent } - let(:cookies) { { tgt: ticket_granting_ticket.ticket } } - before(:each) do - listener.stub(:user_logged_in) - end - - context 'when two-factor authentication is pending' do - 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)) - processor.process(nil, cookies, user_agent) - end - end - - context 'when ticket-granting ticket expired' do - before(:each) do - ticket_granting_ticket.created_at = 25.hours.ago - ticket_granting_ticket.save! - 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)) - processor.process(nil, cookies, user_agent) - end + before do + FactoryGirl.create :ticket_granting_ticket, user: user end context 'with a service' do @@ -82,19 +62,22 @@ it 'calls the #user_logged_in method on the listener' do listener.should_receive(:user_logged_in).with(/^#{service}\?ticket=ST\-/) - processor.process(params, cookies, user_agent) + processor.process(params, user, user_agent) end it 'generates a service ticket' do + listener.stub(:user_logged_in) lambda do - processor.process(params, cookies, user_agent) + processor.process(params, user, user_agent) end.should change(CASino::ServiceTicket, :count).by(1) end context 'with renew parameter' do + let(:params) { super().merge renew:'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)) - processor.process(params.merge({ renew: 'true' }), cookies) + processor.process(params, user, user_agent) end end end @@ -105,30 +88,22 @@ it 'does not remove the attributes' do listener.should_receive(:user_logged_in).with(/\?a%5B%5D=test&a%5B%5D=example&ticket=ST\-[^&]+$/) - processor.process(params, cookies, user_agent) + processor.process(params, user, user_agent) end end context 'without a service' do it 'calls the #user_logged_in method on the listener' do listener.should_receive(:user_logged_in).with(nil) - processor.process(nil, cookies, user_agent) + processor.process end it 'does not generate a service ticket' do + listener.stub(:user_logged_in) lambda do - processor.process(nil, cookies, user_agent) + processor.process end.should change(CASino::ServiceTicket, :count).by(0) end - - context 'with a changed browser' do - 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)) - processor.process(nil, cookies, user_agent) - end - end end end end diff --git a/spec/processor/logout_other_sessions_spec.rb b/spec/processor/logout_other_sessions_spec.rb index 4bc80a81..15407a5f 100644 --- a/spec/processor/logout_other_sessions_spec.rb +++ b/spec/processor/logout_other_sessions_spec.rb @@ -4,7 +4,7 @@ describe '#process' do let(:listener) { Object.new } let(:processor) { described_class.new(listener) } - let(:cookies) { { tgt: tgt } } + let(:user) { FactoryGirl.create :user } let(:url) { nil } let(:params) { { :service => url } unless url.nil? } @@ -13,7 +13,6 @@ end context 'with an existing ticket-granting ticket' do - let(:user) { FactoryGirl.create :user } let!(:other_users_ticket_granting_tickets) { FactoryGirl.create_list :ticket_granting_ticket, 3 } let!(:other_ticket_granting_tickets) { FactoryGirl.create_list :ticket_granting_ticket, 3, user: user } let!(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket, user: user } @@ -22,13 +21,13 @@ it 'deletes all other ticket-granting tickets' do lambda do - processor.process(params, cookies, user_agent) + processor.process(params, user, user_agent) end.should change(CASino::TicketGrantingTicket, :count).by(-3) end it 'calls the #user_logged_out method on the listener' do listener.should_receive(:other_sessions_destroyed).with(nil) - processor.process(params, cookies, user_agent) + processor.process(params, user, user_agent) end context 'with an URL' do @@ -36,7 +35,7 @@ it 'calls the #user_logged_out method on the listener and passes the URL' do listener.should_receive(:other_sessions_destroyed).with(url) - processor.process(params, cookies, user_agent) + processor.process(params, user, user_agent) end end end @@ -46,7 +45,7 @@ it 'calls the #other_sessions_destroyed method on the listener' do listener.should_receive(:other_sessions_destroyed).with(nil) - processor.process(params, cookies) + processor.process(params, user) end end end diff --git a/spec/processor/logout_spec.rb b/spec/processor/logout_spec.rb index 5ff186d1..27b165fe 100644 --- a/spec/processor/logout_spec.rb +++ b/spec/processor/logout_spec.rb @@ -2,29 +2,25 @@ describe CASino::LogoutProcessor do describe '#process' do - let(:listener) { Object.new } + let(:listener) { double('listener') } let(:processor) { described_class.new(listener) } - let(:cookies) { { tgt: tgt } } + let(:user) { FactoryGirl.create :user } let(:url) { nil } let(:params) { { :url => url } unless url.nil? } - before(:each) do - listener.stub(:user_logged_out) - end - context 'with an existing ticket-granting ticket' do - let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket } - let(:tgt) { ticket_granting_ticket.ticket } + let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket, user: user } let(:user_agent) { ticket_granting_ticket.user_agent } it 'deletes the ticket-granting ticket' do - processor.process(params, cookies, user_agent) + listener.stub(:user_logged_out) + processor.process(params, user, user_agent) CASino::TicketGrantingTicket.where(id: ticket_granting_ticket.id).first.should == nil end it 'calls the #user_logged_out method on the listener' do listener.should_receive(:user_logged_out).with(nil) - processor.process(params, cookies, user_agent) + processor.process(params, user, user_agent) end context 'with an URL' do @@ -32,7 +28,7 @@ it 'calls the #user_logged_out method on the listener and passes the URL' do listener.should_receive(:user_logged_out).with(url) - processor.process(params, cookies, user_agent) + processor.process(params, user, user_agent) end end @@ -43,7 +39,7 @@ context '(whitelisted)' do it 'calls the #user_logged_out method on the listener and passes the URL and the redirect_immediate flag' do listener.should_receive(:user_logged_out).with(url, true) - processor.process(params, cookies, user_agent) + processor.process(params, user, user_agent) end end @@ -54,19 +50,10 @@ it 'calls the #user_logged_out method on the listener and passes no URL' do listener.should_receive(:user_logged_out).with(nil) - processor.process(params, cookies, user_agent) + processor.process(params, user, user_agent) end end end end - - context 'with an invlaid ticket-granting ticket' do - let(:tgt) { 'TGT-lalala' } - - it 'calls the #user_logged_out method on the listener' do - listener.should_receive(:user_logged_out).with(nil) - processor.process(params, cookies) - end - end end end diff --git a/spec/processor/other_session_destroyer_spec.rb b/spec/processor/other_session_destroyer_spec.rb new file mode 100644 index 00000000..4dce0841 --- /dev/null +++ b/spec/processor/other_session_destroyer_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe CASino::OtherSessionsDestroyerProcessor do + describe '#process' do + let(:listener) { double('listener', assigned:nil) } + let(:processor) { described_class.new(listener) } + let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket } + let(:user) { ticket_granting_ticket.user } + let(:user_agent) { ticket_granting_ticket.user_agent } + let(:params) { { service:'SERVICE' } } + + context 'with an existing ticket-granting ticket' do + let(:params) { super().merge(id: ticket_granting_ticket.id) } + + it 'deletes all other ticket-granting tickets' do + FactoryGirl.create_list :ticket_granting_ticket, 2, user:user + listener.stub(:other_sessions_destroyed) + lambda do + processor.process(params, user, user_agent) + end.should change(CASino::TicketGrantingTicket, :count).by(-2) + end + + it 'does not delete the ticket-granting ticket' do + listener.stub(:other_sessions_destroyed) + processor.process(params, user, user_agent) + CASino::TicketGrantingTicket.find(params[:id]).should_not be_nil + end + + it 'calls the #other_sessions_destroyed method on the listener' do + listener.should_receive(:other_sessions_destroyed).with('SERVICE') + processor.process(params, user, user_agent) + end + end + + context 'with an invalid ticket-granting ticket' do + let(:user) { nil } + it 'does not delete any ticket-granting ticket' do + FactoryGirl.create_list :ticket_granting_ticket, 2 + listener.stub(:other_sessions_destroyed) + + user_agent + + lambda do + processor.process(params, user, user_agent) + end.should_not change(CASino::TicketGrantingTicket, :count) + end + + it 'calls the #ticket_not_found method on the listener' do + listener.should_receive(:other_sessions_destroyed).with('SERVICE') + processor.process(params, user, user_agent) + end + end + end +end \ No newline at end of file diff --git a/spec/processor/second_factor_authenticaton_acceptor_spec.rb b/spec/processor/second_factor_authenticaton_acceptor_spec.rb index f0acd207..b52cf78d 100644 --- a/spec/processor/second_factor_authenticaton_acceptor_spec.rb +++ b/spec/processor/second_factor_authenticaton_acceptor_spec.rb @@ -2,15 +2,9 @@ describe CASino::SecondFactorAuthenticationAcceptorProcessor do describe '#process' do - let(:listener) { Object.new } + let(:listener) { double('listener', assigned:user) } let(:processor) { described_class.new(listener) } - before(:each) do - listener.stub(:user_not_logged_in) - listener.stub(:invalid_one_time_password) - listener.stub(:user_logged_in) - end - context 'with an existing ticket-granting ticket' do let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket, :awaiting_two_factor_authentication } let(:user) { ticket_granting_ticket.user } @@ -29,12 +23,13 @@ end it 'calls the `#user_logged_in` method an the listener' do - listener.should_receive(:user_logged_in).with(/^#{service}\?ticket=ST\-/, /^TGC\-/) - processor.process(params, user_agent) + listener.should_receive(:user_logged_in).with(/^#{service}\?ticket=ST\-/, /^TGC\-/, nil) + processor.process(params, user, user_agent) end it 'does activate the ticket-granting ticket' do - processor.process(params, user_agent) + listener.stub(:user_logged_in) + processor.process(params, user, user_agent) ticket_granting_ticket.reload ticket_granting_ticket.should_not be_awaiting_two_factor_authentication end @@ -46,7 +41,7 @@ it 'calls the #user_logged_in method on the listener with an expiration date set' do listener.should_receive(:user_logged_in).with(/^#{service}\?ticket=ST\-/, /^TGC\-/, kind_of(Time)) - processor.process(params, user_agent) + processor.process(params, user, user_agent) end end @@ -55,10 +50,11 @@ FactoryGirl.create :service_rule, :regex, url: '^https://.*' end let(:service) { 'http://www.example.org/' } + let(:params) { super().merge(service:service) } it 'calls the #service_not_allowed method on the listener' do listener.should_receive(:service_not_allowed).with(service) - processor.process(params.merge(service: service), user_agent) + processor.process(params, user, user_agent) end end end @@ -70,11 +66,12 @@ it 'calls the `#invalid_one_time_password` method an the listener' do listener.should_receive(:invalid_one_time_password).with(no_args) - processor.process(params, user_agent) + processor.process(params, user, user_agent) end it 'does not activate the ticket-granting ticket' do - processor.process(params, user_agent) + listener.stub(:invalid_one_time_password) + processor.process(params, user, user_agent) ticket_granting_ticket.reload ticket_granting_ticket.should be_awaiting_two_factor_authentication end @@ -82,12 +79,12 @@ end end - context 'with an invalid ticket-granting ticket' do - let(:tgt) { 'TGT-lalala' } - let(:user_agent) { 'TestBrowser 1.0' } + context 'with no logged in user' do + let(:user) { nil } + it 'calls the #user_not_logged_in method on the listener' do listener.should_receive(:user_not_logged_in).with(no_args) - processor.process({tgt: tgt}, user_agent) + processor.process end end end diff --git a/spec/processor/session_destroyer_spec.rb b/spec/processor/session_destroyer_spec.rb index 1a08cbc1..db2f2563 100644 --- a/spec/processor/session_destroyer_spec.rb +++ b/spec/processor/session_destroyer_spec.rb @@ -2,17 +2,11 @@ describe CASino::SessionDestroyerProcessor do describe '#process' do - let(:listener) { Object.new } + let(:listener) { double('listener', assigned:user) } let(:processor) { described_class.new(listener) } let(:owner_ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket } let(:user) { owner_ticket_granting_ticket.user } let(:user_agent) { owner_ticket_granting_ticket.user_agent } - let(:cookies) { { tgt: owner_ticket_granting_ticket.ticket } } - - before(:each) do - listener.stub(:ticket_deleted) - listener.stub(:ticket_not_found) - end context 'with an existing ticket-granting ticket' do let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket, user: user } @@ -21,36 +15,37 @@ let(:params) { { id: ticket_granting_ticket.id } } it 'deletes exactly one ticket-granting ticket' do + listener.stub(:ticket_deleted) ticket_granting_ticket - owner_ticket_granting_ticket lambda do - processor.process(params, cookies, user_agent) + processor.process(params, user, user_agent) end.should change(CASino::TicketGrantingTicket, :count).by(-1) end it 'deletes the ticket-granting ticket' do - processor.process(params, cookies, user_agent) + listener.stub(:ticket_deleted) + processor.process(params, user, user_agent) CASino::TicketGrantingTicket.where(id: params[:id]).length.should == 0 end it 'calls the #ticket_deleted method on the listener' do listener.should_receive(:ticket_deleted).with(no_args) - processor.process(params, cookies, user_agent) + processor.process(params, user, user_agent) end end context 'with an invalid ticket-granting ticket' do let(:params) { { id: 99999 } } it 'does not delete a ticket-granting ticket' do - owner_ticket_granting_ticket + listener.stub(:ticket_not_found) lambda do - processor.process(params, cookies, user_agent) + processor.process(params, user, user_agent) end.should_not change(CASino::TicketGrantingTicket, :count) end it 'calls the #ticket_not_found method on the listener' do listener.should_receive(:ticket_not_found).with(no_args) - processor.process(params, cookies, user_agent) + processor.process(params, user, user_agent) end end @@ -59,16 +54,16 @@ let(:params) { { id: ticket_granting_ticket.id } } it 'does not delete a ticket-granting ticket' do - owner_ticket_granting_ticket + listener.stub(:ticket_not_found) ticket_granting_ticket lambda do - processor.process(params, cookies, user_agent) + processor.process(params, user, user_agent) end.should change(CASino::TicketGrantingTicket, :count).by(0) end it 'calls the #ticket_not_found method on the listener' do listener.should_receive(:ticket_not_found).with(no_args) - processor.process(params, cookies, user_agent) + processor.process(params, user, user_agent) end end end diff --git a/spec/processor/session_overview_spec.rb b/spec/processor/session_overview_spec.rb index 1e48c53c..b49c916a 100644 --- a/spec/processor/session_overview_spec.rb +++ b/spec/processor/session_overview_spec.rb @@ -2,47 +2,40 @@ describe CASino::SessionOverviewProcessor do describe '#process' do - let(:listener) { Object.new } + let(:listener) { double('listener', assigned:user) } let(:processor) { described_class.new(listener) } let(:other_ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket } let(:user) { other_ticket_granting_ticket.user } let(:user_agent) { other_ticket_granting_ticket.user_agent } - let(:cookies) { { tgt: tgt } } - - before(:each) do - listener.stub(:user_not_logged_in) - listener.stub(:ticket_granting_tickets_found) - other_ticket_granting_ticket - end context 'with an existing ticket-granting ticket' do - let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket, user: user } - let(:tgt) { ticket_granting_ticket.ticket } + before do + FactoryGirl.create :ticket_granting_ticket, user: user + end it 'calls the #ticket_granting_tickets_found method on the listener' do listener.should_receive(:ticket_granting_tickets_found) do |tickets| tickets.length.should == 2 end - processor.process(cookies, user_agent) + processor.process(user, user_agent) end end context 'with a ticket-granting ticket with same username but different authenticator' do let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket } - let(:tgt) { ticket_granting_ticket.ticket } it 'calls the #ticket_granting_tickets_found method on the listener' do listener.should_receive(:ticket_granting_tickets_found) do |tickets| tickets.length.should == 1 end - processor.process(cookies, user_agent) + processor.process(user, user_agent) end end - context 'with an invalid ticket-granting ticket' do - let(:tgt) { 'TGT-lalala' } + context 'without a logged in user' do + let(:user) { nil } it 'calls the #user_not_logged_in method on the listener' do listener.should_receive(:user_not_logged_in).with(no_args) - processor.process(cookies, user_agent) + processor.process(user, user_agent) end end end diff --git a/spec/processor/two_factor_authenticator_activator_spec.rb b/spec/processor/two_factor_authenticator_activator_spec.rb index bf106b39..ee2af124 100644 --- a/spec/processor/two_factor_authenticator_activator_spec.rb +++ b/spec/processor/two_factor_authenticator_activator_spec.rb @@ -2,22 +2,12 @@ describe CASino::TwoFactorAuthenticatorActivatorProcessor do describe '#process' do - let(:listener) { Object.new } + let(:listener) { double(:listener, assigned:user) } let(:processor) { described_class.new(listener) } - let(:cookies) { { tgt: tgt } } - - before(:each) do - listener.stub(:user_not_logged_in) - listener.stub(:two_factor_authenticator_activated) - listener.stub(:invalid_two_factor_authenticator) - listener.stub(:invalid_one_time_password) - listener.stub(:two_factor_authenticator_activated) - end context 'with an existing ticket-granting ticket' do let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket } let(:user) { ticket_granting_ticket.user } - let(:tgt) { ticket_granting_ticket.ticket } let(:user_agent) { ticket_granting_ticket.user_agent } let(:id) { two_factor_authenticator.id } let(:otp) { '123456' } @@ -34,7 +24,7 @@ it 'calls the `#invalid_two_factor_authenticator` method an the listener' do listener.should_receive(:invalid_two_factor_authenticator).with(no_args) - processor.process(params, cookies, user_agent) + processor.process(params, user, user_agent) end end @@ -48,13 +38,15 @@ it 'calls the `#invalid_two_factor_authenticator` method an the listener' do listener.should_receive(:invalid_two_factor_authenticator).with(no_args) - processor.process(params, cookies, user_agent) + processor.process(params, user, user_agent) end end end context 'with a valid authenticator' do - let(:two_factor_authenticator) { FactoryGirl.create :two_factor_authenticator, :inactive, user: user } + let(:two_factor_authenticator) do + FactoryGirl.create :two_factor_authenticator, :inactive, user: user + end context 'with a valid OTP' do before(:each) do @@ -63,11 +55,12 @@ it 'calls the `#two_factor_authenticator_activated` method an the listener' do listener.should_receive(:two_factor_authenticator_activated).with(no_args) - processor.process(params, cookies, user_agent) + processor.process(params, user, user_agent) end it 'does activate the authenticator' do - processor.process(params, cookies, user_agent) + listener.stub(:two_factor_authenticator_activated) + processor.process(params, user, user_agent) two_factor_authenticator.reload two_factor_authenticator.should be_active end @@ -76,13 +69,15 @@ let!(:other_two_factor_authenticator) { FactoryGirl.create :two_factor_authenticator, user: user } it 'does activate the authenticator' do - processor.process(params, cookies, user_agent) + listener.stub(:two_factor_authenticator_activated) + processor.process(params, user, user_agent) two_factor_authenticator.reload two_factor_authenticator.should be_active end it 'does delete the other authenticator' do - processor.process(params, cookies, user_agent) + listener.stub(:two_factor_authenticator_activated) + processor.process(params, user, user_agent) lambda do other_two_factor_authenticator.reload end.should raise_error(ActiveRecord::RecordNotFound) @@ -98,11 +93,12 @@ it 'calls the `#invalid_one_time_password` method an the listener' do listener.should_receive(:invalid_one_time_password).with(two_factor_authenticator) - processor.process(params, cookies, user_agent) + processor.process(params, user, user_agent) end it 'does not activate the authenticator' do - processor.process(params, cookies, user_agent) + listener.stub(:invalid_one_time_password) + processor.process(params, user, user_agent) two_factor_authenticator.reload two_factor_authenticator.should_not be_active end @@ -110,12 +106,11 @@ end end - context 'with an invalid ticket-granting ticket' do - let(:tgt) { 'TGT-lalala' } - let(:user_agent) { 'TestBrowser 1.0' } + context 'without a logged in user' do + let(:user) { nil } it 'calls the #user_not_logged_in method on the listener' do listener.should_receive(:user_not_logged_in).with(no_args) - processor.process(nil, cookies, user_agent) + processor.process(nil, user) end end end diff --git a/spec/processor/two_factor_authenticator_destroyer_spec.rb b/spec/processor/two_factor_authenticator_destroyer_spec.rb index 2c1762a3..2061d906 100644 --- a/spec/processor/two_factor_authenticator_destroyer_spec.rb +++ b/spec/processor/two_factor_authenticator_destroyer_spec.rb @@ -2,15 +2,8 @@ describe CASino::TwoFactorAuthenticatorDestroyerProcessor do describe '#process' do - let(:listener) { Object.new } + let(:listener) { double('listener', assigned:user) } let(:processor) { described_class.new(listener) } - let(:cookies) { { tgt: tgt } } - - before(:each) do - listener.stub(:user_not_logged_in) - listener.stub(:two_factor_authenticator_destroyed) - listener.stub(:invalid_two_factor_authenticator) - end context 'with an existing ticket-granting ticket' do let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket } @@ -24,20 +17,22 @@ it 'calls the #two_factor_authenticator_destroyed method on the listener' do listener.should_receive(:two_factor_authenticator_destroyed).with(no_args) - processor.process(params, cookies, user_agent) + processor.process(params, user, user_agent) end it 'deletes the two-factor authenticator' do - processor.process(params, cookies, user_agent) + listener.stub(:two_factor_authenticator_destroyed) + processor.process(params, user, user_agent) lambda do two_factor_authenticator.reload end.should raise_error(ActiveRecord::RecordNotFound) end it 'does not delete other two-factor authenticators' do + listener.stub(:two_factor_authenticator_destroyed) other = FactoryGirl.create :two_factor_authenticator lambda do - processor.process(params, cookies, user_agent) + processor.process(params, user, user_agent) end.should change(CASino::TwoFactorAuthenticator, :count).by(-1) end end @@ -47,24 +42,23 @@ it 'calls the #invalid_two_factor_authenticator method on the listener' do listener.should_receive(:invalid_two_factor_authenticator).with(no_args) - processor.process(params, cookies, user_agent) + processor.process(params, user, user_agent) end it 'does not delete two-factor authenticators' do + listener.stub(:invalid_two_factor_authenticator) lambda do - processor.process(params, cookies, user_agent) + processor.process(params, user, user_agent) end.should_not change(CASino::TwoFactorAuthenticator, :count) end end end - context 'with an invalid ticket-granting ticket' do - let(:params) { {} } - let(:tgt) { 'TGT-lalala' } - let(:user_agent) { 'TestBrowser 1.0' } + context 'without a logged in user' do + let(:user) { nil } it 'calls the #user_not_logged_in method on the listener' do listener.should_receive(:user_not_logged_in).with(no_args) - processor.process(params, cookies, user_agent) + processor.process end end end diff --git a/spec/processor/two_factor_authenticator_overview_spec.rb b/spec/processor/two_factor_authenticator_overview_spec.rb index 8657f0d4..caa5487f 100644 --- a/spec/processor/two_factor_authenticator_overview_spec.rb +++ b/spec/processor/two_factor_authenticator_overview_spec.rb @@ -2,25 +2,18 @@ describe CASino::TwoFactorAuthenticatorOverviewProcessor do describe '#process' do - let(:listener) { Object.new } + let(:listener) { double('listener', assigned:user) } let(:processor) { described_class.new(listener) } - let(:cookies) { { tgt: tgt } } - before(:each) do - listener.stub(:user_not_logged_in) - listener.stub(:two_factor_authenticators_found) - end - - context 'with an existing ticket-granting ticket' do - let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket } - let(:user) { ticket_granting_ticket.user } - let(:tgt) { ticket_granting_ticket.ticket } + context 'with a signed in User' do + let(:user) { FactoryGirl.create :user } + let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket, user: user } let(:user_agent) { ticket_granting_ticket.user_agent } context 'without a two-factor authenticator registered' do it 'calls the #two_factor_authenticators_found method on the listener' do listener.should_receive(:two_factor_authenticators_found).with([]) - processor.process(cookies, user_agent) + processor.process(user, user_agent) end end @@ -29,7 +22,7 @@ it 'does not include the inactive authenticator' do listener.should_receive(:two_factor_authenticators_found).with([]) - processor.process(cookies, user_agent) + processor.process(user, user_agent) end end @@ -39,17 +32,19 @@ it 'calls the #two_factor_authenticators_found method on the listener' do listener.should_receive(:two_factor_authenticators_found).with([two_factor_authenticator]) - processor.process(cookies, user_agent) + processor.process(user, user_agent) end end end - context 'with an invalid ticket-granting ticket' do - let(:tgt) { 'TGT-lalala' } + context 'without a logged in user' do + let(:user) { nil } + let(:user_agent) { 'TestBrowser 1.0' } + it 'calls the #user_not_logged_in method on the listener' do listener.should_receive(:user_not_logged_in).with(no_args) - processor.process(cookies, user_agent) + processor.process(user, user_agent) end end end diff --git a/spec/processor/two_factor_authenticator_registrator_spec.rb b/spec/processor/two_factor_authenticator_registrator_spec.rb index 416ca4dd..cf5e7e64 100644 --- a/spec/processor/two_factor_authenticator_registrator_spec.rb +++ b/spec/processor/two_factor_authenticator_registrator_spec.rb @@ -2,24 +2,18 @@ describe CASino::TwoFactorAuthenticatorRegistratorProcessor do describe '#process' do - let(:listener) { Object.new } + let(:listener) { double('listener', assigned:user) } let(:processor) { described_class.new(listener) } - let(:cookies) { { tgt: tgt } } - - before(:each) do - listener.stub(:user_not_logged_in) - listener.stub(:two_factor_authenticator_registered) - end context 'with an existing ticket-granting ticket' do let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket } let(:user) { ticket_granting_ticket.user } - let(:tgt) { ticket_granting_ticket.ticket } let(:user_agent) { ticket_granting_ticket.user_agent } it 'creates exactly one authenticator' do + listener.stub(:two_factor_authenticator_registered) lambda do - processor.process(cookies, user_agent) + processor.process(user, user_agent) end.should change(CASino::TwoFactorAuthenticator, :count).by(1) end @@ -27,21 +21,21 @@ listener.should_receive(:two_factor_authenticator_registered) do |authenticator| authenticator.should == CASino::TwoFactorAuthenticator.last end - processor.process(cookies, user_agent) + processor.process(user, user_agent) end it 'creates an inactive two-factor authenticator' do - processor.process(cookies, user_agent) + listener.stub(:two_factor_authenticator_registered) + processor.process(user, user_agent) CASino::TwoFactorAuthenticator.last.should_not be_active end end - context 'with an invalid ticket-granting ticket' do - let(:tgt) { 'TGT-lalala' } - let(:user_agent) { 'TestBrowser 1.0' } + context 'without a logged in user' do + let(:user) { nil } it 'calls the #user_not_logged_in method on the listener' do listener.should_receive(:user_not_logged_in).with(no_args) - processor.process(cookies, user_agent) + processor.process end end end diff --git a/spec/support/factories/ticket_granting_ticket_factory.rb b/spec/support/factories/ticket_granting_ticket_factory.rb index 91f68db2..180062fb 100644 --- a/spec/support/factories/ticket_granting_ticket_factory.rb +++ b/spec/support/factories/ticket_granting_ticket_factory.rb @@ -11,5 +11,9 @@ trait :awaiting_two_factor_authentication do awaiting_two_factor_authentication true end + + trait :expired do + created_at { 25.hours.ago } + end end end