diff --git a/.travis.yml b/.travis.yml index e2f0ec2a..56778fa7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,14 @@ language: ruby rvm: - - 1.9.3 - - 2.0.0 - - 2.1.0 +- 1.9.3 +- 2.0.0 +- 2.1.0 +notifications: + hipchat: + rooms: + secure: kXPfZwOtdwJM0NIOj2td/NoPOhzxWVlUfHQuke2N4fuoKDQ+nhz5ZV4btW5J+O5C5aC6qyBBFdm+FzA/8m1WiLMGX0DIE1X67zZts/udMwtIDRNoHV594hd2co4oA72QMUT5kdre7IvTpSnnJwkp/d3V0kB7DOHuEbDJsjipx8I= + template: + - '%{repository} #%{build_number} (%{branch} - %{commit} : %{author}): %{message}' + format: html + on_failure: always + on_success: change diff --git a/app/api/casino/api.rb b/app/api/casino/api.rb new file mode 100644 index 00000000..4c08dbc2 --- /dev/null +++ b/app/api/casino/api.rb @@ -0,0 +1,7 @@ +require 'grape' + +class CASino::API < Grape::API + format :json + + mount CASino::API::Resource::AuthTokenTickets +end diff --git a/app/api/casino/api/entity/auth_token_ticket.rb b/app/api/casino/api/entity/auth_token_ticket.rb new file mode 100644 index 00000000..f5b180d0 --- /dev/null +++ b/app/api/casino/api/entity/auth_token_ticket.rb @@ -0,0 +1,5 @@ +require 'grape-entity' + +class CASino::API::Entity::AuthTokenTicket < Grape::Entity + expose :ticket +end diff --git a/app/api/casino/api/resource/auth_token_tickets.rb b/app/api/casino/api/resource/auth_token_tickets.rb new file mode 100644 index 00000000..518e649d --- /dev/null +++ b/app/api/casino/api/resource/auth_token_tickets.rb @@ -0,0 +1,12 @@ +require 'grape' + +class CASino::API::Resource::AuthTokenTickets < Grape::API + resource :auth_token_tickets do + desc 'Create an auth token ticket' + post do + @ticket = CASino::AuthTokenTicket.create + Rails.logger.debug "Created auth token ticket '#{@ticket.ticket}'" + present @ticket, with: CASino::API::Entity::AuthTokenTicket + end + end +end diff --git a/app/authenticators/casino/static_authenticator.rb b/app/authenticators/casino/static_authenticator.rb index a8c5e1c8..1ad8d1e4 100644 --- a/app/authenticators/casino/static_authenticator.rb +++ b/app/authenticators/casino/static_authenticator.rb @@ -12,12 +12,18 @@ def initialize(options) def validate(username, password) username = :"#{username}" if @users.include?(username) && @users[username][:password] == password + load_user_data(username) + else + false + end + end + + def load_user_data(username) + if @users.include?(username) { username: "#{username}", extra_attributes: @users[username].except(:password) } - else - false end end end diff --git a/app/builders/casino/proxy_response_builder.rb b/app/builders/casino/proxy_response_builder.rb new file mode 100644 index 00000000..22c938bc --- /dev/null +++ b/app/builders/casino/proxy_response_builder.rb @@ -0,0 +1,24 @@ +require 'builder' + +class CASino::ProxyResponseBuilder + attr_reader :success, :options + + def initialize(success, options) + @success = success + @options = options + end + + def build + xml = Builder::XmlMarkup.new(indent: 2) + xml.cas :serviceResponse, 'xmlns:cas' => 'http://www.yale.edu/tp/cas' do |service_response| + if success + service_response.cas :proxySuccess do |proxy_success| + proxy_success.cas :proxyTicket, options[:proxy_ticket].ticket + end + else + service_response.cas :proxyFailure, options[:error_message], code: options[:error_code] + end + end + xml.target! + end +end diff --git a/app/builders/casino/ticket_validation_response_builder.rb b/app/builders/casino/ticket_validation_response_builder.rb index b285bf9b..8eec20cc 100644 --- a/app/builders/casino/ticket_validation_response_builder.rb +++ b/app/builders/casino/ticket_validation_response_builder.rb @@ -1,6 +1,8 @@ require 'builder' class CASino::TicketValidationResponseBuilder + attr_reader :success, :options + def initialize(success, options) @success = success @options = options @@ -9,8 +11,8 @@ def initialize(success, options) def build xml = Builder::XmlMarkup.new(indent: 2) xml.cas :serviceResponse, 'xmlns:cas' => 'http://www.yale.edu/tp/cas' do |service_response| - if @success - ticket = @options[:ticket] + if success + ticket = options[:ticket] if ticket.is_a?(CASino::ProxyTicket) proxies = [] service_ticket = ticket @@ -38,6 +40,8 @@ def serialize_extra_attribute(builder, key, value) key = :"#{key}" if value.kind_of?(String) || value.kind_of?(Numeric) || value.kind_of?(Symbol) builder.cas key, "#{value}" + elsif value.kind_of?(Array) + value.each { |v| serialize_extra_attribute(builder, key, v) } else builder.cas key do |container| container.cdata! value.to_yaml @@ -65,8 +69,8 @@ def build_success_xml(service_response, ticket, service_ticket, ticket_granting_ end end end - if @options[:proxy_granting_ticket] - proxy_granting_ticket = @options[:proxy_granting_ticket] + if options[:proxy_granting_ticket] + proxy_granting_ticket = options[:proxy_granting_ticket] authentication_success.cas :proxyGrantingTicket, proxy_granting_ticket.iou end if ticket.is_a?(CASino::ProxyTicket) @@ -80,6 +84,6 @@ def build_success_xml(service_response, ticket, service_ticket, ticket_granting_ end def build_failure_xml(service_response) - service_response.cas :authenticationFailure, @options[:error_message], code: @options[:error_code] + service_response.cas :authenticationFailure, options[:error_message], code: options[:error_code] end end diff --git a/app/controllers/casino/api/v1/tickets_controller.rb b/app/controllers/casino/api/v1/tickets_controller.rb deleted file mode 100644 index eb7da551..00000000 --- a/app/controllers/casino/api/v1/tickets_controller.rb +++ /dev/null @@ -1,55 +0,0 @@ -class CASino::Api::V1::TicketsController < CASino::ApplicationController - - # POST /cas/v1/tickets - def create - CASino::API::LoginCredentialAcceptorProcessor.new(self).process(params, request.user_agent) - end - - # POST /cas/v1/tickets/{TGT id} - def update - CASino::API::ServiceTicketProviderProcessor.new(self).process(params[:id], params, request.user_agent) - end - - # DELETE /cas/v1/tickets/TGT-fdsjfsdfjkalfewrihfdhfaie - def destroy - CASino::API::LogoutProcessor.new(self).process(params[:id], request.user_agent) - end - - # callbacks - def user_logged_in_via_api(ticket_granting_ticket) - render nothing: true, status: 201, location: api_v1_ticket_url(ticket_granting_ticket) - end - - def invalid_login_credentials_via_api - error_response - end - - def granted_service_ticket_via_api(service_ticket) - render text: service_ticket, status: 200, content_type: Mime::TEXT - end - - def invalid_ticket_granting_ticket_via_api - error_response - end - - def no_service_provided_via_api - error_response - end - - def service_not_allowed_via_api - error_response - end - - def user_logged_out_via_api - render nothing: true, status: 200 - end - - private - def error_response - render nothing: true, status: 400 - end - -end - -# Inflector alias -CASino::API = CASino::Api diff --git a/app/controllers/casino/application_controller.rb b/app/controllers/casino/application_controller.rb index 62068252..151495cc 100644 --- a/app/controllers/casino/application_controller.rb +++ b/app/controllers/casino/application_controller.rb @@ -1,11 +1,9 @@ require 'casino' -require 'http_accept_language' class CASino::ApplicationController < ::ApplicationController include ApplicationHelper layout 'application' - before_filter :set_locale unless Rails.env.development? rescue_from ActionView::MissingTemplate, with: :missing_template @@ -16,26 +14,6 @@ def cookies 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 - - def extract_locale_from_accept_language_header - if request.env['HTTP_ACCEPT_LANGUAGE'] - http_accept_language.preferred_language_from(I18n.available_locales) - end - end - - def http_accept_language - HttpAcceptLanguage::Parser.new request.env['HTTP_ACCEPT_LANGUAGE'] - end - def missing_template(exception) render plain: 'Format not supported', status: :not_acceptable end diff --git a/app/controllers/casino/auth_tokens_controller.rb b/app/controllers/casino/auth_tokens_controller.rb new file mode 100644 index 00000000..076756ba --- /dev/null +++ b/app/controllers/casino/auth_tokens_controller.rb @@ -0,0 +1,34 @@ +class CASino::AuthTokensController < CASino::ApplicationController + include CASino::SessionsHelper + + def login + validation_result = validation_service.validation_result + return redirect_to_login unless validation_result + sign_in(validation_result) + end + + private + def validation_service + @validation_service ||= CASino::AuthTokenValidationService.new(auth_token, auth_token_signature) + end + + def redirect_to_login + redirect_to login_path(service: params[:service]) + end + + def auth_token_signature + @auth_token_signature ||= base64_decode(params[:ats]) + end + + def auth_token + @auth_token ||= base64_decode(params[:at]) + end + + def base64_decode(data) + begin + Base64.strict_decode64(data) + rescue + '' + end + end +end diff --git a/app/controllers/casino/controller_concern/ticket_validator.rb b/app/controllers/casino/controller_concern/ticket_validator.rb new file mode 100644 index 00000000..53bfbb55 --- /dev/null +++ b/app/controllers/casino/controller_concern/ticket_validator.rb @@ -0,0 +1,30 @@ +module CASino::ControllerConcern::TicketValidator + extend ActiveSupport::Concern + include CASino::ServiceTicketProcessor + include CASino::ProxyGrantingTicketProcessor + + def validate_ticket(ticket) + validation_result = validate_ticket_for_service(ticket, params[:service], renew: params[:renew]) + if validation_result.success? + options = { ticket: ticket } + options[:proxy_granting_ticket] = acquire_proxy_granting_ticket(params[:pgtUrl], ticket) unless params[:pgtUrl].nil? + build_ticket_validation_response(true, options) + else + build_ticket_validation_response(false, + error_code: validation_result.error_code, + error_message: validation_result.error_message) + end + end + + def build_ticket_validation_response(success, options = {}) + render xml: CASino::TicketValidationResponseBuilder.new(success, options).build + end + + def ensure_service_ticket_parameters_present + if params[:ticket].nil? || params[:service].nil? + build_ticket_validation_response(false, + error_code: 'INVALID_REQUEST', + error_message: '"ticket" and "service" parameters are both required') + end + end +end diff --git a/app/controllers/casino/proxy_tickets_controller.rb b/app/controllers/casino/proxy_tickets_controller.rb index 79769576..7e95134e 100644 --- a/app/controllers/casino/proxy_tickets_controller.rb +++ b/app/controllers/casino/proxy_tickets_controller.rb @@ -1,9 +1,49 @@ class CASino::ProxyTicketsController < CASino::ApplicationController + include CASino::ControllerConcern::TicketValidator + + before_action :load_ticket, only: [:proxy_validate] + before_action :ensure_service_ticket_parameters_present, only: [:proxy_validate] + + before_action :load_proxy_granting_ticket, only: [:create] + before_action :ensure_proxy_parameters_present, only: [:create] + def proxy_validate - processor(:ProxyTicketValidator, :TicketValidator).process(params) + validate_ticket(@ticket) end def create - processor(:ProxyTicketProvider).process(params) + proxy_ticket = @proxy_granting_ticket.proxy_tickets.create!(service: params[:targetService]) + build_proxy_response(true, proxy_ticket: proxy_ticket) + end + + private + def load_ticket + @ticket = case params[:ticket] + when /\APT-/ + CASino::ProxyTicket.where(ticket: params[:ticket]).first + when /\AST-/ + CASino::ServiceTicket.where(ticket: params[:ticket]).first + end + end + + def build_proxy_response(success, options = {}) + render xml: CASino::ProxyResponseBuilder.new(success, options).build + end + + def ensure_proxy_parameters_present + if params[:pgt].nil? || params[:targetService].nil? + build_proxy_response(false, + error_code: 'INVALID_REQUEST', + error_message: '"pgt" and "targetService" parameters are both required') + end + end + + def load_proxy_granting_ticket + @proxy_granting_ticket = CASino::ProxyGrantingTicket.where(ticket: params[:pgt]).first if params[:pgt].present? + if @proxy_granting_ticket.nil? + build_proxy_response(false, + error_code: 'BAD_PGT', + error_message: 'PGT not found') + end end end diff --git a/app/controllers/casino/service_tickets_controller.rb b/app/controllers/casino/service_tickets_controller.rb index b2be712d..84cd88f7 100644 --- a/app/controllers/casino/service_tickets_controller.rb +++ b/app/controllers/casino/service_tickets_controller.rb @@ -1,9 +1,22 @@ class CASino::ServiceTicketsController < CASino::ApplicationController + include CASino::ControllerConcern::TicketValidator + + before_action :load_service_ticket + before_action :ensure_service_ticket_parameters_present, only: [:service_validate] + def validate - processor(:LegacyValidator).process(params) + if ticket_valid_for_service?(@service_ticket, params[:service], renew: params[:renew]) + @username = @service_ticket.ticket_granting_ticket.user.username + end + render :validate, formats: [:text] end def service_validate - processor(:ServiceTicketValidator, :TicketValidator).process(params) + validate_ticket(@service_ticket) + end + + private + def load_service_ticket + @service_ticket = CASino::ServiceTicket.where(ticket: params[:ticket]).first if params[:service].present? end end diff --git a/app/controllers/casino/sessions_controller.rb b/app/controllers/casino/sessions_controller.rb index b5d774e0..321b702c 100644 --- a/app/controllers/casino/sessions_controller.rb +++ b/app/controllers/casino/sessions_controller.rb @@ -1,32 +1,83 @@ class CASino::SessionsController < CASino::ApplicationController include CASino::SessionsHelper + include CASino::AuthenticationProcessor + include CASino::TwoFactorAuthenticatorProcessor + + before_action :validate_login_ticket, only: [:create] + before_action :ensure_service_allowed, only: [:new, :create] + before_action :load_ticket_granting_ticket_from_parameter, only: [:validate_otp] + before_action :ensure_signed_in, only: [:index, :destroy] def index - processor(:TwoFactorAuthenticatorOverview).process(cookies, request.user_agent) - processor(:SessionOverview).process(cookies, request.user_agent) + @ticket_granting_tickets = current_user.ticket_granting_tickets.active + @two_factor_authenticators = current_user.two_factor_authenticators.active end def new - processor(:LoginCredentialRequestor).process(params, cookies, request.user_agent) + tgt = current_ticket_granting_ticket + handle_signed_in(tgt) unless params[:renew] || tgt.nil? + redirect_to(params[:service]) if params[:gateway] && params[:service].present? end def create - processor(:LoginCredentialAcceptor).process(params, request.user_agent) + validation_result = validate_login_credentials(params[:username], params[:password]) + if !validation_result + show_login_error I18n.t('login_credential_acceptor.invalid_login_credentials') + else + sign_in(validation_result, long_term: params[:rememberMe], credentials_supplied: true) + end end def destroy - processor(:SessionDestroyer).process(params, cookies, request.user_agent) + tickets = current_user.ticket_granting_tickets.where(id: params[:id]) + tickets.first.destroy if tickets.any? + redirect_to sessions_path end def destroy_others - processor(:OtherSessionsDestroyer).process(params, cookies, request.user_agent) + current_user + .ticket_granting_tickets + .where('id != ?', current_ticket_granting_ticket.id) + .destroy_all if signed_in? + redirect_to params[:service] || sessions_path end def logout - processor(:Logout).process(params, cookies, request.user_agent) + sign_out + @url = params[:url] + if params[:service].present? && service_allowed?(params[:service]) + redirect_to params[:service], status: :see_other + end end def validate_otp - processor(:SecondFactorAuthenticationAcceptor).process(params, request.user_agent) + validation_result = validate_one_time_password(params[:otp], @ticket_granting_ticket.user.active_two_factor_authenticator) + return flash.now[:error] = I18n.t('validate_otp.invalid_otp') unless validation_result.success? + @ticket_granting_ticket.update_attribute(:awaiting_two_factor_authentication, false) + set_tgt_cookie(@ticket_granting_ticket) + handle_signed_in(@ticket_granting_ticket) + end + + private + def show_login_error(message) + flash.now[:error] = message + render :new, status: :forbidden + end + + def validate_login_ticket + unless CASino::LoginTicket.consume(params[:lt]) + show_login_error I18n.t('login_credential_acceptor.invalid_login_ticket') + end + end + + def ensure_service_allowed + if params[:service].present? && !service_allowed?(params[:service]) + render 'service_not_allowed', status: :forbidden + end + end + + def load_ticket_granting_ticket_from_parameter + @ticket_granting_ticket = find_valid_ticket_granting_ticket(params[:tgt], request.user_agent, ignore_two_factor: true) + redirect_to login_path if @ticket_granting_ticket.nil? end end diff --git a/app/controllers/casino/two_factor_authenticators_controller.rb b/app/controllers/casino/two_factor_authenticators_controller.rb index 8d5271ff..83de2cf3 100644 --- a/app/controllers/casino/two_factor_authenticators_controller.rb +++ b/app/controllers/casino/two_factor_authenticators_controller.rb @@ -1,15 +1,40 @@ +require 'rotp' + class CASino::TwoFactorAuthenticatorsController < CASino::ApplicationController include CASino::SessionsHelper + include CASino::TwoFactorAuthenticatorsHelper + include CASino::TwoFactorAuthenticatorProcessor + + before_action :ensure_signed_in def new - processor(:TwoFactorAuthenticatorRegistrator).process(cookies, request.user_agent) + @two_factor_authenticator = current_user.two_factor_authenticators.create! secret: ROTP::Base32.random_base32 end def create - processor(:TwoFactorAuthenticatorActivator).process(params, cookies, request.user_agent) + @two_factor_authenticator = current_user.two_factor_authenticators.where(id: params[:id]).first + validation_result = validate_one_time_password(params[:otp], @two_factor_authenticator) + case + when validation_result.success? + current_user.two_factor_authenticators.where(active: true).delete_all + @two_factor_authenticator.update_attribute(:active, true) + flash[:notice] = I18n.t('two_factor_authenticators.successfully_activated') + redirect_to sessions_path + when validation_result.error_code == 'INVALID_OTP' + flash.now[:error] = I18n.t('two_factor_authenticators.invalid_one_time_password') + render :new + else + flash[:error] = I18n.t('two_factor_authenticators.invalid_two_factor_authenticator') + redirect_to new_two_factor_authenticator_path + end end def destroy - processor(:TwoFactorAuthenticatorDestroyer).process(params, cookies, request.user_agent) + authenticators = current_user.two_factor_authenticators.where(id: params[:id]) + if authenticators.any? + authenticators.first.destroy + flash[:notice] = I18n.t('two_factor_authenticators.successfully_deleted') + end + redirect_to sessions_path end end diff --git a/app/helpers/casino/sessions_helper.rb b/app/helpers/casino/sessions_helper.rb index fc91a05b..1f688846 100644 --- a/app/helpers/casino/sessions_helper.rb +++ b/app/helpers/casino/sessions_helper.rb @@ -1,5 +1,80 @@ +require 'addressable/uri' + module CASino::SessionsHelper + include CASino::TicketGrantingTicketProcessor + include CASino::ServiceTicketProcessor + def current_ticket_granting_ticket?(ticket_granting_ticket) ticket_granting_ticket.ticket == cookies[:tgt] end + + def current_ticket_granting_ticket + return nil unless cookies[:tgt] + return @current_ticket_granting_ticket unless @current_ticket_granting_ticket.nil? + find_valid_ticket_granting_ticket(cookies[:tgt], request.user_agent).tap do |tgt| + cookies.delete :tgt if tgt.nil? + @current_ticket_granting_ticket = tgt + end + end + + def current_user + tgt = current_ticket_granting_ticket + return nil if tgt.nil? + tgt.user + end + + def ensure_signed_in + redirect_to login_path unless signed_in? + end + + def signed_in? + !current_ticket_granting_ticket.nil? + end + + def sign_in(authentication_result, options = {}) + tgt = acquire_ticket_granting_ticket(authentication_result, request.user_agent, options) + set_tgt_cookie(tgt) + handle_signed_in(tgt, options) + end + + def set_tgt_cookie(tgt) + cookies[:tgt] = { value: tgt.ticket }.tap do |cookie| + if tgt.long_term? + cookie[:expires] = CASino.config.ticket_granting_ticket[:lifetime_long_term].seconds.from_now + end + end + end + + def sign_out + remove_ticket_granting_ticket(cookies[:tgt], request.user_agent) + cookies.delete :tgt + end + + private + def handle_signed_in(tgt, options = {}) + if tgt.awaiting_two_factor_authentication? + @ticket_granting_ticket = tgt + render 'casino/sessions/validate_otp' + else + if params[:service].present? + begin + handle_signed_in_with_service(tgt, options) + return + rescue Addressable::URI::InvalidURIError => e + Rails.logger.warn "Service #{params[:service]} not valid: #{e}" + end + end + redirect_to sessions_path, status: :see_other + end + end + + def handle_signed_in_with_service(tgt, options) + if !service_allowed?(params[:service]) + @service = params[:service] + render 'casino/sessions/service_not_allowed', status: 403 + else + url = acquire_service_ticket(tgt, params[:service], options).service_with_ticket_url + redirect_to url, status: :see_other + end + end end diff --git a/app/helpers/casino/two_factor_authenticators_helper.rb b/app/helpers/casino/two_factor_authenticators_helper.rb new file mode 100644 index 00000000..b4487f8d --- /dev/null +++ b/app/helpers/casino/two_factor_authenticators_helper.rb @@ -0,0 +1,12 @@ +require 'rqrcode_png' + +module CASino::TwoFactorAuthenticatorsHelper + def otp_auth_url(two_factor_authenticator) + "otpauth://totp/#{u CASino.config.frontend[:sso_name] + ': ' + two_factor_authenticator.user.username}?secret=#{two_factor_authenticator.secret}&issuer=#{u CASino.config.frontend[:sso_name]}" + end + + def otp_qr_code_data_url(two_factor_authenticator) + qr = RQRCode::QRCode.new(otp_auth_url(two_factor_authenticator), size: 5, level: :l) + qr.to_img.resize(250,250).to_data_url + end +end diff --git a/app/helpers/service_tickets_helper.rb b/app/helpers/service_tickets_helper.rb deleted file mode 100644 index 0cd8b3d7..00000000 --- a/app/helpers/service_tickets_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module ServiceTicketsHelper -end diff --git a/app/listeners/casino/legacy_validator_listener.rb b/app/listeners/casino/legacy_validator_listener.rb deleted file mode 100644 index 78eddf04..00000000 --- a/app/listeners/casino/legacy_validator_listener.rb +++ /dev/null @@ -1,11 +0,0 @@ -require_relative 'listener' - -class CASino::LegacyValidatorListener < CASino::Listener - def validation_failed(text) - @controller.render text: text, content_type: 'text/plain' - end - - def validation_succeeded(text) - @controller.render text: text, content_type: 'text/plain' - end -end diff --git a/app/listeners/casino/listener.rb b/app/listeners/casino/listener.rb deleted file mode 100644 index a05b7d1a..00000000 --- a/app/listeners/casino/listener.rb +++ /dev/null @@ -1,16 +0,0 @@ -module CASino - class Listener - - # include helpers to have the route path methods (like sessions_path) - include CASino::Engine.routes.url_helpers - - def initialize(controller) - @controller = controller - end - - protected - def assign(name, value) - @controller.instance_variable_set("@#{name}", value) - end - end -end diff --git a/app/listeners/casino/login_credential_acceptor_listener.rb b/app/listeners/casino/login_credential_acceptor_listener.rb deleted file mode 100644 index c5819a46..00000000 --- a/app/listeners/casino/login_credential_acceptor_listener.rb +++ /dev/null @@ -1,38 +0,0 @@ -require_relative 'listener' - -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 } - if url.nil? - @controller.redirect_to sessions_path, status: :see_other - else - @controller.redirect_to url, status: :see_other - end - end - - def two_factor_authentication_pending(ticket_granting_ticket) - assign(:ticket_granting_ticket, ticket_granting_ticket) - @controller.render 'validate_otp' - end - - def invalid_login_credentials(login_ticket) - @controller.flash.now[:error] = I18n.t('login_credential_acceptor.invalid_login_credentials') - rerender_login_page(login_ticket) - end - - def invalid_login_ticket(login_ticket) - @controller.flash.now[:error] = I18n.t('login_credential_acceptor.invalid_login_ticket') - rerender_login_page(login_ticket) - end - - def service_not_allowed(service) - assign(:service, service) - @controller.render 'service_not_allowed', status: 403 - end - - private - def rerender_login_page(login_ticket) - assign(:login_ticket, login_ticket) - @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 deleted file mode 100644 index 019e27c0..00000000 --- a/app/listeners/casino/login_credential_requestor_listener.rb +++ /dev/null @@ -1,21 +0,0 @@ -require_relative 'listener' - -class CASino::LoginCredentialRequestorListener < CASino::Listener - def user_not_logged_in(login_ticket) - assign(:login_ticket, login_ticket) - @controller.cookies.delete :tgt - end - - def service_not_allowed(service) - assign(:service, service) - @controller.render 'service_not_allowed', status: 403 - end - - def user_logged_in(url) - if url.nil? - @controller.redirect_to sessions_path - else - @controller.redirect_to url, status: :see_other - end - end -end diff --git a/app/listeners/casino/logout_listener.rb b/app/listeners/casino/logout_listener.rb deleted file mode 100644 index bc66fa90..00000000 --- a/app/listeners/casino/logout_listener.rb +++ /dev/null @@ -1,12 +0,0 @@ -require_relative 'listener' - -class CASino::LogoutListener < CASino::Listener - def user_logged_out(url, redirect_immediately = false) - if redirect_immediately - @controller.redirect_to url, status: :see_other - else - assign(:url, url) - end - @controller.cookies.delete :tgt - end -end diff --git a/app/listeners/casino/other_sessions_destroyer_listener.rb b/app/listeners/casino/other_sessions_destroyer_listener.rb deleted file mode 100644 index 05dfb19c..00000000 --- a/app/listeners/casino/other_sessions_destroyer_listener.rb +++ /dev/null @@ -1,7 +0,0 @@ -require_relative 'listener' - -class CASino::OtherSessionsDestroyerListener < CASino::Listener - def other_sessions_destroyed(url) - @controller.redirect_to(url || sessions_path) - end -end diff --git a/app/listeners/casino/proxy_ticket_provider_listener.rb b/app/listeners/casino/proxy_ticket_provider_listener.rb deleted file mode 100644 index 55fdb961..00000000 --- a/app/listeners/casino/proxy_ticket_provider_listener.rb +++ /dev/null @@ -1,11 +0,0 @@ -require_relative 'listener' - -class CASino::ProxyTicketProviderListener < CASino::Listener - def request_failed(xml) - @controller.render xml: xml - end - - def request_succeeded(xml) - @controller.render xml: xml - end -end diff --git a/app/listeners/casino/second_factor_authentication_acceptor_listener.rb b/app/listeners/casino/second_factor_authentication_acceptor_listener.rb deleted file mode 100644 index 0248c604..00000000 --- a/app/listeners/casino/second_factor_authentication_acceptor_listener.rb +++ /dev/null @@ -1,26 +0,0 @@ -require_relative 'listener' - -class CASino::SecondFactorAuthenticationAcceptorListener < CASino::Listener - - def user_not_logged_in - @controller.redirect_to login_path - end - - def user_logged_in(url, ticket_granting_ticket, cookie_expiry_time = nil) - @controller.cookies[:tgt] = { value: ticket_granting_ticket, expires: cookie_expiry_time } - if url.nil? - @controller.redirect_to sessions_path, status: :see_other - else - @controller.redirect_to url, status: :see_other - end - end - - def invalid_one_time_password - @controller.flash.now[:error] = I18n.t('validate_otp.invalid_otp') - end - - def service_not_allowed(service) - assign(:service, service) - @controller.render 'service_not_allowed', status: 403 - end -end diff --git a/app/listeners/casino/session_destroyer_listener.rb b/app/listeners/casino/session_destroyer_listener.rb deleted file mode 100644 index fa720ede..00000000 --- a/app/listeners/casino/session_destroyer_listener.rb +++ /dev/null @@ -1,11 +0,0 @@ -require_relative 'listener' - -class CASino::SessionDestroyerListener < CASino::Listener - def ticket_deleted - @controller.redirect_to(sessions_path) - end - - def ticket_not_found - @controller.redirect_to(sessions_path) - end -end diff --git a/app/listeners/casino/session_overview_listener.rb b/app/listeners/casino/session_overview_listener.rb deleted file mode 100644 index 180c01fb..00000000 --- a/app/listeners/casino/session_overview_listener.rb +++ /dev/null @@ -1,11 +0,0 @@ -require_relative 'listener' - -class CASino::SessionOverviewListener < CASino::Listener - def user_not_logged_in - @controller.redirect_to login_path - end - - def ticket_granting_tickets_found(ticket_granting_tickets) - assign(:ticket_granting_tickets, ticket_granting_tickets) - end -end diff --git a/app/listeners/casino/ticket_validator_listener.rb b/app/listeners/casino/ticket_validator_listener.rb deleted file mode 100644 index 2b790f73..00000000 --- a/app/listeners/casino/ticket_validator_listener.rb +++ /dev/null @@ -1,11 +0,0 @@ -require_relative 'listener' - -class CASino::TicketValidatorListener < CASino::Listener - def validation_failed(xml) - @controller.render xml: xml - end - - def validation_succeeded(xml) - @controller.render xml: xml - end -end diff --git a/app/listeners/casino/two_factor_authenticator_activator_listener.rb b/app/listeners/casino/two_factor_authenticator_activator_listener.rb deleted file mode 100644 index 821ee3c0..00000000 --- a/app/listeners/casino/two_factor_authenticator_activator_listener.rb +++ /dev/null @@ -1,23 +0,0 @@ -require_relative 'listener' - -class CASino::TwoFactorAuthenticatorActivatorListener < CASino::Listener - def user_not_logged_in - @controller.redirect_to login_path - end - - def two_factor_authenticator_activated - @controller.flash[:notice] = I18n.t('two_factor_authenticators.successfully_activated') - @controller.redirect_to sessions_path - end - - def invalid_one_time_password(two_factor_authenticator) - @controller.flash.now[:error] = I18n.t('two_factor_authenticators.invalid_one_time_password') - assign(:two_factor_authenticator, two_factor_authenticator) - @controller.render 'new' - end - - def invalid_two_factor_authenticator - @controller.flash[:error] = I18n.t('two_factor_authenticators.invalid_two_factor_authenticator') - @controller.redirect_to new_two_factor_authenticator_path - end -end diff --git a/app/listeners/casino/two_factor_authenticator_destroyer_listener.rb b/app/listeners/casino/two_factor_authenticator_destroyer_listener.rb deleted file mode 100644 index 6e7d85c3..00000000 --- a/app/listeners/casino/two_factor_authenticator_destroyer_listener.rb +++ /dev/null @@ -1,16 +0,0 @@ -require_relative 'listener' - -class CASino::TwoFactorAuthenticatorDestroyerListener < CASino::Listener - def user_not_logged_in - @controller.redirect_to login_path - end - - def two_factor_authenticator_destroyed - @controller.flash[:notice] = I18n.t('two_factor_authenticators.successfully_deleted') - @controller.redirect_to sessions_path - end - - def invalid_two_factor_authenticator - @controller.redirect_to sessions_path - end -end diff --git a/app/listeners/casino/two_factor_authenticator_overview_listener.rb b/app/listeners/casino/two_factor_authenticator_overview_listener.rb deleted file mode 100644 index 7fc0dbcb..00000000 --- a/app/listeners/casino/two_factor_authenticator_overview_listener.rb +++ /dev/null @@ -1,11 +0,0 @@ -require_relative 'listener' - -class CASino::TwoFactorAuthenticatorOverviewListener < CASino::Listener - def user_not_logged_in - # nothing to do here - end - - def two_factor_authenticators_found(two_factor_authenticators) - assign(:two_factor_authenticators, two_factor_authenticators) - end -end diff --git a/app/listeners/casino/two_factor_authenticator_registrator_listener.rb b/app/listeners/casino/two_factor_authenticator_registrator_listener.rb deleted file mode 100644 index a9c31ccb..00000000 --- a/app/listeners/casino/two_factor_authenticator_registrator_listener.rb +++ /dev/null @@ -1,11 +0,0 @@ -require_relative 'listener' - -class CASino::TwoFactorAuthenticatorRegistratorListener < CASino::Listener - def user_not_logged_in - @controller.redirect_to login_path - end - - def two_factor_authenticator_registered(two_factor_authenticator) - assign(:two_factor_authenticator, two_factor_authenticator) - end -end diff --git a/app/models/casino/auth_token_ticket.rb b/app/models/casino/auth_token_ticket.rb new file mode 100644 index 00000000..dce6f077 --- /dev/null +++ b/app/models/casino/auth_token_ticket.rb @@ -0,0 +1,15 @@ +class CASino::AuthTokenTicket < ActiveRecord::Base + include CASino::ModelConcern::Ticket + include CASino::ModelConcern::ConsumableTicket + + self.ticket_prefix = 'ATT'.freeze + + def self.cleanup + delete_all(['created_at < ?', CASino.config.auth_token_ticket[:lifetime].seconds.ago]) + end + + def expired? + (Time.now - (self.created_at || Time.now)) > CASino.config.auth_token_ticket[:lifetime].seconds + end + +end diff --git a/app/models/casino/login_ticket.rb b/app/models/casino/login_ticket.rb index bbad1487..afc789ac 100644 --- a/app/models/casino/login_ticket.rb +++ b/app/models/casino/login_ticket.rb @@ -1,11 +1,14 @@ class CASino::LoginTicket < ActiveRecord::Base - validates :ticket, uniqueness: true + include CASino::ModelConcern::Ticket + include CASino::ModelConcern::ConsumableTicket + + self.ticket_prefix = 'LT'.freeze def self.cleanup - self.delete_all(['created_at < ?', CASino.config.login_ticket[:lifetime].seconds.ago]) + delete_all(['created_at < ?', CASino.config.login_ticket[:lifetime].seconds.ago]) end - def to_s - self.ticket + def expired? + (Time.now - (self.created_at || Time.now)) > CASino.config.login_ticket[:lifetime].seconds end end diff --git a/app/models/casino/model_concern/consumable_ticket.rb b/app/models/casino/model_concern/consumable_ticket.rb new file mode 100644 index 00000000..93271b3f --- /dev/null +++ b/app/models/casino/model_concern/consumable_ticket.rb @@ -0,0 +1,20 @@ +module CASino::ModelConcern::ConsumableTicket + extend ActiveSupport::Concern + + module ClassMethods + def consume(ticket_identifier) + ticket = find_by_ticket(ticket_identifier) + if ticket.nil? + Rails.logger.info "#{model_name.human} '#{ticket_identifier}' not found" + false + elsif ticket.expired? + Rails.logger.info "#{model_name.human} '#{ticket.ticket}' expired" + false + else + Rails.logger.debug "#{model_name.human} '#{ticket.ticket}' successfully validated" + ticket.delete + true + end + end + end +end diff --git a/app/models/casino/model_concern/ticket.rb b/app/models/casino/model_concern/ticket.rb new file mode 100644 index 00000000..056df03a --- /dev/null +++ b/app/models/casino/model_concern/ticket.rb @@ -0,0 +1,28 @@ +module CASino::ModelConcern::Ticket + extend ActiveSupport::Concern + + included do + before_validation :ensure_ticket_present + validates :ticket, uniqueness: true + class_attribute :ticket_prefix + end + + def to_s + ticket + end + + private + TICKET_ALLOWED_CHARACTERS = ('A'..'Z').to_a + ('a'..'z').to_a + ('0'..'9').to_a + TICKET_LENGTH = 40 + + def ensure_ticket_present + self.ticket ||= create_random_ticket_string(self.class.ticket_prefix) + end + + def create_random_ticket_string(prefix) + random_string = SecureRandom.random_bytes(TICKET_LENGTH).each_char.map do |char| + TICKET_ALLOWED_CHARACTERS[(char.ord % TICKET_ALLOWED_CHARACTERS.length)] + end.join + "#{prefix}-#{'%d' % (Time.now.to_f * 10000)}-#{random_string}" + end +end diff --git a/app/models/casino/proxy_granting_ticket.rb b/app/models/casino/proxy_granting_ticket.rb index e3cfff70..07dece3d 100644 --- a/app/models/casino/proxy_granting_ticket.rb +++ b/app/models/casino/proxy_granting_ticket.rb @@ -1,7 +1,19 @@ class CASino::ProxyGrantingTicket < ActiveRecord::Base + include CASino::ModelConcern::Ticket + + self.ticket_prefix = 'PGT'.freeze + + before_validation :ensure_iou_present + validates :ticket, uniqueness: true validates :iou, uniqueness: true + belongs_to :granter, polymorphic: true has_many :proxy_tickets, dependent: :destroy + + private + def ensure_iou_present + self.iou ||= create_random_ticket_string('PGTIOU') + end end diff --git a/app/models/casino/proxy_ticket.rb b/app/models/casino/proxy_ticket.rb index faab95f6..e64b358a 100644 --- a/app/models/casino/proxy_ticket.rb +++ b/app/models/casino/proxy_ticket.rb @@ -1,6 +1,10 @@ require 'addressable/uri' class CASino::ProxyTicket < ActiveRecord::Base + include CASino::ModelConcern::Ticket + + self.ticket_prefix = 'PT'.freeze + validates :ticket, uniqueness: true belongs_to :proxy_granting_ticket has_many :proxy_granting_tickets, as: :granter, dependent: :destroy diff --git a/app/models/casino/service_ticket.rb b/app/models/casino/service_ticket.rb index eaf0f76e..6e56fa4f 100644 --- a/app/models/casino/service_ticket.rb +++ b/app/models/casino/service_ticket.rb @@ -1,7 +1,10 @@ require 'addressable/uri' class CASino::ServiceTicket < ActiveRecord::Base - validates :ticket, uniqueness: true + include CASino::ModelConcern::Ticket + + self.ticket_prefix = 'ST'.freeze + belongs_to :ticket_granting_ticket before_destroy :send_single_sign_out_notification, if: :consumed? has_many :proxy_granting_tickets, as: :granter, dependent: :destroy @@ -18,13 +21,11 @@ def self.cleanup_consumed_hard self.delete_all(['created_at < ? AND consumed = ?', (CASino.config.service_ticket[:lifetime_consumed] * 2).seconds.ago, true]) end - def service=(service) normalized_encoded_service = Addressable::URI.parse(service).normalize.to_str super(normalized_encoded_service) end - def service_with_ticket_url service_uri = Addressable::URI.parse(self.service) service_uri.query_values = (service_uri.query_values(Array) || []) << ['ticket', self.ticket] diff --git a/app/models/casino/ticket_granting_ticket.rb b/app/models/casino/ticket_granting_ticket.rb index eb74743a..1cab2942 100644 --- a/app/models/casino/ticket_granting_ticket.rb +++ b/app/models/casino/ticket_granting_ticket.rb @@ -1,11 +1,15 @@ require 'user_agent' class CASino::TicketGrantingTicket < ActiveRecord::Base - validates :ticket, uniqueness: true + include CASino::ModelConcern::Ticket + + self.ticket_prefix = 'TGC'.freeze belongs_to :user has_many :service_tickets, dependent: :destroy + scope :active, -> { where(awaiting_two_factor_authentication: false).order('updated_at DESC') } + def self.cleanup(user = nil) if user.nil? base = self diff --git a/app/models/casino/two_factor_authenticator.rb b/app/models/casino/two_factor_authenticator.rb index bb759f7e..ed93c604 100644 --- a/app/models/casino/two_factor_authenticator.rb +++ b/app/models/casino/two_factor_authenticator.rb @@ -2,6 +2,8 @@ class CASino::TwoFactorAuthenticator < ActiveRecord::Base belongs_to :user + scope :active, -> { where(active: true) } + def self.cleanup self.delete_all(['(created_at < ?) AND active = ?', self.lifetime.ago, false]) end diff --git a/app/processors/casino/api/login_credential_acceptor_processor.rb b/app/processors/casino/api/login_credential_acceptor_processor.rb deleted file mode 100644 index 38bde3be..00000000 --- a/app/processors/casino/api/login_credential_acceptor_processor.rb +++ /dev/null @@ -1,46 +0,0 @@ -# This processor should be used for API calls: POST /cas/v1/tickets -class CASino::API::LoginCredentialAcceptorProcessor < CASino::Processor - include CASino::ProcessorConcern::ServiceTickets - include CASino::ProcessorConcern::Authentication - include CASino::ProcessorConcern::TicketGrantingTickets - - # Use this method to process the request. It expects the username in the parameter "username" and the password - # in "password". - # - # The method will call one of the following methods on the listener: - # * `#user_logged_in_via_api`: First and only argument is a String with the TGT-id - # * `#invalid_login_credentials_via_api`: No argument - # - # @param [Hash] login_data parameters supplied by user (username and password) - def process(login_data, user_agent = nil) - @login_data = login_data - @user_agent = user_agent - - validate_login_data - - unless @authentication_result.nil? - generate_ticket_granting_ticket - callback_user_logged_in - else - callback_invalid_login_credentials - end - end - - private - def validate_login_data - @authentication_result = validate_login_credentials(@login_data[:username], @login_data[:password]) - end - - def callback_user_logged_in - @listener.user_logged_in_via_api @ticket_granting_ticket.ticket - end - - def generate_ticket_granting_ticket - @ticket_granting_ticket = acquire_ticket_granting_ticket(@authentication_result, @user_agent) - end - - def callback_invalid_login_credentials - @listener.invalid_login_credentials_via_api - end - -end diff --git a/app/processors/casino/api/logout_processor.rb b/app/processors/casino/api/logout_processor.rb deleted file mode 100644 index 435b833d..00000000 --- a/app/processors/casino/api/logout_processor.rb +++ /dev/null @@ -1,17 +0,0 @@ -# The Logout processor should be used to process API DELETE requests to /cas/v1/tickets/ -class CASino::API::LogoutProcessor < CASino::Processor - include CASino::ProcessorConcern::TicketGrantingTickets - - # This method will call `#user_logged_out_via_api` on the listener. - # - # @param [String] ticket_granting_ticket Ticket-granting ticket to logout - def process(ticket_granting_ticket, user_agent = nil) - remove_ticket_granting_ticket(ticket_granting_ticket, user_agent) - callback_user_logged_out - end - - def callback_user_logged_out - @listener.user_logged_out_via_api - end - -end diff --git a/app/processors/casino/api/service_ticket_provider_processor.rb b/app/processors/casino/api/service_ticket_provider_processor.rb deleted file mode 100644 index bff5466d..00000000 --- a/app/processors/casino/api/service_ticket_provider_processor.rb +++ /dev/null @@ -1,69 +0,0 @@ -# The ServiceTicketProvider processor should be used to handle API calls: POST requests to /cas/v1/tickets/ -class CASino::API::ServiceTicketProviderProcessor < CASino::Processor - include CASino::ProcessorConcern::ServiceTickets - include CASino::ProcessorConcern::TicketGrantingTickets - - # Use this method to process the request. - # - # The method will call one of the following methods on the listener: - # * `#granted_service_ticket_via_api`: First and only argument is a String with the service ticket. - # The service ticket (and nothing else) should be displayed. - # * `#invalid_ticket_granting_ticket_via_api`: No argument. The application should respond with status "400 Bad Request" - # * `#no_service_provided_via_api`: No argument. The application should respond with status "400 Bad Request" - # * `#service_not_allowed_via_api`: The user tried to access a service that this CAS server is not allowed to serve. - # - # @param [String] ticket_granting_ticket ticket_granting_ticket supplied by the user in the URL - # @param [Hash] parameters parameters supplied by user (`service` in particular) - # @param [String] user_agent user-agent delivered by the client - def process(ticket_granting_ticket, parameters = nil, user_agent = nil) - parameters ||= {} - @client_ticket_granting_ticket = ticket_granting_ticket - @service_url = parameters[:service] - @user_agent = user_agent - - fetch_valid_ticket_granting_ticket - handle_ticket_granting_ticket - end - - private - def fetch_valid_ticket_granting_ticket - @ticket_granting_ticket = find_valid_ticket_granting_ticket(@client_ticket_granting_ticket, @user_agent) - end - - def handle_ticket_granting_ticket - case - when (@service_url and @ticket_granting_ticket) - begin - create_service_ticket - callback_granted_service_ticket - rescue ServiceNotAllowedError - callback_service_not_allowed - end - when (@service_url and not @ticket_granting_ticket) - callback_invalid_tgt - when (not @service_url and @ticket_granting_ticket) - callback_empty_service - end - end - - def create_service_ticket - @service_ticket = acquire_service_ticket(@ticket_granting_ticket, @service_url) - end - - def callback_granted_service_ticket - @listener.granted_service_ticket_via_api @service_ticket.ticket - end - - def callback_invalid_tgt - @listener.invalid_ticket_granting_ticket_via_api - end - - def callback_empty_service - @listener.no_service_provided_via_api - end - - def callback_service_not_allowed - @listener.service_not_allowed_via_api(clean_service_url @service_url) - end - -end diff --git a/app/processors/casino/authentication_processor.rb b/app/processors/casino/authentication_processor.rb new file mode 100644 index 00000000..8234094d --- /dev/null +++ b/app/processors/casino/authentication_processor.rb @@ -0,0 +1,73 @@ +module CASino::AuthenticationProcessor + extend ActiveSupport::Concern + + def validate_login_credentials(username, password) + authentication_result = nil + authenticators.each do |authenticator_name, authenticator| + begin + data = authenticator.validate(username, password) + rescue CASino::Authenticator::AuthenticatorError => e + Rails.logger.error "Authenticator '#{authenticator_name}' (#{authenticator.class}) raised an error: #{e}" + end + if data + authentication_result = { authenticator: authenticator_name, user_data: data } + Rails.logger.info("Credentials for username '#{data[:username]}' successfully validated using authenticator '#{authenticator_name}' (#{authenticator.class})") + break + end + end + authentication_result + end + + def load_user_data(authenticator_name, username) + authenticator = authenticators[authenticator_name] + return nil if authenticator.nil? + return nil unless authenticator.respond_to?(:load_user_data) + authenticator.load_user_data(username) + end + + def authenticators + @authenticators ||= {}.tap do |authenticators| + CASino.config[:authenticators].each do |name, auth| + next unless auth.is_a?(Hash) + + authenticator = if auth[:class] + auth[:class].constantize + else + load_authenticator(auth[:authenticator]) + end + + authenticators[name] = authenticator.new(auth[:options]) + end + end + end + + private + def load_authenticator(name) + gemname, classname = parse_name(name) + + begin + require gemname unless CASino.const_defined?(classname) + CASino.const_get(classname) + rescue LoadError => error + raise LoadError, load_error_message(name, gemname, error) + rescue NameError => error + raise NameError, name_error_message(name, error) + end + end + + def parse_name(name) + [ "casino-#{name.underscore}_authenticator", "#{name.camelize}Authenticator" ] + end + + def load_error_message(name, gemname, error) + "Failed to load authenticator '#{name}'. Maybe you have to include " \ + "\"gem '#{gemname}'\" in your Gemfile?\n" \ + " Error: #{error.message}\n" + end + + def name_error_message(name, error) + "Failed to load authenticator '#{name}'. The authenticator class must " \ + "be defined in the CASino namespace.\n" \ + " Error: #{error.message}\n" + end +end diff --git a/app/processors/casino/browser_processor.rb b/app/processors/casino/browser_processor.rb new file mode 100644 index 00000000..ed69e9a2 --- /dev/null +++ b/app/processors/casino/browser_processor.rb @@ -0,0 +1,12 @@ +module CASino::BrowserProcessor + extend ActiveSupport::Concern + + def browser_info(user_agent) + user_agent = UserAgent.parse(user_agent) + "#{user_agent.browser} (#{user_agent.platform})" + end + + def same_browser?(user_agent, other_user_agent) + user_agent == other_user_agent || browser_info(user_agent) == browser_info(other_user_agent) + end +end diff --git a/app/processors/casino/legacy_validator_processor.rb b/app/processors/casino/legacy_validator_processor.rb deleted file mode 100644 index c264a06a..00000000 --- a/app/processors/casino/legacy_validator_processor.rb +++ /dev/null @@ -1,19 +0,0 @@ -# The LegacyValidator processor should be used for GET requests to /validate -class CASino::LegacyValidatorProcessor < CASino::Processor - include CASino::ProcessorConcern::ServiceTickets - - # This method will call `#validation_succeeded` or `#validation_failed`. In both cases, it supplies - # a string as argument. The web application should present that string (and nothing else) to the - # requestor. - # - # @param [Hash] params parameters supplied by requestor (a service) - def process(params = nil) - params ||= {} - ticket = CASino::ServiceTicket.where(ticket: params[:ticket]).first - if !params[:service].nil? && ticket_valid_for_service?(ticket, params[:service], !!params[:renew]) - @listener.validation_succeeded("yes\n#{ticket.ticket_granting_ticket.user.username}\n") - else - @listener.validation_failed("no\n\n") - end - end -end diff --git a/app/processors/casino/login_credential_acceptor_processor.rb b/app/processors/casino/login_credential_acceptor_processor.rb deleted file mode 100644 index 3cd5164a..00000000 --- a/app/processors/casino/login_credential_acceptor_processor.rb +++ /dev/null @@ -1,63 +0,0 @@ -# This processor should be used for POST requests to /login -class CASino::LoginCredentialAcceptorProcessor < CASino::Processor - include CASino::ProcessorConcern::LoginTickets - include CASino::ProcessorConcern::ServiceTickets - include CASino::ProcessorConcern::Authentication - include CASino::ProcessorConcern::TicketGrantingTickets - - # Use this method to process the request. It expects the username in the parameter "username" and the password - # in "password". - # - # The method will call one of the following methods on the listener: - # * `#user_logged_in`: The first argument (String) is the URL (if any), the user should be redirected to. - # 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. - # 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 [String] user_agent user-agent delivered by the client - def process(params = nil, user_agent = nil) - @params = params || {} - @user_agent = user_agent - if login_ticket_valid?(@params[:lt]) - authenticate_user - else - @listener.invalid_login_ticket(acquire_login_ticket) - end - end - - private - def authenticate_user - authentication_result = validate_login_credentials(@params[:username], @params[:password]) - if !authentication_result.nil? - user_logged_in(authentication_result) - else - @listener.invalid_login_credentials(acquire_login_ticket) - end - end - - def user_logged_in(authentication_result) - long_term = @params[:rememberMe] - ticket_granting_ticket = acquire_ticket_granting_ticket(authentication_result, @user_agent, long_term) - if ticket_granting_ticket.awaiting_two_factor_authentication? - @listener.two_factor_authentication_pending(ticket_granting_ticket.ticket) - else - begin - url = unless @params[:service].blank? - acquire_service_ticket(ticket_granting_ticket, @params[:service], true).service_with_ticket_url - end - if long_term - @listener.user_logged_in(url, ticket_granting_ticket.ticket, CASino.config.ticket_granting_ticket[:lifetime_long_term].seconds.from_now) - else - @listener.user_logged_in(url, ticket_granting_ticket.ticket) - end - rescue ServiceNotAllowedError => e - @listener.service_not_allowed(clean_service_url @params[:service]) - end - end - end -end diff --git a/app/processors/casino/login_credential_requestor_processor.rb b/app/processors/casino/login_credential_requestor_processor.rb deleted file mode 100644 index cec31845..00000000 --- a/app/processors/casino/login_credential_requestor_processor.rb +++ /dev/null @@ -1,70 +0,0 @@ -# This processor should be used for GET requests to /login -class CASino::LoginCredentialRequestorProcessor < CASino::Processor - include CASino::ProcessorConcern::Browser - include CASino::ProcessorConcern::LoginTickets - include CASino::ProcessorConcern::ServiceTickets - include CASino::ProcessorConcern::TicketGrantingTickets - - # Use this method to process the request. - # - # The method will call one of the following methods on the listener: - # * `#user_logged_in`: The first argument (String) is the URL (if any), the user should be redirected to. - # * `#user_not_logged_in`: The first argument is a LoginTicket. It should be stored in a hidden field with name "lt". - # * `#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 [String] user_agent user-agent delivered by the client - def process(params = nil, cookies = nil, user_agent = nil) - @params = params || {} - @cookies = cookies || {} - @user_agent = user_agent || {} - begin - @service_url = clean_service_url(@params[:service]) unless @params[:service].nil? - rescue Addressable::URI::InvalidURIError => e - Rails.logger.warn "Service #{@params[:service]} not valid: #{e}" - end - if service_allowed? - handle_allowed_service - end - end - - private - def handle_allowed_service - if !@params[:renew] && (@ticket_granting_ticket = find_valid_ticket_granting_ticket(@cookies[:tgt], @user_agent)) - handle_logged_in - else - handle_not_logged_in - end - end - - def handle_logged_in - service_url_with_ticket = unless @service_url.nil? - acquire_service_ticket(@ticket_granting_ticket, @service_url, true).service_with_ticket_url - end - @listener.user_logged_in(service_url_with_ticket) - end - - def handle_not_logged_in - if gateway_request? - # we actually lie to the listener to simplify things - @listener.user_logged_in(@service_url) - else - login_ticket = acquire_login_ticket - @listener.user_not_logged_in(login_ticket) - end - end - - def service_allowed? - if @service_url.nil? || CASino::ServiceRule.allowed?(@service_url) - true - else - @listener.service_not_allowed(@service_url) - false - end - end - - def gateway_request? - @params[:gateway] == 'true' && @service_url - end -end diff --git a/app/processors/casino/logout_processor.rb b/app/processors/casino/logout_processor.rb deleted file mode 100644 index 6ac8a523..00000000 --- a/app/processors/casino/logout_processor.rb +++ /dev/null @@ -1,23 +0,0 @@ -# The Logout processor should be used to process GET requests to /logout. -class CASino::LogoutProcessor < CASino::Processor - include CASino::ProcessorConcern::TicketGrantingTickets - - # 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. - # For example, "The application you just logged out of has provided a link it would like you to follow. - # 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 [String] user_agent user-agent delivered by the client - def process(params = nil, cookies = nil, user_agent = nil) - params ||= {} - cookies ||= {} - remove_ticket_granting_ticket(cookies[:tgt], user_agent) - if params[:service] && CASino::ServiceRule.allowed?(params[:service]) - @listener.user_logged_out(params[:service], true) - else - @listener.user_logged_out(params[:url]) - end - end -end diff --git a/app/processors/casino/other_sessions_destroyer_processor.rb b/app/processors/casino/other_sessions_destroyer_processor.rb deleted file mode 100644 index 808b234e..00000000 --- a/app/processors/casino/other_sessions_destroyer_processor.rb +++ /dev/null @@ -1,26 +0,0 @@ -# The OtherSessionsDestroyer processor should be used to process GET requests to /destroy-other-sessions. -# -# It is usefule to redirect users to this action after a password change. -# -# 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 - - # 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 [String] user_agent user-agent delivered by the client - def process(params = nil, cookies = 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 - @listener.other_sessions_destroyed(params[:service]) - end -end diff --git a/app/processors/casino/processor.rb b/app/processors/casino/processor.rb deleted file mode 100644 index cd20196e..00000000 --- a/app/processors/casino/processor.rb +++ /dev/null @@ -1,5 +0,0 @@ -class CASino::Processor - def initialize(listener) - @listener = listener - end -end diff --git a/app/processors/casino/processor_concern/authentication.rb b/app/processors/casino/processor_concern/authentication.rb deleted file mode 100644 index 615b80a6..00000000 --- a/app/processors/casino/processor_concern/authentication.rb +++ /dev/null @@ -1,89 +0,0 @@ -require 'casino/authenticator' - -module CASino - module ProcessorConcern - module Authentication - - def validate_login_credentials(username, password) - authentication_result = nil - authenticators.each do |authenticator_name, authenticator| - begin - data = authenticator.validate(username, password) - rescue CASino::Authenticator::AuthenticatorError => e - Rails.logger.error "Authenticator '#{authenticator_name}' (#{authenticator.class}) raised an error: #{e}" - end - if data - authentication_result = { authenticator: authenticator_name, user_data: data } - Rails.logger.info("Credentials for username '#{data[:username]}' successfully validated using authenticator '#{authenticator_name}' (#{authenticator.class})") - break - end - end - authentication_result - end - - def authenticators - @authenticators ||= begin - CASino.config[:authenticators].each do |name, auth| - next unless auth.is_a?(Hash) - - authenticator = if auth[:class] - auth[:class].constantize - else - load_authenticator(auth[:authenticator]) - end - - CASino.config[:authenticators][name] = authenticator.new(auth[:options]) - end - end - end - - private - def load_legacy_authenticator(name) - gemname, classname = parse_legacy_name(name) - - begin - require gemname - CASinoCore::Authenticator.const_get("#{classname}") - rescue LoadError, NameError - false - end - end - - def load_authenticator(name) - legacy_authenticator = load_legacy_authenticator(name) - return legacy_authenticator if legacy_authenticator - - gemname, classname = parse_name(name) - - begin - require gemname - CASino.const_get(classname) - rescue LoadError => error - raise LoadError, load_error_message(name, gemname, error) - rescue NameError => error - raise NameError, name_error_message(name, error) - end - end - - def parse_name(name) - [ "casino-#{name.underscore}_authenticator", "#{name.camelize}Authenticator" ] - end - - def parse_legacy_name(name) - [ "casino_core-authenticator-#{name.underscore}", name.camelize ] - end - - def load_error_message(name, gemname, error) - "Failed to load authenticator '#{name}'. Maybe you have to include " \ - "\"gem '#{gemname}'\" in your Gemfile?\n" \ - " Error: #{error.message}\n" - end - - def name_error_message(name, error) - "Failed to load authenticator '#{name}'. The authenticator class must " \ - "be defined in the CASino namespace.\n" \ - " Error: #{error.message}\n" - end - end - end -end diff --git a/app/processors/casino/processor_concern/browser.rb b/app/processors/casino/processor_concern/browser.rb deleted file mode 100644 index 39ff834e..00000000 --- a/app/processors/casino/processor_concern/browser.rb +++ /dev/null @@ -1,14 +0,0 @@ -module CASino - module ProcessorConcern - module Browser - def browser_info(user_agent) - user_agent = UserAgent.parse(user_agent) - "#{user_agent.browser} (#{user_agent.platform})" - end - - def same_browser?(user_agent, other_user_agent) - user_agent == other_user_agent || browser_info(user_agent) == browser_info(other_user_agent) - end - end - end -end diff --git a/app/processors/casino/processor_concern/login_tickets.rb b/app/processors/casino/processor_concern/login_tickets.rb deleted file mode 100644 index 9b19771c..00000000 --- a/app/processors/casino/processor_concern/login_tickets.rb +++ /dev/null @@ -1,28 +0,0 @@ -module CASino - module ProcessorConcern - module LoginTickets - include CASino::ProcessorConcern::Tickets - - def acquire_login_ticket - ticket = CASino::LoginTicket.create ticket: random_ticket_string('LT') - Rails.logger.debug "Created login ticket '#{ticket.ticket}'" - ticket - end - - def login_ticket_valid?(lt) - ticket = CASino::LoginTicket.find_by_ticket lt - if ticket.nil? - Rails.logger.info "Login ticket '#{lt}' not found" - false - elsif ticket.created_at < CASino.config.login_ticket[:lifetime].seconds.ago - Rails.logger.info "Login ticket '#{ticket.ticket}' expired" - false - else - Rails.logger.debug "Login ticket '#{ticket.ticket}' successfully validated" - ticket.delete - true - end - end - end - end -end diff --git a/app/processors/casino/processor_concern/proxy_granting_tickets.rb b/app/processors/casino/processor_concern/proxy_granting_tickets.rb deleted file mode 100644 index c73ca546..00000000 --- a/app/processors/casino/processor_concern/proxy_granting_tickets.rb +++ /dev/null @@ -1,43 +0,0 @@ -require 'addressable/uri' -require 'faraday' - -module CASino - module ProcessorConcern - module ProxyGrantingTickets - include CASino::ProcessorConcern::Tickets - - def acquire_proxy_granting_ticket(pgt_url, service_ticket) - callback_uri = Addressable::URI.parse(pgt_url) - if callback_uri.scheme != 'https' - Rails.logger.warn "Proxy tickets can only be granted to callback servers using HTTPS." - nil - else - contact_callback_server(callback_uri, service_ticket) - end - end - - private - def contact_callback_server(callback_uri, service_ticket) - pgt = service_ticket.proxy_granting_tickets.new({ - ticket: random_ticket_string('PGT'), - iou: random_ticket_string('PGTIOU'), - pgt_url: "#{callback_uri}" - }) - callback_uri.query_values = (callback_uri.query_values || {}).merge(pgtId: pgt.ticket, pgtIou: pgt.iou) - response = Faraday.get "#{callback_uri}" - # TODO: does this follow redirects? CAS specification says that redirects MAY be followed (2.5.4) - if response.success? - pgt.save! - Rails.logger.debug "Proxy-granting ticket generated for service '#{service_ticket.service}': #{pgt.inspect}" - pgt - else - Rails.logger.warn "Proxy-granting ticket callback server responded with a bad result code '#{response.status}'. PGT will not be stored." - nil - end - rescue Faraday::Error::ClientError => error - Rails.logger.warn "Exception while communicating with proxy-granting ticket callback server: #{error.message}" - nil - end - end - end -end diff --git a/app/processors/casino/processor_concern/proxy_tickets.rb b/app/processors/casino/processor_concern/proxy_tickets.rb deleted file mode 100644 index 67362ec9..00000000 --- a/app/processors/casino/processor_concern/proxy_tickets.rb +++ /dev/null @@ -1,56 +0,0 @@ -module CASino - module ProcessorConcern - module ProxyTickets - - include CASino::ProcessorConcern::Tickets - - class ValidationResult < CASino::ValidationResult; end - - def acquire_proxy_ticket(proxy_granting_ticket, service) - proxy_granting_ticket.proxy_tickets.create!({ - ticket: random_ticket_string('PT'), - service: service, - }) - end - - def validate_ticket_for_service(ticket, service, renew = false) - if ticket.nil? - result = ValidationResult.new 'INVALID_TICKET', 'Invalid validate request: Ticket does not exist', :warn - else - result = validate_existing_ticket_for_service(ticket, service, renew) - ticket.consumed = true - ticket.save! - Rails.logger.debug "Consumed ticket '#{ticket.ticket}'" - end - if result.success? - Rails.logger.info "Ticket '#{ticket.ticket}' for service '#{service}' successfully validated" - else - Rails.logger.send(result.error_severity, result.error_message) - end - result - end - - def ticket_valid_for_service?(ticket, service, renew = false) - validate_ticket_for_service(ticket, service, renew).success? - end - - private - def validate_existing_ticket_for_service(ticket, service, renew = false) - if ticket.is_a?(CASino::ServiceTicket) - service = clean_service_url(service) - end - if ticket.consumed? - ValidationResult.new 'INVALID_TICKET', "Ticket '#{ticket.ticket}' already consumed", :warn - elsif ticket.expired? - ValidationResult.new 'INVALID_TICKET', "Ticket '#{ticket.ticket}' has expired", :warn - elsif service != ticket.service - ValidationResult.new 'INVALID_SERVICE', "Ticket '#{ticket.ticket}' is not valid for service '#{service}'", :warn - elsif renew && !ticket.issued_from_credentials? - ValidationResult.new 'INVALID_TICKET', "Ticket '#{ticket.ticket}' was not issued from credentials but service '#{service}' will only accept a renewed ticket", :info - else - ValidationResult.new - end - end - end - end -end diff --git a/app/processors/casino/processor_concern/service_tickets.rb b/app/processors/casino/processor_concern/service_tickets.rb deleted file mode 100644 index 2d35c636..00000000 --- a/app/processors/casino/processor_concern/service_tickets.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'addressable/uri' - -module CASino - module ProcessorConcern - module ServiceTickets - include CASino::ProcessorConcern::Tickets - include CASino::ProcessorConcern::ProxyTickets - - class ServiceNotAllowedError < StandardError; end - - RESERVED_CAS_PARAMETER_KEYS = ['service', 'ticket', 'gateway', 'renew'] - - def acquire_service_ticket(ticket_granting_ticket, service, credentials_supplied = nil) - service_url = clean_service_url(service) - unless CASino::ServiceRule.allowed?(service_url) - message = "#{service_url} is not in the list of allowed URLs" - Rails.logger.error message - raise ServiceNotAllowedError, message - end - service_tickets = ticket_granting_ticket.service_tickets - service_tickets.where(service: service_url).destroy_all - service_tickets.create!({ - ticket: random_ticket_string('ST'), - service: service_url, - issued_from_credentials: !!credentials_supplied - }) - end - - def clean_service_url(dirty_service) - return dirty_service if dirty_service.blank? - service_uri = Addressable::URI.parse dirty_service - unless service_uri.query_values.nil? - service_uri.query_values = service_uri.query_values(Array).select { |k,v| !RESERVED_CAS_PARAMETER_KEYS.include?(k) } - end - if service_uri.query_values.blank? - service_uri.query_values = nil - end - - service_uri.path = (service_uri.path || '').gsub(/\/+\z/, '') - service_uri.path = '/' if service_uri.path.blank? - - clean_service = service_uri.normalize.to_s - - Rails.logger.debug("Cleaned dirty service URL '#{dirty_service}' to '#{clean_service}'") if dirty_service != clean_service - - clean_service - 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 deleted file mode 100644 index 45ffbd07..00000000 --- a/app/processors/casino/processor_concern/ticket_granting_tickets.rb +++ /dev/null @@ -1,65 +0,0 @@ -require 'addressable/uri' - -module CASino - module ProcessorConcern - module TicketGrantingTickets - - include CASino::ProcessorConcern::Browser - - def find_valid_ticket_granting_ticket(tgt, user_agent, ignore_two_factor = false) - ticket_granting_ticket = CASino::TicketGrantingTicket.where(ticket: tgt).first - unless ticket_granting_ticket.nil? - if ticket_granting_ticket.expired? - Rails.logger.info "Ticket-granting ticket expired (Created: #{ticket_granting_ticket.created_at})" - ticket_granting_ticket.destroy - nil - elsif !ignore_two_factor && ticket_granting_ticket.awaiting_two_factor_authentication? - Rails.logger.info 'Ticket-granting ticket is valid, but two-factor authentication is pending' - nil - elsif same_browser?(ticket_granting_ticket.user_agent, user_agent) - ticket_granting_ticket.user_agent = user_agent - ticket_granting_ticket.touch - ticket_granting_ticket.save! - ticket_granting_ticket - else - Rails.logger.info 'User-Agent changed: ticket-granting ticket not valid for this browser' - nil - end - end - end - - def acquire_ticket_granting_ticket(authentication_result, user_agent = nil, long_term = nil) - user_data = authentication_result[:user_data] - user = load_or_initialize_user(authentication_result[:authenticator], user_data[:username], user_data[:extra_attributes]) - cleanup_expired_ticket_granting_tickets(user) - user.ticket_granting_tickets.create!({ - ticket: random_ticket_string('TGC'), - awaiting_two_factor_authentication: !user.active_two_factor_authenticator.nil?, - user_agent: user_agent, - long_term: !!long_term - }) - end - - def load_or_initialize_user(authenticator, username, extra_attributes) - user = CASino::User.where( - authenticator: authenticator, - username: username).first_or_initialize - user.extra_attributes = extra_attributes - user.save! - return user - 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 - end - end - - def cleanup_expired_ticket_granting_tickets(user) - CASino::TicketGrantingTicket.cleanup(user) - end - - end - end -end diff --git a/app/processors/casino/processor_concern/tickets.rb b/app/processors/casino/processor_concern/tickets.rb deleted file mode 100644 index 43708815..00000000 --- a/app/processors/casino/processor_concern/tickets.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'securerandom' - -module CASino - module ProcessorConcern - module Tickets - - ALLOWED_TICKET_STRING_CHARACTERS = ('A'..'Z').to_a + ('a'..'z').to_a + ('0'..'9').to_a - - def random_ticket_string(prefix, length = 40) - random_string = SecureRandom.random_bytes(length).each_char.map do |char| - ALLOWED_TICKET_STRING_CHARACTERS[(char.ord % ALLOWED_TICKET_STRING_CHARACTERS.length)] - end.join - "#{prefix}-#{'%d' % (Time.now.to_f * 10000)}-#{random_string}" - end - end - end -end diff --git a/app/processors/casino/processor_concern/two_factor_authenticators.rb b/app/processors/casino/processor_concern/two_factor_authenticators.rb deleted file mode 100644 index 96ff44f3..00000000 --- a/app/processors/casino/processor_concern/two_factor_authenticators.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'addressable/uri' -require 'rotp' - -module CASino - module ProcessorConcern - module TwoFactorAuthenticators - class ValidationResult < CASino::ValidationResult; end - - def validate_one_time_password(otp, authenticator) - if authenticator.nil? || authenticator.expired? - ValidationResult.new 'INVALID_AUTHENTICATOR', 'Authenticator does not exist or expired', :warn - else - totp = ROTP::TOTP.new(authenticator.secret) - if totp.verify_with_drift(otp, CASino.config.two_factor_authenticator[:drift]) - ValidationResult.new - else - ValidationResult.new 'INVALID_OTP', 'One-time password not valid', :warn - end - end - end - end - end -end diff --git a/app/processors/casino/proxy_granting_ticket_processor.rb b/app/processors/casino/proxy_granting_ticket_processor.rb new file mode 100644 index 00000000..9d8ef61f --- /dev/null +++ b/app/processors/casino/proxy_granting_ticket_processor.rb @@ -0,0 +1,37 @@ +require 'faraday' + +module CASino::ProxyGrantingTicketProcessor + extend ActiveSupport::Concern + + def acquire_proxy_granting_ticket(pgt_url, service_ticket) + callback_uri = Addressable::URI.parse(pgt_url) + if callback_uri.scheme != 'https' + Rails.logger.warn "Proxy tickets can only be granted to callback servers using HTTPS." + nil + else + contact_callback_server(callback_uri, service_ticket) + end + end + + private + def contact_callback_server(callback_uri, service_ticket) + pgt = service_ticket.proxy_granting_tickets.new({ + pgt_url: "#{callback_uri}" + }) + return unless pgt.valid? + callback_uri.query_values = (callback_uri.query_values || {}).merge(pgtId: pgt.ticket, pgtIou: pgt.iou) + response = Faraday.get "#{callback_uri}" + # TODO: does this follow redirects? CAS specification says that redirects MAY be followed (2.5.4) + if response.success? + pgt.save! + Rails.logger.debug "Proxy-granting ticket generated for service '#{service_ticket.service}': #{pgt.inspect}" + pgt + else + Rails.logger.warn "Proxy-granting ticket callback server responded with a bad result code '#{response.status}'. PGT will not be stored." + nil + end + rescue Faraday::Error::ClientError => error + Rails.logger.warn "Exception while communicating with proxy-granting ticket callback server: #{error.message}" + nil + end +end diff --git a/app/processors/casino/proxy_ticket_provider_processor.rb b/app/processors/casino/proxy_ticket_provider_processor.rb deleted file mode 100644 index 0c6aac8b..00000000 --- a/app/processors/casino/proxy_ticket_provider_processor.rb +++ /dev/null @@ -1,41 +0,0 @@ -require 'builder' - -# The ProxyTicketProvider processor should be used to handle GET requests to /proxy -class CASino::ProxyTicketProviderProcessor < CASino::Processor - include CASino::ProcessorConcern::ProxyGrantingTickets - include CASino::ProcessorConcern::ProxyTickets - - # This method will call `#request_succeeded` or `#request_failed`. In both cases, it supplies - # a string as argument. The web application should present that string (and nothing else) to the - # requestor. The Content-Type should be set to 'text/xml; charset=utf-8' - # - # @param [Hash] params parameters delivered by the client - def process(params = nil) - if params[:pgt].nil? || params[:targetService].nil? - @listener.request_failed build_xml false, error_code: 'INVALID_REQUEST', error_message: '"pgt" and "targetService" parameters are both required' - else - proxy_granting_ticket = CASino::ProxyGrantingTicket.where(ticket: params[:pgt]).first - if proxy_granting_ticket.nil? - @listener.request_failed build_xml false, error_code: 'BAD_PGT', error_message: 'PGT not found' - else - proxy_ticket = acquire_proxy_ticket(proxy_granting_ticket, params[:targetService]) - @listener.request_succeeded build_xml true, proxy_ticket: proxy_ticket - end - end - end - - private - def build_xml(success, options = {}) - xml = Builder::XmlMarkup.new(indent: 2) - xml.cas :serviceResponse, 'xmlns:cas' => 'http://www.yale.edu/tp/cas' do |service_response| - if success - service_response.cas :proxySuccess do |proxy_success| - proxy_success.cas :proxyTicket, options[:proxy_ticket].ticket - end - else - service_response.cas :proxyFailure, options[:error_message], code: options[:error_code] - end - end - xml.target! - end -end diff --git a/app/processors/casino/proxy_ticket_validator_processor.rb b/app/processors/casino/proxy_ticket_validator_processor.rb deleted file mode 100644 index 0920d239..00000000 --- a/app/processors/casino/proxy_ticket_validator_processor.rb +++ /dev/null @@ -1,22 +0,0 @@ -# The ProxyTicketValidator processor should be used to handle GET requests to /proxyValidate -class CASino::ProxyTicketValidatorProcessor < CASino::ServiceTicketValidatorProcessor - - # This method will call `#validation_succeeded` or `#validation_failed`. In both cases, it supplies - # a string as argument. The web application should present that string (and nothing else) to the - # requestor. The Content-Type should be set to 'text/xml; charset=utf-8' - # - # @param [Hash] params parameters delivered by the client - def process(params = nil) - params ||= {} - if request_valid?(params) - ticket = if params[:ticket].start_with?('PT-') - CASino::ProxyTicket.where(ticket: params[:ticket]).first - elsif params[:ticket].start_with?('ST-') - CASino::ServiceTicket.where(ticket: params[:ticket]).first - else - nil - end - validate_ticket!(ticket, params) - end - end -end diff --git a/app/processors/casino/second_factor_authentication_acceptor_processor.rb b/app/processors/casino/second_factor_authentication_acceptor_processor.rb deleted file mode 100644 index 9f0856be..00000000 --- a/app/processors/casino/second_factor_authentication_acceptor_processor.rb +++ /dev/null @@ -1,45 +0,0 @@ -# The SecondFactorAuthenticationAcceptor processor can be used to activate a previously generated ticket-granting ticket with pending two-factor authentication. -# -# This feature is not described in the CAS specification so it's completly optional -# to implement this on the web application side. -class CASino::SecondFactorAuthenticationAcceptorProcessor < CASino::Processor - include CASino::ProcessorConcern::ServiceTickets - include CASino::ProcessorConcern::TicketGrantingTickets - include CASino::ProcessorConcern::TwoFactorAuthenticators - - # The method will call one of the following methods on the listener: - # * `#user_not_logged_in`: The user should be redirected to /login. - # * `#user_logged_in`: The first argument (String) is the URL (if any), the user should be redirected to. - # The second argument (String) is the ticket-granting ticket. It should be stored in a cookie named "tgt". - # * `#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 [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) - if validation_result.success? - tgt.awaiting_two_factor_authentication = false - tgt.save! - 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]) - end - else - @listener.invalid_one_time_password - end - end - end -end diff --git a/app/processors/casino/service_ticket_processor.rb b/app/processors/casino/service_ticket_processor.rb new file mode 100644 index 00000000..94155e1d --- /dev/null +++ b/app/processors/casino/service_ticket_processor.rb @@ -0,0 +1,81 @@ +module CASino::ServiceTicketProcessor + extend ActiveSupport::Concern + + class ServiceNotAllowedError < StandardError; end + class ValidationResult < CASino::ValidationResult; end + + RESERVED_CAS_PARAMETER_KEYS = ['service', 'ticket', 'gateway', 'renew'] + + def service_allowed?(service) + CASino::ServiceRule.allowed?(service) + end + + def acquire_service_ticket(ticket_granting_ticket, service, options = {}) + service_url = clean_service_url(service) + unless service_allowed?(service_url) + message = "#{service_url} is not in the list of allowed URLs" + Rails.logger.error message + raise ServiceNotAllowedError, message + end + service_tickets = ticket_granting_ticket.service_tickets + service_tickets.where(service: service_url).destroy_all + service_tickets.create!({ + service: service_url, + issued_from_credentials: !!options[:credentials_supplied] + }) + end + + def clean_service_url(dirty_service) + return dirty_service if dirty_service.blank? + service_uri = Addressable::URI.parse dirty_service + unless service_uri.query_values.nil? + service_uri.query_values = service_uri.query_values(Array).select { |k,v| !RESERVED_CAS_PARAMETER_KEYS.include?(k) } + end + if service_uri.query_values.blank? + service_uri.query_values = nil + end + + service_uri.path = (service_uri.path || '').gsub(/\/+\z/, '') + service_uri.path = '/' if service_uri.path.blank? + + service_uri.normalize.to_s.tap do |clean_service| + Rails.logger.debug("Cleaned dirty service URL '#{dirty_service}' to '#{clean_service}'") if dirty_service != clean_service + end + end + + def ticket_valid_for_service?(ticket, service, options = {}) + validate_ticket_for_service(ticket, service, options).success? + end + + def validate_ticket_for_service(ticket, service, options = {}) + if ticket.nil? + result = ValidationResult.new 'INVALID_TICKET', 'Invalid validate request: Ticket does not exist', :warn + else + result = validate_existing_ticket_for_service(ticket, service, options) + ticket.update_attribute(:consumed, true) + Rails.logger.debug "Consumed ticket '#{ticket.ticket}'" + end + if result.success? + Rails.logger.info "Ticket '#{ticket.ticket}' for service '#{service}' successfully validated" + else + Rails.logger.send(result.error_severity, result.error_message) + end + result + end + + private + def validate_existing_ticket_for_service(ticket, service, options = {}) + service = clean_service_url(service) if ticket.is_a?(CASino::ServiceTicket) + if ticket.consumed? + ValidationResult.new 'INVALID_TICKET', "Ticket '#{ticket.ticket}' already consumed", :warn + elsif ticket.expired? + ValidationResult.new 'INVALID_TICKET', "Ticket '#{ticket.ticket}' has expired", :warn + elsif service != ticket.service + ValidationResult.new 'INVALID_SERVICE', "Ticket '#{ticket.ticket}' is not valid for service '#{service}'", :warn + elsif options[:renew] && !ticket.issued_from_credentials? + ValidationResult.new 'INVALID_TICKET', "Ticket '#{ticket.ticket}' was not issued from credentials but service '#{service}' will only accept a renewed ticket", :info + else + ValidationResult.new + end + end +end diff --git a/app/processors/casino/service_ticket_validator_processor.rb b/app/processors/casino/service_ticket_validator_processor.rb deleted file mode 100644 index 856a310f..00000000 --- a/app/processors/casino/service_ticket_validator_processor.rb +++ /dev/null @@ -1,46 +0,0 @@ -# The ServiceTicketValidator processor should be used to handle GET requests to /serviceValidate -class CASino::ServiceTicketValidatorProcessor < CASino::Processor - include CASino::ProcessorConcern::ServiceTickets - include CASino::ProcessorConcern::ProxyGrantingTickets - - # This method will call `#validation_succeeded` or `#validation_failed`. In both cases, it supplies - # a string as argument. The web application should present that string (and nothing else) to the - # requestor. The Content-Type should be set to 'text/xml; charset=utf-8' - # - # @param [Hash] params parameters delivered by the client - def process(params = nil) - params ||= {} - if request_valid?(params) - ticket = CASino::ServiceTicket.where(ticket: params[:ticket]).first - validate_ticket!(ticket, params) - end - end - - protected - def build_service_response(success, options = {}) - builder = CASino::TicketValidationResponseBuilder.new(success, options) - builder.build - end - - def request_valid?(params) - if params[:ticket].nil? || params[:service].nil? - @listener.validation_failed build_service_response(false, error_code: 'INVALID_REQUEST', error_message: '"ticket" and "service" parameters are both required') - false - else - true - end - end - - def validate_ticket!(ticket, params) - validation_result = validate_ticket_for_service(ticket, params[:service], !!params[:renew]) - if validation_result.success? - options = { ticket: ticket } - unless params[:pgtUrl].nil? - options[:proxy_granting_ticket] = acquire_proxy_granting_ticket(params[:pgtUrl], ticket) - end - @listener.validation_succeeded(build_service_response(true, options)) - else - @listener.validation_failed(build_service_response(false, error_code: validation_result.error_code, error_message: validation_result.error_message)) - end - end -end diff --git a/app/processors/casino/session_destroyer_processor.rb b/app/processors/casino/session_destroyer_processor.rb deleted file mode 100644 index 82ffe750..00000000 --- a/app/processors/casino/session_destroyer_processor.rb +++ /dev/null @@ -1,25 +0,0 @@ -# The SessionDestroyer processor is used to destroy a ticket-granting ticket. -# -# This feature is not described in the CAS specification so it's completly optional -# to implement this on the web application side. It is especially useful in -# combination with the {CASino::SessionOverviewProcessor} processor. -class CASino::SessionDestroyerProcessor < CASino::Processor - - # 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 [String] user_agent user-agent delivered by the client - def process(params = nil, cookies = nil, user_agent = nil) - params ||= {} - cookies ||= {} - 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 - Rails.logger.info "Destroying ticket-granting ticket '#{ticket.ticket}'" - ticket.destroy - @listener.ticket_deleted - end - end -end diff --git a/app/processors/casino/session_overview_processor.rb b/app/processors/casino/session_overview_processor.rb deleted file mode 100644 index b464c4ff..00000000 --- a/app/processors/casino/session_overview_processor.rb +++ /dev/null @@ -1,21 +0,0 @@ -# The SessionOverview processor to list all open session for the currently signed in user. -# -# 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 - - # 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 [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') - @listener.ticket_granting_tickets_found(ticket_granting_tickets) - end - end -end diff --git a/app/processors/casino/ticket_granting_ticket_processor.rb b/app/processors/casino/ticket_granting_ticket_processor.rb new file mode 100644 index 00000000..d63e82c8 --- /dev/null +++ b/app/processors/casino/ticket_granting_ticket_processor.rb @@ -0,0 +1,56 @@ +module CASino::TicketGrantingTicketProcessor + extend ActiveSupport::Concern + + include CASino::BrowserProcessor + + def find_valid_ticket_granting_ticket(ticket, user_agent, options = {}) + tgt = CASino::TicketGrantingTicket.where(ticket: ticket).first + unless tgt.nil? + if tgt.expired? + Rails.logger.info "Ticket-granting ticket expired (Created: #{tgt.created_at})" + tgt.destroy + nil + elsif !options[:ignore_two_factor] && tgt.awaiting_two_factor_authentication? + Rails.logger.info 'Ticket-granting ticket is valid, but two-factor authentication is pending' + nil + elsif same_browser?(tgt.user_agent, user_agent) + tgt.user_agent = user_agent + tgt.touch + tgt.save! + tgt + else + Rails.logger.info 'User-Agent changed: ticket-granting ticket not valid for this browser' + nil + end + end + end + + def acquire_ticket_granting_ticket(authentication_result, user_agent, options = {}) + user_data = authentication_result[:user_data] + user = load_or_initialize_user(authentication_result[:authenticator], user_data[:username], user_data[:extra_attributes]) + cleanup_expired_ticket_granting_tickets(user) + user.ticket_granting_tickets.create!({ + awaiting_two_factor_authentication: !user.active_two_factor_authenticator.nil?, + user_agent: user_agent, + long_term: !!options[:long_term] + }) + end + + def load_or_initialize_user(authenticator, username, extra_attributes) + user = CASino::User + .where(authenticator: authenticator, username: username) + .first_or_initialize + user.extra_attributes = extra_attributes + user.save! + return user + end + + def remove_ticket_granting_ticket(ticket_granting_ticket, user_agent) + tgt = find_valid_ticket_granting_ticket(ticket_granting_ticket, user_agent) + tgt.destroy unless tgt.nil? + end + + def cleanup_expired_ticket_granting_tickets(user) + CASino::TicketGrantingTicket.cleanup(user) + end +end diff --git a/app/processors/casino/two_factor_authenticator_activator_processor.rb b/app/processors/casino/two_factor_authenticator_activator_processor.rb deleted file mode 100644 index 8f38596b..00000000 --- a/app/processors/casino/two_factor_authenticator_activator_processor.rb +++ /dev/null @@ -1,41 +0,0 @@ -# The TwoFactorAuthenticatorActivator processor can be used to activate a previously generated two-factor authenticator. -# -# 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::TwoFactorAuthenticators - - # The method will call one of the following methods on the listener: - # * `#user_not_logged_in`: The user is not logged in and should be redirected to /login. - # * `#two_factor_authenticator_activated`: The two-factor authenticator was successfully activated. - # * `#invalid_two_factor_authenticator`: The two-factor authenticator is not valid. - # * `#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 [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) - if validation_result.success? - tgt.user.two_factor_authenticators.where(active: true).delete_all - authenticator.active = true - authenticator.save! - @listener.two_factor_authenticator_activated - else - if validation_result.error_code == 'INVALID_OTP' - @listener.invalid_one_time_password(authenticator) - else - @listener.invalid_two_factor_authenticator - end - end - end - end -end diff --git a/app/processors/casino/two_factor_authenticator_destroyer_processor.rb b/app/processors/casino/two_factor_authenticator_destroyer_processor.rb deleted file mode 100644 index 16396517..00000000 --- a/app/processors/casino/two_factor_authenticator_destroyer_processor.rb +++ /dev/null @@ -1,33 +0,0 @@ -# The TwoFactorAuthenticatorDestroyer processor can be used to deactivate a previously activated two-factor authenticator. -# -# 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::TwoFactorAuthenticators - - # The method will call one of the following methods on the listener: - # * `#user_not_logged_in`: The user is not logged in and should be redirected to /login. - # * `#two_factor_authenticator_destroyed`: The two-factor authenticator was successfully destroyed. - # * `#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 [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 - if authenticator - authenticator.destroy - @listener.two_factor_authenticator_destroyed - else - @listener.invalid_two_factor_authenticator - end - end - end -end diff --git a/app/processors/casino/two_factor_authenticator_overview_processor.rb b/app/processors/casino/two_factor_authenticator_overview_processor.rb deleted file mode 100644 index 42ffd6d8..00000000 --- a/app/processors/casino/two_factor_authenticator_overview_processor.rb +++ /dev/null @@ -1,20 +0,0 @@ -# The TwoFactorAuthenticatorOverview processor lists registered two factor devices for the currently signed in user. -# -# 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 - - # 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 [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 - @listener.two_factor_authenticators_found(tgt.user.two_factor_authenticators.where(active: true)) - end - end -end diff --git a/app/processors/casino/two_factor_authenticator_processor.rb b/app/processors/casino/two_factor_authenticator_processor.rb new file mode 100644 index 00000000..32785684 --- /dev/null +++ b/app/processors/casino/two_factor_authenticator_processor.rb @@ -0,0 +1,18 @@ +require 'rotp' + +module CASino::TwoFactorAuthenticatorProcessor + extend ActiveSupport::Concern + + def validate_one_time_password(otp, authenticator) + if authenticator.nil? || authenticator.expired? + CASino::ValidationResult.new 'INVALID_AUTHENTICATOR', 'Authenticator does not exist or expired', :warn + else + totp = ROTP::TOTP.new(authenticator.secret) + if totp.verify_with_drift(otp, CASino.config.two_factor_authenticator[:drift]) + CASino::ValidationResult.new + else + CASino::ValidationResult.new 'INVALID_OTP', 'One-time password not valid', :warn + end + end + end +end diff --git a/app/processors/casino/two_factor_authenticator_registrator_processor.rb b/app/processors/casino/two_factor_authenticator_registrator_processor.rb deleted file mode 100644 index b50e0a54..00000000 --- a/app/processors/casino/two_factor_authenticator_registrator_processor.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'rotp' - -# The TwoFactorAuthenticatorRegistrator processor can be used as the first step to register a new two-factor authenticator. -# It is inactive until activated using TwoFactorAuthenticatorActivator. -# -# 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 - - # 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 [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 - @listener.two_factor_authenticator_registered(two_factor_authenticator) - end - end -end diff --git a/app/services/casino/auth_token_validation_service.rb b/app/services/casino/auth_token_validation_service.rb new file mode 100644 index 00000000..4f9e4665 --- /dev/null +++ b/app/services/casino/auth_token_validation_service.rb @@ -0,0 +1,66 @@ +class CASino::AuthTokenValidationService + include CASino::AuthenticationProcessor + + AUTH_TOKEN_SIGNERS_GLOB = Rails.root.join('config/auth_token_signers/*.pem').freeze + + attr_reader :token, :signature + + def initialize(token, signature) + @token = token + @signature = signature + end + + def validation_result + return nil unless user_data + { authenticator: token_data[:authenticator], user_data: user_data } + end + + def user_data + return @user_data unless @user_data.nil? + return nil unless signature_valid? + return nil unless ticket_valid? + @user_data = load_user_data(token_data[:authenticator], token_data[:username]).tap do |user| + if user.nil? + Rails.logger.warn("Could not load user '#{token_data[:authenticator]}'/'#{token_data[:username]}'") + else + Rails.logger.info("User '#{token_data[:authenticator]}'/'#{token_data[:username]}' successfully identified through auth token.") + end + end + end + + def token_data + begin + JSON.parse(token).symbolize_keys + rescue JSON::ParserError + {} + end + end + + private + def signature_valid? + Dir.glob(AUTH_TOKEN_SIGNERS_GLOB) do |path| + if signature_valid_with_key?(path) + Rails.logger.info("Successfully validated auth token signature with #{File.basename(path)}") + return true + end + end + Rails.logger.warn('Signature could not be validated: No matching key found.') + false + end + + def signature_valid_with_key?(path) + digest = OpenSSL::Digest::SHA256.new + key = OpenSSL::PKey::RSA.new(File.read(path)) + key.verify(digest, signature, token) + end + + def ticket_valid? + CASino::AuthTokenTicket.consume(token_data[:ticket]).tap do |is_valid| + Rails.logger.warn('Could not find a valid auth token ticket.') unless is_valid + end + end + + def authentication_service + @authentication_service ||= CASino::AuthenticationService.new + end +end diff --git a/app/views/casino/sessions/index.html.erb b/app/views/casino/sessions/index.html.erb index 4f0bc6aa..dc861a4e 100644 --- a/app/views/casino/sessions/index.html.erb +++ b/app/views/casino/sessions/index.html.erb @@ -4,7 +4,7 @@

<%= t('sessions.title') %>

- <%= 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' %> @@ -18,7 +18,7 @@ <% if @two_factor_authenticators.blank? %> <%= t('two_factor_authenticators.disabled') %> - <%= link_to t('two_factor_authenticators.enable'), new_two_factor_authenticator_path %> <% else %> - <%= t('two_factor_authenticators.enabled') %> - <%= button_to t('two_factor_authenticators.disable'), two_factor_authenticator_path(@two_factor_authenticators[0].id), method: :delete %> + <%= t('two_factor_authenticators.enabled') %> - <%= button_to t('two_factor_authenticators.disable'), two_factor_authenticator_path(@two_factor_authenticators.first.id), method: :delete %> <% end %>

<%= t('sessions.your_active_sessions') %>

diff --git a/app/views/casino/sessions/new.html.erb b/app/views/casino/sessions/new.html.erb index 610d99ba..71050fc3 100644 --- a/app/views/casino/sessions/new.html.erb +++ b/app/views/casino/sessions/new.html.erb @@ -13,7 +13,7 @@
<%= form_tag(login_path, method: :post, id: 'login-form') do %> - <%= hidden_field_tag :lt, @login_ticket.ticket %> + <%= hidden_field_tag :lt, CASino::LoginTicket.create.ticket %> <%= hidden_field_tag :service, params[:service] unless params[:service].nil? %> <%= label_tag :username, t('login.label_username') %> <%= text_field_tag :username, params[:username], autofocus:true %> diff --git a/app/views/casino/sessions/validate_otp.html.erb b/app/views/casino/sessions/validate_otp.html.erb index c3122cfc..f075bf9f 100644 --- a/app/views/casino/sessions/validate_otp.html.erb +++ b/app/views/casino/sessions/validate_otp.html.erb @@ -8,7 +8,7 @@ <%= hidden_field_tag :tgt, @ticket_granting_ticket || params[:tgt] %> <%= hidden_field_tag :service, params[:service] %> <%= label_tag :code, t('validate_otp.code') %> - <%= text_field_tag :otp, nil, maxlength: 6, autofocus:true %> + <%= text_field_tag :otp, nil, maxlength: 6, autofocus: true, autocomplete: 'off' %> <%= button_tag t('validate_otp.submit'), :class => 'button' %> <% end %>
diff --git a/app/views/casino/two_factor_authenticators/new.html.erb b/app/views/casino/two_factor_authenticators/new.html.erb index 3bc79d27..07998255 100644 --- a/app/views/casino/two_factor_authenticators/new.html.erb +++ b/app/views/casino/two_factor_authenticators/new.html.erb @@ -11,10 +11,13 @@ <%= t('two_factor_authenticators.instructions') %>

- " height="250" width="250">
+

- <%= t('two_factor_authenticators.secret') %>: <%= @two_factor_authenticator.secret %> + <%= t('two_factor_authenticators.secret') %>: + + <%= @two_factor_authenticator.secret %> +

@@ -22,7 +25,7 @@ <%= form_tag(two_factor_authenticators_path, method: :post, id: 'two_factor_authenticators-form') do %> <%= hidden_field_tag :id, @two_factor_authenticator.id %> <%= label_tag :code, t('two_factor_authenticators.code') %> - <%= text_field_tag :otp, nil, maxlength: 6, autofocus:true %> + <%= text_field_tag :otp, nil, maxlength: 6, autofocus: true, autocomplete: 'off' %> <%= link_to t('two_factor_authenticators.cancel'), sessions_path, :class => 'secondary button' %> <%= button_tag t('two_factor_authenticators.submit'), :class => 'button' %> <% end %> diff --git a/casino.gemspec b/casino.gemspec index 9a6ccc21..4021670b 100644 --- a/casino.gemspec +++ b/casino.gemspec @@ -34,11 +34,13 @@ Gem::Specification.new do |s| s.add_development_dependency 'coveralls', '~> 0.7' s.add_runtime_dependency 'rails', '>= 4.1.0', '< 4.3.0' - s.add_runtime_dependency 'sass-rails', '~> 4.0.0' - s.add_runtime_dependency 'http_accept_language', '~> 2.0.0.pre' + s.add_runtime_dependency 'sass-rails', '>= 4.0.0', '< 6.0.0' s.add_runtime_dependency 'addressable', '~> 2.3' s.add_runtime_dependency 'terminal-table', '~> 1.4' s.add_runtime_dependency 'useragent', '~> 0.4' s.add_runtime_dependency 'faraday', '~> 0.8' s.add_runtime_dependency 'rotp', '~> 2.0' + s.add_runtime_dependency 'grape', '~> 0.8' + s.add_runtime_dependency 'grape-entity', '~> 0.4' + s.add_runtime_dependency 'rqrcode_png', '~> 0.1' end diff --git a/config/routes.rb b/config/routes.rb index 125959a3..0f819f41 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,6 @@ CASino::Engine.routes.draw do + mount CASino::API => '/api' + resources :sessions, only: [:index, :destroy] resources :two_factor_authenticators, only: [:new, :create, :destroy] @@ -15,16 +17,7 @@ get 'proxyValidate' => 'proxy_tickets#proxy_validate' get 'proxy' => 'proxy_tickets#create' - # api - scope '/cas' do - scope module: :api, as: :api do - namespace :v1 do - resources :tickets, only: [:create, :destroy] do - post :update, on: :member - end - end - end - end + get 'authTokenLogin' => 'auth_tokens#login' root to: redirect('login') diff --git a/db/migrate/20140831205255_create_auth_token_tickets.rb b/db/migrate/20140831205255_create_auth_token_tickets.rb new file mode 100644 index 00000000..5e6a0987 --- /dev/null +++ b/db/migrate/20140831205255_create_auth_token_tickets.rb @@ -0,0 +1,10 @@ +class CreateAuthTokenTickets < ActiveRecord::Migration + def change + create_table :casino_auth_token_tickets do |t| + t.string :ticket, :null => false + + t.timestamps + end + add_index :casino_auth_token_tickets, :ticket, :unique => true + end +end diff --git a/lib/casino.rb b/lib/casino.rb index f83857c4..7e59d284 100644 --- a/lib/casino.rb +++ b/lib/casino.rb @@ -21,6 +21,9 @@ module CASino two_factor_authenticator: nil, user: nil ), + auth_token_ticket: { + lifetime: 60 + }, login_ticket: { lifetime: 600 }, @@ -47,4 +50,4 @@ module CASino } self.config.merge! defaults.deep_dup -end \ No newline at end of file +end diff --git a/lib/casino/tasks/authentication.rake b/lib/casino/tasks/authentication.rake index b49efafa..13febaf1 100644 --- a/lib/casino/tasks/authentication.rake +++ b/lib/casino/tasks/authentication.rake @@ -4,7 +4,7 @@ namespace :casino do namespace :authentication do desc 'Test authentication.' task test: :environment do |task, args| - include CASino::ProcessorConcern::Authentication + include CASino::AuthenticationProcessor print "Username: " username = STDIN.gets.chomp print "Password (won't be shown): " diff --git a/lib/casino/tasks/cleanup.rake b/lib/casino/tasks/cleanup.rake index 6b0e528a..af9d6039 100644 --- a/lib/casino/tasks/cleanup.rake +++ b/lib/casino/tasks/cleanup.rake @@ -31,6 +31,12 @@ namespace :casino do puts "Deleted #{rows_affected} login tickets." end + desc 'Remove expired auth token tickets.' + task auth_token_tickets: :environment do + rows_affected = CASino::AuthTokenTicket.cleanup + puts "Deleted #{rows_affected} auth token tickets." + end + desc 'Remove expired inactive two-factor authenticators.' task two_factor_authenticators: :environment do rows_affected = CASino::TwoFactorAuthenticator.cleanup @@ -53,7 +59,13 @@ namespace :casino do end desc 'Perform all cleanup tasks.' - task all: [:acquire_lock, :ticket_granting_tickets, :service_tickets, :proxy_tickets, :login_tickets, :two_factor_authenticators] do + task all: [:acquire_lock, + :ticket_granting_tickets, + :service_tickets, + :proxy_tickets, + :auth_token_tickets, + :login_tickets, + :two_factor_authenticators] do end end end diff --git a/lib/casino/tasks/service_rule.rake b/lib/casino/tasks/service_rule.rake index 482d31f8..c554c5a6 100644 --- a/lib/casino/tasks/service_rule.rake +++ b/lib/casino/tasks/service_rule.rake @@ -5,7 +5,7 @@ namespace :casino do desc 'Add a service rule (prefix the url parameter with "regex:" to add a regular expression)' task :add, [:name, :url] => :environment do |task, args| - include CASino::ProcessorConcern::ServiceTickets + include CASino::ServiceTicketProcessor service_rule = CASino::ServiceRule.new name: args[:name] match = /^regex:(.*)/.match(args[:url]) if match.nil? diff --git a/lib/casino/version.rb b/lib/casino/version.rb index a980330b..572a730c 100644 --- a/lib/casino/version.rb +++ b/lib/casino/version.rb @@ -1,3 +1,3 @@ module CASino - VERSION = '3.0.4' + VERSION = '4.0.0.pre.2' end diff --git a/spec/controllers/api/v1/tickets_controller_spec.rb b/spec/controllers/api/v1/tickets_controller_spec.rb deleted file mode 100644 index a267f1f0..00000000 --- a/spec/controllers/api/v1/tickets_controller_spec.rb +++ /dev/null @@ -1,114 +0,0 @@ -require 'spec_helper' - -describe CASino::Api::V1::TicketsController do - - describe "POST /cas/v1/tickets" do - context "with correct credentials" do - - before do - CASino::API::LoginCredentialAcceptorProcessor.any_instance.should_receive(:process) do - @controller.user_logged_in_via_api "TGT-long-string" - end - - post :create, username: 'valid', password: 'valid', use_route: :casino - end - - subject { response } - its(:response_code) { should eq 201 } - its(:location) { should eq 'http://test.host/cas/v1/tickets/TGT-long-string' } - end - - context "with incorrect credentials" do - - before do - CASino::API::LoginCredentialAcceptorProcessor.any_instance.should_receive(:process) do - @controller.invalid_login_credentials_via_api - end - - post :create, username: 'invalid', password: 'invalid', use_route: :casino - end - - subject { response } - its(:response_code) { should eq 400 } - end - - context "with a not allowed service" do - - before do - CASino::API::LoginCredentialAcceptorProcessor.any_instance.should_receive(:process) do - @controller.service_not_allowed_via_api - end - - post :create, username: 'example', password: 'example', use_route: :casino - end - - subject { response } - its(:response_code) { should eq 400 } - end - end - - describe "POST /cas/v1/tickets/{TGT id}" do - - context "with a valid TGT" do - - before do - CASino::API::ServiceTicketProviderProcessor.any_instance.should_receive(:process).with('TGT-valid', kind_of(Hash), request.user_agent) do |ticket, params| - params.should == controller.params - @controller.granted_service_ticket_via_api 'ST-1-VALIDSERVICETICKET' - end - post :update, id: 'TGT-valid', service: 'http://example.org/', use_route: :casino - end - - subject { response } - - its(:response_code) { should eq 200 } - its(:body) { should eq 'ST-1-VALIDSERVICETICKET' } - end - - context "with an invalid TGT" do - - before do - CASino::API::ServiceTicketProviderProcessor.any_instance.should_receive(:process).with('TGT-invalid', kind_of(Hash), request.user_agent) do |ticket, params| - params.should == controller.params - @controller.invalid_ticket_granting_ticket_via_api - end - post :update, id: 'TGT-invalid', service: 'http://example.org/', use_route: :casino - end - - subject { response } - - its(:response_code) { should eq 400 } - - end - - context "without a service" do - - before do - CASino::API::ServiceTicketProviderProcessor.any_instance.should_receive(:process).with('TGT-valid', kind_of(Hash), request.user_agent) do |ticket, params| - params.should == controller.params - @controller.no_service_provided_via_api - end - post :update, id:'TGT-valid', use_route: :casino - end - - subject { response } - - its(:response_code) { should eq 400 } - - end - end - - describe "DELETE /cas/v1/tickets/TGT-fdsjfsdfjkalfewrihfdhfaie" do - before do - CASino::API::LogoutProcessor.any_instance.should_receive(:process).with('TGT-fdsjfsdfjkalfewrihfdhfaie', request.user_agent) do - @controller.user_logged_out_via_api - end - post :destroy, id: 'TGT-fdsjfsdfjkalfewrihfdhfaie', use_route: :casino - end - - subject { response } - - its(:response_code) { should eq 200 } - end - -end diff --git a/spec/controllers/auth_tokens_controller_spec.rb b/spec/controllers/auth_tokens_controller_spec.rb new file mode 100644 index 00000000..3aade54d --- /dev/null +++ b/spec/controllers/auth_tokens_controller_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +describe CASino::AuthTokensController do + include CASino::Engine.routes.url_helpers + + let(:params) { { } } + let(:request_options) { params.merge(use_route: :casino) } + + before(:each) do + CASino::AuthTokenValidationService.any_instance.stub(:validation_result).and_return(validation_result) + end + + describe 'GET "authTokenLogin"' do + context 'with invalid data' do + let(:validation_result) { false } + let(:service) { 'http://www.example.org/' } + let(:params) { { service: service } } + + it 'redirects to the login' do + get :login, request_options + response.should redirect_to(login_path(service: service)) + end + end + + context 'with valid data' do + let(:validation_result) { { authenticator: 'icanhaz', user_data: { username: 'cheezeburger' } } } + + context 'with a not allowed service' do + before(:each) do + FactoryGirl.create :service_rule, :regex, url: '^https://.*' + end + + let(:service) { 'http://www.example.org/' } + let(:params) { { service: service } } + + it 'renders the service_not_allowed template' do + get :login, request_options + response.should render_template(:service_not_allowed) + end + end + + context 'with a service' do + let(:service) { 'http://www.example.org/' } + let(:params) { { service: service } } + + it 'redirects to the service' do + get :login, request_options + response.location.should =~ /^#{Regexp.escape service}\?ticket=ST-/ + end + + it 'generates a service ticket' do + lambda do + get :login, request_options + end.should change(CASino::ServiceTicket, :count).by(1) + end + end + + it 'creates a cookie' do + get :login, request_options + response.cookies['tgt'].should_not be_nil + end + + it 'generates a ticket-granting ticket' do + lambda do + get :login, request_options + end.should change(CASino::TicketGrantingTicket, :count).by(1) + end + + it 'redirects to the session overview' do + get :login, request_options + response.should redirect_to(sessions_path) + end + end + end +end diff --git a/spec/controllers/listener/legacy_validator_spec.rb b/spec/controllers/listener/legacy_validator_spec.rb deleted file mode 100644 index cc78d023..00000000 --- a/spec/controllers/listener/legacy_validator_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'spec_helper' - -describe CASino::LegacyValidatorListener do - let(:controller) { Object.new } - let(:listener) { described_class.new(controller) } - let(:response_text) { "foobar\nbla\n" } - let(:render_parameters) { { text: response_text, content_type: 'text/plain' } } - - describe '#validation_succeeded' do - it 'tells the controller to render the response text' do - controller.should_receive(:render).with(render_parameters) - listener.validation_succeeded(response_text) - end - end - - describe '#validation_failed' do - it 'tells the controller to render the response text' do - controller.should_receive(:render).with(render_parameters) - listener.validation_failed(response_text) - end - end -end diff --git a/spec/controllers/listener/login_credential_acceptor_spec.rb b/spec/controllers/listener/login_credential_acceptor_spec.rb deleted file mode 100644 index 29f7cb71..00000000 --- a/spec/controllers/listener/login_credential_acceptor_spec.rb +++ /dev/null @@ -1,108 +0,0 @@ -require 'spec_helper' - -describe CASino::LoginCredentialAcceptorListener do - include CASino::Engine.routes.url_helpers - let(:controller) { Struct.new(:cookies).new(cookies: {}) } - let(:listener) { described_class.new(controller) } - - before(:each) do - controller.stub(:redirect_to) - end - - describe '#user_logged_in' do - let(:ticket_granting_ticket) { 'TGT-123' } - context 'with a service url' do - let(:url) { 'http://www.example.com/?ticket=ST-123' } - it 'tells the controller to redirect the client' do - controller.should_receive(:redirect_to).with(url, status: :see_other) - listener.user_logged_in(url, ticket_granting_ticket) - end - end - - context 'without a service url' do - let(:url) { nil } - it 'tells the controller to redirect to the session overview' do - controller.should_receive(:redirect_to).with(sessions_path, status: :see_other) - listener.user_logged_in(url, ticket_granting_ticket) - end - - it 'creates the tgt cookie' do - listener.user_logged_in(url, ticket_granting_ticket) - controller.cookies[:tgt][:value].should == ticket_granting_ticket - end - end - - context 'with cookie expiry time' do - let(:url) { Object.new } - let(:expiry_time) { Time.now } - it 'set the tgt cookie expiry time' do - listener.user_logged_in(url, ticket_granting_ticket, expiry_time) - controller.cookies[:tgt][:value].should == ticket_granting_ticket - controller.cookies[:tgt][:expires].should == expiry_time - end - end - end - - [:invalid_login_credentials, :invalid_login_ticket].each do |method| - context "##{method}" do - let(:login_ticket) { Object.new } - let(:flash) { ActionDispatch::Flash::FlashHash.new } - - before(:each) do - controller.stub(:render) - controller.stub(:flash).and_return(flash) - end - - it 'tells the controller to render the new template' do - controller.should_receive(:render).with('new', status: 403) - listener.send(method, login_ticket) - end - - it 'assigns a new login ticket' do - listener.send(method, login_ticket) - controller.instance_variable_get(:@login_ticket).should == login_ticket - end - - it 'should add an error message' do - listener.send(method, login_ticket) - flash[:error].should == I18n.t("login_credential_acceptor.#{method}") - end - end - end - - context '#service_not_allowed' do - let(:service) { 'http://www.example.com/foo' } - - before(:each) do - controller.stub(:render) - end - - it 'tells the controller to render the service_not_allowed template' do - controller.should_receive(:render).with('service_not_allowed', status: 403) - listener.send(:service_not_allowed, service) - end - - it 'assigns the not allowed service' do - listener.send(:service_not_allowed, service) - controller.instance_variable_get(:@service).should == service - end - end - - context '#two_factor_authentication_pending' do - let(:ticket_granting_ticket) { 'TGT-123' } - - before(:each) do - controller.stub(:render) - end - - it 'tells the controller to render the service_not_allowed template' do - controller.should_receive(:render).with('validate_otp') - listener.send(:two_factor_authentication_pending, ticket_granting_ticket) - end - - it 'assigns the not allowed service' do - listener.send(:two_factor_authentication_pending, ticket_granting_ticket) - controller.instance_variable_get(:@ticket_granting_ticket).should == ticket_granting_ticket - end - end -end diff --git a/spec/controllers/listener/login_credential_requestor_spec.rb b/spec/controllers/listener/login_credential_requestor_spec.rb deleted file mode 100644 index 3fe67518..00000000 --- a/spec/controllers/listener/login_credential_requestor_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -require 'spec_helper' - -describe CASino::LoginCredentialRequestorListener do - include CASino::Engine.routes.url_helpers - let(:controller) { Struct.new(:cookies).new(cookies: {}) } - let(:listener) { described_class.new(controller) } - - describe '#user_not_logged_in' do - let(:login_ticket) { Object.new } - it 'assigns the login ticket' do - listener.user_not_logged_in(login_ticket) - controller.instance_variable_get(:@login_ticket).should == login_ticket - end - - it 'deletes an existing ticket-granting ticket cookie' do - controller.cookies = { tgt: 'TGT-12345' } - listener.user_not_logged_in(login_ticket) - controller.cookies[:tgt].should be_nil - end - end - - describe '#user_logged_in' do - context 'with a service url' do - let(:url) { 'http://www.example.com/?ticket=ST-123' } - it 'tells the controller to redirect the client' do - controller.should_receive(:redirect_to).with(url, status: :see_other) - listener.user_logged_in(url) - end - end - - context 'without a service url' do - let(:url) { nil } - it 'tells the controller to redirect to the session overview' do - controller.should_receive(:redirect_to).with(sessions_path) - listener.user_logged_in(url) - end - end - end - - context '#service_not_allowed' do - let(:service) { 'http://www.example.com/foo' } - - before(:each) do - controller.stub(:render) - end - - it 'tells the controller to render the service_not_allowed template' do - controller.should_receive(:render).with('service_not_allowed', status: 403) - listener.send(:service_not_allowed, service) - end - - it 'assigns the not allowed service' do - listener.send(:service_not_allowed, service) - controller.instance_variable_get(:@service).should == service - end - end -end diff --git a/spec/controllers/listener/logout_spec.rb b/spec/controllers/listener/logout_spec.rb deleted file mode 100644 index 3c13c022..00000000 --- a/spec/controllers/listener/logout_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -require 'spec_helper' - -describe CASino::LogoutListener do - include Rails.application.routes.url_helpers - let(:controller) { Struct.new(:cookies).new(cookies: {}) } - let(:listener) { described_class.new(controller) } - - describe '#user_logged_out' do - let(:url) { 'http://www.example.com/test' } - it 'assigns the url' do - listener.user_logged_out(url) - controller.instance_variable_get(:@url).should == url - end - - it 'deletes an existing ticket-granting ticket cookie' do - controller.cookies = { tgt: 'TGT-12345' } - listener.user_logged_out(url) - controller.cookies[:tgt].should be_nil - end - - context 'with redirect_immediately flag' do - before(:each) do - controller.stub(:redirect_to) - end - - it 'tells the controller to redirect the client' do - controller.should_receive(:redirect_to).with(url, status: :see_other) - listener.user_logged_out(url, true) - end - - it 'deletes an existing ticket-granting ticket cookie' do - controller.cookies = { tgt: 'TGT-12345' } - listener.user_logged_out(url, true) - controller.cookies[:tgt].should be_nil - end - end - end -end diff --git a/spec/controllers/listener/other_sessions_destroyer_spec.rb b/spec/controllers/listener/other_sessions_destroyer_spec.rb deleted file mode 100644 index 819cd935..00000000 --- a/spec/controllers/listener/other_sessions_destroyer_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'spec_helper' - -describe CASino::OtherSessionsDestroyerListener do - include CASino::Engine.routes.url_helpers - let(:controller) { Struct.new(:cookies).new(cookies: {}) } - let(:listener) { described_class.new(controller) } - - before(:each) do - controller.stub(:redirect_to) - end - - describe '#other_sessions_destroyed' do - let(:service) { 'http://www.example.com/' } - it 'redirects back to the URL' do - controller.should_receive(:redirect_to).with(service) - listener.other_sessions_destroyed(service) - end - end -end diff --git a/spec/controllers/listener/proxy_ticket_provider_spec.rb b/spec/controllers/listener/proxy_ticket_provider_spec.rb deleted file mode 100644 index bd25f5d7..00000000 --- a/spec/controllers/listener/proxy_ticket_provider_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'spec_helper' - -describe CASino::ProxyTicketProviderListener do - let(:controller) { Object.new } - let(:listener) { described_class.new(controller) } - let(:xml) { "bla" } - let(:render_parameters) { { xml: xml } } - - describe '#request_succeeded' do - it 'tells the controller to render the response xml' do - controller.should_receive(:render).with(render_parameters) - listener.request_succeeded(xml) - end - end - - describe '#request_failed' do - it 'tells the controller to render the response xml' do - controller.should_receive(:render).with(render_parameters) - listener.request_failed(xml) - end - end -end diff --git a/spec/controllers/listener/second_factor_authentication_acceptor_spec.rb b/spec/controllers/listener/second_factor_authentication_acceptor_spec.rb deleted file mode 100644 index a6ac5a99..00000000 --- a/spec/controllers/listener/second_factor_authentication_acceptor_spec.rb +++ /dev/null @@ -1,74 +0,0 @@ -require 'spec_helper' - -describe CASino::SecondFactorAuthenticationAcceptorListener do - include CASino::Engine.routes.url_helpers - let(:controller) { Struct.new(:cookies).new(cookies: {}) } - let(:listener) { described_class.new(controller) } - - before(:each) do - controller.stub(:redirect_to) - end - - describe '#user_not_logged_in' do - it 'redirects to the login page' do - controller.should_receive(:redirect_to).with(login_path) - listener.user_not_logged_in - end - end - - describe '#user_logged_in' do - let(:ticket_granting_ticket) { 'TGT-123' } - context 'with a service url' do - let(:url) { 'http://www.example.com/?ticket=ST-123' } - it 'tells the controller to redirect the client' do - controller.should_receive(:redirect_to).with(url, status: :see_other) - listener.user_logged_in(url, ticket_granting_ticket) - end - end - - context 'without a service url' do - let(:url) { nil } - it 'tells the controller to redirect to the session overview' do - controller.should_receive(:redirect_to).with(sessions_path, status: :see_other) - listener.user_logged_in(url, ticket_granting_ticket) - end - - it 'creates the tgt cookie' do - listener.user_logged_in(url, ticket_granting_ticket) - controller.cookies[:tgt].should == { value: ticket_granting_ticket, expires: nil } - end - end - end - - context "#invalid_one_time_password" do - let(:flash) { ActionDispatch::Flash::FlashHash.new } - - before(:each) do - controller.stub(:render) - controller.stub(:flash).and_return(flash) - end - - it 'should add an error message' do - listener.invalid_one_time_password - flash[:error].should == I18n.t('validate_otp.invalid_otp') - end - end - - context '#service_not_allowed' do - let(:service) { 'http://www.example.com/foo' } - - before(:each) do - controller.stub(:render) - end - - it 'tells the controller to render the service_not_allowed template' do - controller.should_receive(:render).with('service_not_allowed', status: 403) - listener.send(:service_not_allowed, service) - end - - it 'assigns the not allowed service' do - listener.send(:service_not_allowed, service) - controller.instance_variable_get(:@service).should == service - end - end -end diff --git a/spec/controllers/listener/session_destroyer_spec.rb b/spec/controllers/listener/session_destroyer_spec.rb deleted file mode 100644 index e3243285..00000000 --- a/spec/controllers/listener/session_destroyer_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -require 'spec_helper' - -describe CASino::SessionDestroyerListener do - include CASino::Engine.routes.url_helpers - let(:controller) { Struct.new(:cookies).new(cookies: {}) } - let(:listener) { described_class.new(controller) } - - before(:each) do - controller.stub(:redirect_to) - end - - describe '#ticket_not_found' do - it 'redirects back to the session overview' do - controller.should_receive(:redirect_to).with(sessions_path) - listener.ticket_not_found - end - end - - describe '#ticket_deleted' do - it 'redirects back to the session overview' do - controller.should_receive(:redirect_to).with(sessions_path) - listener.ticket_deleted - end - end -end diff --git a/spec/controllers/listener/session_overview_spec.rb b/spec/controllers/listener/session_overview_spec.rb deleted file mode 100644 index 1a597fec..00000000 --- a/spec/controllers/listener/session_overview_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -require 'spec_helper' - -describe CASino::SessionOverviewListener do - include CASino::Engine.routes.url_helpers - let(:controller) { Object.new } - let(:listener) { described_class.new(controller) } - - describe '#user_not_logged_in' do - before(:each) do - controller.stub(:redirect_to) - end - - it 'redirects to the login page' do - controller.should_receive(:redirect_to).with(login_path) - listener.user_not_logged_in - end - end - - describe '#ticket_granting_tickets_found' do - let(:ticket_granting_tickets) { [ Object.new, Object.new ] } - it 'assigns the ticket-granting tickets' do - listener.ticket_granting_tickets_found(ticket_granting_tickets) - controller.instance_variable_get(:@ticket_granting_tickets).should == ticket_granting_tickets - end - end -end diff --git a/spec/controllers/listener/ticket_validator_spec.rb b/spec/controllers/listener/ticket_validator_spec.rb deleted file mode 100644 index 250c36e1..00000000 --- a/spec/controllers/listener/ticket_validator_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'spec_helper' - -describe CASino::TicketValidatorListener do - let(:controller) { Object.new } - let(:listener) { described_class.new(controller) } - let(:xml) { "bla" } - let(:render_parameters) { { xml: xml } } - - describe '#validation_succeeded' do - it 'tells the controller to render the response xml' do - controller.should_receive(:render).with(render_parameters) - listener.validation_succeeded(xml) - end - end - - describe '#validation_failed' do - it 'tells the controller to render the response xml' do - controller.should_receive(:render).with(render_parameters) - listener.validation_failed(xml) - end - end -end diff --git a/spec/controllers/listener/two_factor_authenticator_activator_spec.rb b/spec/controllers/listener/two_factor_authenticator_activator_spec.rb deleted file mode 100644 index dc1b131d..00000000 --- a/spec/controllers/listener/two_factor_authenticator_activator_spec.rb +++ /dev/null @@ -1,64 +0,0 @@ -require 'spec_helper' - -describe CASino::TwoFactorAuthenticatorActivatorListener do - include CASino::Engine.routes.url_helpers - let(:controller) { Struct.new(:cookies).new(cookies: {}) } - let(:listener) { described_class.new(controller) } - let(:flash) { ActionDispatch::Flash::FlashHash.new } - - before(:each) do - controller.stub(:redirect_to) - controller.stub(:render) - controller.stub(:flash).and_return(flash) - end - - describe '#user_not_logged_in' do - it 'redirects to the login page' do - controller.should_receive(:redirect_to).with(login_path) - listener.user_not_logged_in - end - end - - describe '#two_factor_authenticator_activated' do - it 'redirects to the session overview' do - controller.should_receive(:redirect_to).with(sessions_path) - listener.two_factor_authenticator_activated - end - - it 'adds a notice' do - listener.two_factor_authenticator_activated - flash[:notice].should == I18n.t('two_factor_authenticators.successfully_activated') - end - end - - describe '#invalid_two_factor_authenticator' do - it 'redirects to the two-factor authenticator new page' do - controller.should_receive(:redirect_to).with(new_two_factor_authenticator_path) - listener.invalid_two_factor_authenticator - end - - it 'adds a error message' do - listener.invalid_two_factor_authenticator - flash[:error].should == I18n.t('two_factor_authenticators.invalid_two_factor_authenticator') - end - end - - describe '#invalid_one_time_password' do - let(:two_factor_authenticator) { Object.new } - - it 'rerenders the new page' do - controller.should_receive(:render).with('new') - listener.invalid_one_time_password(two_factor_authenticator) - end - - it 'adds a error message' do - listener.invalid_one_time_password(two_factor_authenticator) - flash[:error].should == I18n.t('two_factor_authenticators.invalid_one_time_password') - end - - it 'assigns the two-factor authenticator' do - listener.invalid_one_time_password(two_factor_authenticator) - controller.instance_variable_get(:@two_factor_authenticator).should == two_factor_authenticator - end - end -end diff --git a/spec/controllers/listener/two_factor_authenticator_destroyer_spec.rb b/spec/controllers/listener/two_factor_authenticator_destroyer_spec.rb deleted file mode 100644 index 255f1279..00000000 --- a/spec/controllers/listener/two_factor_authenticator_destroyer_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'spec_helper' - -describe CASino::TwoFactorAuthenticatorDestroyerListener do - include CASino::Engine.routes.url_helpers - let(:controller) { Struct.new(:cookies).new(cookies: {}) } - let(:listener) { described_class.new(controller) } - let(:flash) { ActionDispatch::Flash::FlashHash.new } - - before(:each) do - controller.stub(:redirect_to) - controller.stub(:render) - controller.stub(:flash).and_return(flash) - end - - describe '#user_not_logged_in' do - it 'redirects to the login page' do - controller.should_receive(:redirect_to).with(login_path) - listener.user_not_logged_in - end - end - - describe '#two_factor_authenticator_destroyed' do - it 'redirects to the session overview' do - controller.should_receive(:redirect_to).with(sessions_path) - listener.two_factor_authenticator_destroyed - end - - it 'adds a notice' do - listener.two_factor_authenticator_destroyed - flash[:notice].should == I18n.t('two_factor_authenticators.successfully_deleted') - end - end - - describe '#invalid_two_factor_authenticator' do - it 'redirects to the session overview' do - controller.should_receive(:redirect_to).with(sessions_path) - listener.invalid_two_factor_authenticator - end - end -end diff --git a/spec/controllers/listener/two_factor_authenticator_overview_spec.rb b/spec/controllers/listener/two_factor_authenticator_overview_spec.rb deleted file mode 100644 index 18aa4b3b..00000000 --- a/spec/controllers/listener/two_factor_authenticator_overview_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'spec_helper' - -describe CASino::TwoFactorAuthenticatorOverviewListener do - include CASino::Engine.routes.url_helpers - let(:controller) { Struct.new(:cookies).new(cookies: {}) } - let(:listener) { described_class.new(controller) } - - describe '#two_factor_authenticators_found' do - let(:two_factor_authenticators) { [Object.new] } - - it 'assigns the two-factor authenticators' do - listener.two_factor_authenticators_found(two_factor_authenticators) - controller.instance_variable_get(:@two_factor_authenticators).should == two_factor_authenticators - end - end -end diff --git a/spec/controllers/listener/two_factor_authenticator_registrator_spec.rb b/spec/controllers/listener/two_factor_authenticator_registrator_spec.rb deleted file mode 100644 index efc36b94..00000000 --- a/spec/controllers/listener/two_factor_authenticator_registrator_spec.rb +++ /dev/null @@ -1,27 +0,0 @@ -require 'spec_helper' - -describe CASino::TwoFactorAuthenticatorRegistratorListener do - include CASino::Engine.routes.url_helpers - let(:controller) { Struct.new(:cookies).new(cookies: {}) } - let(:listener) { described_class.new(controller) } - - before(:each) do - controller.stub(:redirect_to) - end - - describe '#user_not_logged_in' do - it 'redirects to the login page' do - controller.should_receive(:redirect_to).with(login_path) - listener.user_not_logged_in - end - end - - describe '#two_factor_authenticator_registered' do - let(:two_factor_authenticator) { Object.new } - - it 'assigns the two-factor authenticator' do - listener.two_factor_authenticator_registered(two_factor_authenticator) - controller.instance_variable_get(:@two_factor_authenticator).should == two_factor_authenticator - end - end -end diff --git a/spec/controllers/proxy_tickets_controller_spec.rb b/spec/controllers/proxy_tickets_controller_spec.rb index 961414a8..ea79e0f5 100644 --- a/spec/controllers/proxy_tickets_controller_spec.rb +++ b/spec/controllers/proxy_tickets_controller_spec.rb @@ -1,25 +1,131 @@ require 'spec_helper' describe CASino::ProxyTicketsController do - describe 'GET "serviceValidate"' do - let(:params) { { service: 'https://www.example.com/', use_route: :casino } } - it 'calls the process method of the ProxyTicketValidator' do - CASino::ProxyTicketValidatorProcessor.any_instance.should_receive(:process).with(kind_of(Hash)) do |params| - params.should == controller.params - controller.render nothing: true - end - get :proxy_validate, params + let(:request_options) { params.merge(use_route: :casino) } + + describe 'GET "proxyValidate"' do + let(:proxy_ticket) { FactoryGirl.create :proxy_ticket } + let(:service) { proxy_ticket.service } + let(:parameters) { { service: service, ticket: proxy_ticket.ticket }} + let(:params) { parameters } + let(:regex_success) { /\A\s*#{proxy_ticket.proxy_granting_ticket.pgt_url}<\/cas:proxy>\s*<\/cas:proxies>/ } + + it 'answers with the success text' do + get :proxy_validate, request_options + response.body.should =~ regex_success + end + + it 'includes the proxy in the response' do + get :proxy_validate, request_options + response.body.should =~ regex_proxy + end + + context 'with an expired proxy ticket' do + before(:each) do + CASino::ProxyTicket.any_instance.stub(:expired?).and_return(true) + end + + it 'answers with the failure text' do + get :proxy_validate, request_options + response.body.should =~ regex_failure + end + end + + context 'with an other service' do + let(:params) { parameters.merge(service: 'this_is_another_service') } + + it 'answers with the failure text' do + get :proxy_validate, request_options + response.body.should =~ regex_failure + end + end + + context 'without an existing ticket' do + let(:params) { { ticket: 'PT-1234', service: 'https://www.example.com/' } } + + it 'answers with the failure text' do + get :proxy_validate, request_options + response.body.should =~ regex_failure + end + end end end describe 'GET "proxy"' do - let(:params) { { service: 'https://www.example.com/', use_route: :casino } } - it 'calls the process method of the ProxyTicketProvider' do - CASino::ProxyTicketProviderProcessor.any_instance.should_receive(:process).with(kind_of(Hash)) do |params| - params.should == controller.params - controller.render nothing: true + let(:parameters) { { targetService: 'this_does_not_have_to_be_a_url' } } + let(:params) { parameters } + let(:regex_success) { /\A#{proxy_ticket.ticket}<\/cas:proxyTicket>/ + end + + context 'without a targetService' do + let(:params) { parameters.merge(pgt: proxy_granting_ticket.ticket, targetService: nil) } + + it 'answers with the failure text' do + get :create, request_options + response.body.should =~ regex_failure + end + + it 'does not create a proxy ticket' do + lambda do + get :create, request_options + end.should_not change(CASino::ProxyTicket, :count) + end end - get :create, params end end end diff --git a/spec/controllers/service_and_proxy_tickets_controller_spec.rb b/spec/controllers/service_and_proxy_tickets_controller_spec.rb new file mode 100644 index 00000000..64cdbbe1 --- /dev/null +++ b/spec/controllers/service_and_proxy_tickets_controller_spec.rb @@ -0,0 +1,224 @@ +require 'spec_helper' + +shared_examples_for 'a service ticket validator' do + include CASino::Engine.routes.url_helpers + let(:request_options) { params.merge(use_route: :casino) } + let(:service_ticket) { FactoryGirl.create :service_ticket } + let(:service) { service_ticket.service } + let(:parameters) { { service: service, ticket: service_ticket.ticket }} + let(:params) { parameters } + + render_views + + describe 'GET "serviceValidate"' do + let(:regex_failure) { /\A\ 1234 }) + end + + it 'includes the extra attributes' do + get validation_action, request_options + response.body.should =~ /1234<\/cas\:id\>/ + end + end + + context 'with extra attributes using array as value' do + before(:each) do + CASino::User.any_instance.stub(:extra_attributes).and_return({ "memberOf" => [ "test", "yolo" ] }) + end + + it 'includes all values' do + get validation_action, request_options + response.body.should =~ /test<\/cas\:memberOf\>/ + response.body.should =~ /yolo<\/cas\:memberOf\>/ + end + end + + context 'issued from a long_term ticket-granting ticket' do + before(:each) do + service_ticket.ticket_granting_ticket.update_attribute(:long_term, true) + end + + it 'includes the long-term flag in the answer' do + get validation_action, request_options + response.body.should =~ /true<\/cas\:longTermAuthenticationRequestTokenUsed>/ + end + end + + context 'without renew flag' do + it 'consumes the service ticket' do + get validation_action, request_options + service_ticket.reload.consumed.should == true + end + + it 'answers with the success text' do + get validation_action, request_options + response.body.should =~ regex_success + end + end + + context 'with empty query values' do + let(:service) { "#{service_ticket.service}?" } + + it 'answers with the success text' do + get validation_action, request_options + response.body.should =~ regex_success + end + end + + context 'with renew flag' do + let(:params) { parameters.merge renew: 'true' } + + context 'with a service ticket without issued_from_credentials flag' do + it 'consumes the service ticket' do + get validation_action, request_options + service_ticket.reload.consumed.should == true + end + + it 'answers with the failure text' do + get validation_action, request_options + response.body.should =~ regex_failure + end + end + + context 'with a service ticket with issued_from_credentials flag' do + before(:each) do + service_ticket.issued_from_credentials = true + service_ticket.save! + end + + it 'consumes the service ticket' do + get validation_action, request_options + service_ticket.reload.consumed.should == true + end + + it 'answers with the success text' do + get validation_action, request_options + response.body.should =~ regex_success + end + end + end + + context 'with proxy-granting ticket callback server' do + let(:pgt_url) { 'https://www.example.org' } + let(:params) { parameters.merge pgtUrl: pgt_url } + + before(:each) do + stub_request(:get, /#{pgt_url}\/\?pgtId=[^&]+&pgtIou=[^&]+/) + end + + context 'not using https' do + let(:pgt_url) { 'http://www.example.org' } + + it 'answers with the success text' do + get validation_action, request_options + response.body.should =~ regex_success + end + + it 'does not create a proxy-granting ticket' do + lambda do + get validation_action, request_options + end.should_not change(service_ticket.proxy_granting_tickets, :count) + end + end + + it 'answers with the success text' do + get validation_action, request_options + response.body.should =~ regex_success + end + + it 'includes the PGTIOU in the response' do + get validation_action, request_options + response.body.should =~ /\\n?\s*PGTIOU-.+/ + end + + it 'creates a proxy-granting ticket' do + lambda do + get validation_action, request_options + end.should change(service_ticket.proxy_granting_tickets, :count).by(1) + end + + it 'contacts the callback server' do + get validation_action, request_options + proxy_granting_ticket = CASino::ProxyGrantingTicket.last + WebMock.should have_requested(:get, 'https://www.example.org').with(query: { + pgtId: proxy_granting_ticket.ticket, + pgtIou: proxy_granting_ticket.iou + }) + end + + context 'when callback server gives an error' do + before(:each) do + stub_request(:get, /#{pgt_url}.*/).to_return status: 404 + end + + it 'answers with the success text' do + get validation_action, request_options + response.body.should =~ regex_success + end + + it 'does not create a proxy-granting ticket' do + lambda do + get validation_action, request_options + end.should_not change(service_ticket.proxy_granting_tickets, :count) + end + end + + context 'when callback server is unreachable' do + before(:each) do + stub_request(:get, /#{pgt_url}.*/).to_raise(Timeout::Error) + end + + it 'answers with the success text' do + get validation_action, request_options + response.body.should =~ regex_success + end + + it 'does not create a proxy-granting ticket' do + lambda do + get validation_action, request_options + end.should_not change(service_ticket.proxy_granting_tickets, :count) + end + end + end + end + + context 'with a consumed service ticket' do + before(:each) do + service_ticket.update_attribute(:consumed, true) + end + + it 'answers with the failure text' do + get validation_action, request_options + response.body.should =~ regex_failure + end + end + end +end + +describe CASino::ServiceTicketsController do + let(:validation_action) { :service_validate } + it_behaves_like 'a service ticket validator' +end + +describe CASino::ProxyTicketsController do + let(:validation_action) { :proxy_validate } + it_behaves_like 'a service ticket validator' +end diff --git a/spec/controllers/service_tickets_controller_spec.rb b/spec/controllers/service_tickets_controller_spec.rb index c15129a0..e6ace6fc 100644 --- a/spec/controllers/service_tickets_controller_spec.rb +++ b/spec/controllers/service_tickets_controller_spec.rb @@ -1,25 +1,71 @@ -require 'spec_helper' - describe CASino::ServiceTicketsController do describe 'GET "validate"' do - let(:params) { { service: 'https://www.example.com/', use_route: :casino } } - it 'calls the process method of the LegacyValidator' do - CASino::LegacyValidatorProcessor.any_instance.should_receive(:process).with(kind_of(Hash)) do |params| - params.should == controller.params - controller.render nothing: true + let(:request_options) { params.merge(use_route: :casino) } + let(:service_ticket) { FactoryGirl.create :service_ticket } + let(:service) { service_ticket.service } + let(:parameters) { { service: service, ticket: service_ticket.ticket }} + let(:params) { parameters } + let(:username) { service_ticket.ticket_granting_ticket.user.username } + let(:response_text_success) { "yes\n#{username}\n" } + let(:response_text_failure) { "no\n\n" } + + render_views + + context 'with an unconsumed service ticket' do + context 'without renew flag' do + it 'consumes the service ticket' do + get :validate, request_options + service_ticket.reload.consumed.should == true + end + + it 'answers with the expected response text' do + get :validate, request_options + response.body.should == response_text_success + end + end + + context 'with renew flag' do + let(:params) { parameters.merge renew: 'true' } + + context 'with a service ticket without issued_from_credentials flag' do + it 'consumes the service ticket' do + get :validate, request_options + service_ticket.reload.consumed.should == true + end + + it 'answers with the expected response text' do + get :validate, request_options + response.body.should == response_text_failure + end + end + + context 'with a service ticket with issued_from_credentials flag' do + before(:each) do + service_ticket.update_attribute(:issued_from_credentials, true) + end + + it 'consumes the service ticket' do + get :validate, request_options + service_ticket.reload.consumed.should == true + end + + it 'answers with the expected response text' do + get :validate, request_options + response.body.should == response_text_success + end + end end - get :validate, params end - end - describe 'GET "serviceValidate"' do - let(:params) { { service: 'https://www.example.com/', use_route: :casino } } - it 'calls the process method of the LegacyValidator' do - CASino::ServiceTicketValidatorProcessor.any_instance.should_receive(:process).with(kind_of(Hash)) do |params| - params.should == controller.params - controller.render nothing: true + context 'with a consumed service ticket' do + before(:each) do + service_ticket.update_attribute(:consumed, true) + end + + it 'answers with the expected response text' do + get :validate, request_options + response.body.should == response_text_failure end - get :service_validate, params end end end diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index d049ef06..d9a7f9b2 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -1,10 +1,159 @@ require 'spec_helper' describe CASino::SessionsController do + include CASino::Engine.routes.url_helpers + let(:params) { { } } + let(:request_options) { params.merge(use_route: :casino) } + let(:user_agent) { 'YOLOBrowser 420.00'} + + before(:each) do + request.user_agent = user_agent + end + describe 'GET "new"' do - it 'calls the process method of the LoginCredentialRequestor' do - CASino::LoginCredentialRequestorProcessor.any_instance.should_receive(:process) - get :new, use_route: :casino + context 'with a not allowed service' do + before(:each) do + FactoryGirl.create :service_rule, :regex, url: '^https://.*' + end + + let(:service) { 'http://www.example.org/' } + let(:params) { { service: service } } + + it 'renders the service_not_allowed template' do + get :new, request_options + response.should render_template(:service_not_allowed) + end + end + + context 'when logged out' do + it 'renders the new template' do + get :new, request_options + response.should render_template(:new) + end + + context 'with gateway parameter' do + context 'with a service' do + let(:service) { 'http://example.com/' } + let(:params) { { service: service, gateway: 'true' } } + + it 'redirects to the service' do + get :new, request_options + response.should redirect_to(service) + end + end + + context 'without a service' do + let(:params) { { gateway: 'true' } } + + it 'renders the new template' do + get :new, request_options + response.should render_template(:new) + end + end + end + end + + context 'when logged in' do + let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket } + + before(:each) do + sign_in(ticket_granting_ticket) + end + + context 'when two-factor authentication is pending' do + let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket, :awaiting_two_factor_authentication } + + it 'renders the new template' do + get :new, request_options + response.should render_template(:new) + 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 'renders the new template' do + get :new, request_options + response.should render_template(:new) + end + end + + context 'with a service' do + let(:service) { 'http://example.com/' } + let(:params) { { service: service } } + + it 'redirects to the service' do + get :new, request_options + response.location.should =~ /^#{Regexp.escape service}\?ticket=ST-/ + end + + it 'generates a service ticket' do + lambda do + get :new, request_options + end.should change(CASino::ServiceTicket, :count).by(1) + end + + it 'does not set the issued_from_credentials flag on the service ticket' do + get :new, request_options + CASino::ServiceTicket.last.should_not be_issued_from_credentials + end + + context 'with renew parameter' do + it 'renders the new template' do + get :new, request_options.merge(renew: 'true') + response.should render_template(:new) + end + end + end + + context 'with a service with nested attributes' do + let(:service) { 'http://example.com/?a%5B%5D=test&a%5B%5D=example' } + let(:params) { { service: service } } + + it 'does not remove the attributes' do + get :new, request_options + response.location.should =~ /^#{Regexp.escape service}&ticket=ST-/ + end + end + + context 'with a broken service' do + let(:service) { '%3Atest' } + let(:params) { { service: service } } + + it 'redirects to the session overview' do + get :new, request_options + response.should redirect_to(sessions_path) + end + end + + context 'without a service' do + it 'redirects to the session overview' do + get :new, request_options + response.should redirect_to(sessions_path) + end + + it 'does not generate a service ticket' do + lambda do + get :new, request_options + end.should change(CASino::ServiceTicket, :count).by(0) + end + + context 'with a changed browser' do + let(:user_agent) { 'FooBar 1.0' } + + before(:each) do + request.user_agent = user_agent + end + + it 'renders the new template' do + get :new, request_options + response.should render_template(:new) + end + end + end end context 'with an unsupported format' do @@ -16,63 +165,500 @@ end describe 'POST "create"' do - it 'calls the process method of the LoginCredentialAcceptor' do - CASino::LoginCredentialAcceptorProcessor.any_instance.should_receive(:process) do - @controller.render nothing: true + context 'without a valid login ticket' do + it 'renders the new template' do + post :create, request_options + response.should render_template(:new) + end + end + + context 'with an expired login ticket' do + let(:expired_login_ticket) { FactoryGirl.create :login_ticket, :expired } + let(:params) { { lt: expired_login_ticket.ticket }} + + it 'renders the new template' do + post :create, request_options + response.should render_template(:new) + end + end + + context 'with a valid login ticket' do + let(:login_ticket) { FactoryGirl.create :login_ticket } + let(:params) { { lt: login_ticket.ticket }} + + context 'with invalid credentials' do + it 'renders the new template' do + post :create, request_options + response.should render_template(:new) + end + end + + context 'with valid credentials' do + let(:service) { 'https://www.example.org' } + let(:username) { 'testuser' } + let(:authenticator) { 'static' } + let(:params) { { lt: login_ticket.ticket, username: username, password: 'foobar123', service: service } } + + it 'creates a cookie' do + post :create, request_options + response.cookies['tgt'].should_not be_nil + end + + context 'with rememberMe set' do + let(:cookie_jar) { HashWithIndifferentAccess.new } + + before(:each) do + params[:rememberMe] = true + controller.stub(:cookies).and_return(cookie_jar) + end + + it 'creates a cookie with an expiration date set' do + post :create, request_options + cookie_jar['tgt']['expires'].should be_kind_of(Time) + end + + it 'creates a long-term ticket-granting ticket' do + post :create, request_options + tgt = CASino::TicketGrantingTicket.last + tgt.long_term.should == true + end + end + + context 'with two-factor authentication enabled' do + let!(:user) { CASino::User.create! username: username, authenticator: authenticator } + let!(:two_factor_authenticator) { FactoryGirl.create :two_factor_authenticator, user: user } + + it 'renders the validate_otp template' do + post :create, request_options + response.should render_template(:validate_otp) + end + end + + context 'with a not allowed service' do + before(:each) do + FactoryGirl.create :service_rule, :regex, url: '^https://.*' + end + let(:service) { 'http://www.example.org/' } + + it 'renders the service_not_allowed template' do + post :create, request_options + response.should render_template(:service_not_allowed) + end + end + + context 'when all authenticators raise an error' do + before(:each) do + CASino::StaticAuthenticator.any_instance.stub(:validate) do + raise CASino::Authenticator::AuthenticatorError, 'error123' + end + end + + it 'renders the new template' do + post :create, request_options + response.should render_template(:new) + end + end + + context 'without a service' do + let(:service) { nil } + + it 'redirects to the session overview' do + post :create, request_options + response.should redirect_to(sessions_path) + end + + it 'generates a ticket-granting ticket' do + lambda do + post :create, request_options + end.should change(CASino::TicketGrantingTicket, :count).by(1) + end + + context 'when the user does not exist yet' do + it 'generates exactly one user' do + lambda do + post :create, request_options + end.should change(CASino::User, :count).by(1) + end + + it 'sets the users attributes' do + post :create, request_options + user = CASino::User.last + user.username.should == username + user.authenticator.should == authenticator + end + end + + context 'when the user already exists' do + let!(:user) { CASino::User.create! username: username, authenticator: authenticator } + + it 'does not regenerate the user' do + lambda do + post :create, request_options + end.should_not change(CASino::User, :count) + end + + it 'updates the extra attributes' do + lambda do + post :create, request_options + user.reload + end.should change(user, :extra_attributes) + end + end + end + + context 'with a service' do + let(:service) { 'https://www.example.com' } + + it 'redirects to the service' do + post :create, request_options + response.location.should =~ /^#{Regexp.escape service}\/\?ticket=ST-/ + end + + it 'generates a service ticket' do + lambda do + post :create, request_options + end.should change(CASino::ServiceTicket, :count).by(1) + end + + it 'does set the issued_from_credentials flag on the service ticket' do + post :create, request_options + CASino::ServiceTicket.last.should be_issued_from_credentials + end + + it 'generates a ticket-granting ticket' do + lambda do + post :create, request_options + end.should change(CASino::TicketGrantingTicket, :count).by(1) + end + end end - post :create, use_route: :casino end end describe 'POST "validate_otp"' do - it 'calls the process method of the SecondFactorAuthenticatonAcceptor' do - CASino::SecondFactorAuthenticationAcceptorProcessor.any_instance.should_receive(:process) do - @controller.render nothing: true + 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 } + let(:tgt) { ticket_granting_ticket.ticket } + let(:user_agent) { ticket_granting_ticket.user_agent } + let(:otp) { '123456' } + let(:service) { 'http://www.example.com/testing' } + let(:params) { { tgt: tgt, otp: otp, service: service }} + + context 'with an active authenticator' do + let!(:two_factor_authenticator) { FactoryGirl.create :two_factor_authenticator, user: user } + + context 'with a valid OTP' do + before(:each) do + ROTP::TOTP.any_instance.should_receive(:verify_with_drift).with(otp, 30).and_return(true) + end + + it 'redirects to the service' do + post :validate_otp, request_options + response.location.should =~ /^#{Regexp.escape service}\?ticket=ST-/ + end + + it 'does activate the ticket-granting ticket' do + post :validate_otp, request_options + ticket_granting_ticket.reload.should_not be_awaiting_two_factor_authentication + end + + context 'with a long-term ticket-granting ticket' do + let(:cookie_jar) { HashWithIndifferentAccess.new } + + before(:each) do + ticket_granting_ticket.update_attributes! long_term: true + controller.stub(:cookies).and_return(cookie_jar) + end + + it 'creates a cookie with an expiration date set' do + post :validate_otp, request_options + cookie_jar['tgt']['expires'].should be_kind_of(Time) + end + end + + context 'with a not allowed service' do + before(:each) do + FactoryGirl.create :service_rule, :regex, url: '^https://.*' + end + let(:service) { 'http://www.example.org/' } + + it 'renders the service_not_allowed template' do + post :validate_otp, request_options + response.should render_template(:service_not_allowed) + end + end + end + + context 'with an invalid OTP' do + before(:each) do + ROTP::TOTP.any_instance.should_receive(:verify_with_drift).with(otp, 30).and_return(false) + end + + it 'renders the validate_otp template' do + post :validate_otp, request_options + response.should render_template(:validate_otp) + end + + it 'does not activate the ticket-granting ticket' do + post :validate_otp, request_options + ticket_granting_ticket.reload.should be_awaiting_two_factor_authentication + end + end + end + end + + context 'without a ticket-granting ticket' do + it 'redirects to the login page' do + post :validate_otp, request_options + response.should redirect_to(login_path) end - post :validate_otp, use_route: :casino end end 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| - params.should == controller.params - cookies.should == controller.cookies - user_agent.should == request.user_agent + let(:url) { nil } + let(:params) { { :url => url } } + + context 'with an existing ticket-granting ticket' do + let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket } + + before(:each) do + sign_in(ticket_granting_ticket) + end + + it 'deletes the ticket-granting ticket' do + get :logout, request_options + CASino::TicketGrantingTicket.where(id: ticket_granting_ticket.id).first.should == nil + end + + it 'renders the logout template' do + get :logout, request_options + response.should render_template(:logout) + end + + context 'with an URL' do + let(:url) { 'http://www.example.com' } + + it 'assigns the URL' do + get :logout, request_options + assigns(:url).should == url + end + end + + context 'with a service' do + let(:params) { { :service => url } } + let(:url) { 'http://www.example.org' } + + context 'when whitelisted' do + it 'redirects to the service' do + get :logout, request_options + response.should redirect_to(url) + end + end + + context 'when not whitelisted' do + before(:each) do + FactoryGirl.create :service_rule, :regex, url: '^https://.*' + end + + it 'renders the logout template' do + get :logout, request_options + response.should render_template(:logout) + end + + it 'does not assign the URL' do + get :logout, request_options + assigns(:url).should be_nil + end + end + end + end + + context 'without a ticket-granting ticket' do + it 'renders the logout template' do + get :logout, request_options + response.should render_template(:logout) end - get :logout, use_route: :casino end end describe 'GET "index"' do - it 'calls the process method of the SessionOverview processor' do - CASino::TwoFactorAuthenticatorOverviewProcessor.any_instance.should_receive(:process) - CASino::SessionOverviewProcessor.any_instance.should_receive(:process) - get :index, use_route: :casino + context 'with an existing ticket-granting ticket' do + before(:each) do + sign_in(ticket_granting_ticket) + end + + describe 'two-factor authenticator settings' do + let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket } + let(:user) { ticket_granting_ticket.user } + + context 'without a two-factor authenticator registered' do + it 'does not assign any two-factor authenticators' do + get :index, request_options + assigns(:two_factor_authenticators).should == [] + end + end + + context 'with an inactive two-factor authenticator' do + let!(:two_factor_authenticator) { FactoryGirl.create :two_factor_authenticator, :inactive, user: user } + + it 'does not assign any two-factor authenticators' do + get :index, request_options + assigns(:two_factor_authenticators).should == [] + end + end + + context 'with a two-factor authenticator registered' do + let(:two_factor_authenticator) { FactoryGirl.create :two_factor_authenticator, user: user } + let!(:other_two_factor_authenticator) { FactoryGirl.create :two_factor_authenticator } + + it 'does assign the two-factor authenticator' do + get :index, request_options + assigns(:two_factor_authenticators).should == [two_factor_authenticator] + end + end + end + + describe 'sessions overview' do + let!(:other_ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket } + let(:user) { other_ticket_granting_ticket.user } + + context 'as user owning the other ticket granting ticket' do + let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket, user: user } + + it 'assigns both ticket granting tickets' do + get :index, request_options + assigns(:ticket_granting_tickets).should == [ticket_granting_ticket, other_ticket_granting_ticket] + 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 'does not assign the other ticket granting ticket' do + get :index, request_options + assigns(:ticket_granting_tickets).should == [ticket_granting_ticket] + end + end + end + end + + context 'without a ticket-granting ticket' do + it 'redirects to the login page' do + get :index, request_options + response.should redirect_to(login_path) + end end end describe 'DELETE "destroy"' do - let(:id) { '123' } - let(:tgt) { 'TGT-foobar' } - 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| - params[:id].should == id - cookies[:tgt].should == tgt - user_agent.should == request.user_agent - @controller.render nothing: true - end - delete :destroy, id:id, use_route: :casino + let(:owner_ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket } + let(:user) { owner_ticket_granting_ticket.user } + + before(:each) do + sign_in(owner_ticket_granting_ticket) + end + + context 'with an existing ticket-granting ticket' do + let!(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket, user: user } + let(:service_ticket) { FactoryGirl.create :service_ticket, ticket_granting_ticket: ticket_granting_ticket } + let(:consumed_service_ticket) { FactoryGirl.create :service_ticket, :consumed, ticket_granting_ticket: ticket_granting_ticket } + let(:params) { { id: ticket_granting_ticket.id } } + + it 'deletes exactly one ticket-granting ticket' do + lambda do + delete :destroy, request_options + end.should change(CASino::TicketGrantingTicket, :count).by(-1) + end + + it 'deletes the ticket-granting ticket' do + delete :destroy, request_options + CASino::TicketGrantingTicket.where(id: params[:id]).length.should == 0 + end + + it 'redirects to the session overview' do + delete :destroy, request_options + response.should redirect_to(sessions_path) + end + end + + context 'with an invalid ticket-granting ticket' do + let(:params) { { id: 99999 } } + it 'does not delete a ticket-granting ticket' do + lambda do + delete :destroy, request_options + end.should_not change(CASino::TicketGrantingTicket, :count) + end + + it 'redirects to the session overview' do + delete :destroy, request_options + response.should redirect_to(sessions_path) + end + end + + context 'when trying to delete ticket-granting ticket of another user' do + let!(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket } + let(:params) { { id: ticket_granting_ticket.id } } + + it 'does not delete a ticket-granting ticket' do + lambda do + delete :destroy, request_options + end.should_not change(CASino::TicketGrantingTicket, :count) + end + + it 'redirects to the session overview' do + delete :destroy, request_options + response.should redirect_to(sessions_path) + end end end describe 'GET "destroy_others"' do - it 'calls the process method of the OtherSessionsDestroyer' do - CASino::OtherSessionsDestroyerProcessor.any_instance.should_receive(:process) do - @controller.render nothing: true + let(:url) { nil } + let(:params) { { :service => url } } + + 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 } + + before(:each) do + sign_in(ticket_granting_ticket) + end + + it 'deletes all other ticket-granting tickets' do + lambda do + get :destroy_others, request_options + end.should change(CASino::TicketGrantingTicket, :count).by(-3) + end + + it 'redirects to the session overview' do + get :destroy_others, request_options + response.should redirect_to(sessions_path) + end + + context 'with an URL' do + let(:url) { 'http://www.example.com' } + + it 'redirects to the service' do + get :destroy_others, request_options + response.should redirect_to(url) + end + end + end + + context 'without a ticket-granting ticket' do + context 'with an URL' do + let(:url) { 'http://www.example.com' } + + it 'redirects to the service' do + get :destroy_others, request_options + response.should redirect_to(url) + end end - get :destroy_others, use_route: :casino end end end diff --git a/spec/controllers/two_factor_authenticators_controller_spec.rb b/spec/controllers/two_factor_authenticators_controller_spec.rb index a6a49047..d167d61b 100644 --- a/spec/controllers/two_factor_authenticators_controller_spec.rb +++ b/spec/controllers/two_factor_authenticators_controller_spec.rb @@ -1,34 +1,233 @@ require 'spec_helper' describe CASino::TwoFactorAuthenticatorsController do + include CASino::Engine.routes.url_helpers + let(:params) { { } } + let(:request_options) { params.merge(use_route: :casino) } + describe 'GET "new"' do - it 'calls the process method of the TwoFactorAuthenticatorRegistrator' do - CASino::TwoFactorAuthenticatorRegistratorProcessor.any_instance.should_receive(:process) - get :new, use_route: :casino + context 'with an existing ticket-granting ticket' do + let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket } + let(:user) { ticket_granting_ticket.user } + let(:user_agent) { ticket_granting_ticket.user_agent } + + before(:each) do + sign_in(ticket_granting_ticket) + end + + it 'creates exactly one authenticator' do + lambda do + get :new, request_options + end.should change(CASino::TwoFactorAuthenticator, :count).by(1) + end + + it 'assigns the two_factor_authenticator' do + get :new, request_options + assigns(:two_factor_authenticator).should be_kind_of(CASino::TwoFactorAuthenticator) + end + + it 'creates an inactive two-factor authenticator' do + get :new, request_options + CASino::TwoFactorAuthenticator.last.should_not be_active + end + + it 'renders the new template' do + get :new, request_options + response.should render_template(:new) + end + end + + context 'without a ticket-granting ticket' do + it 'redirects to the login page' do + get :new, request_options + response.should redirect_to(login_path) + end end end describe 'POST "create"' do - it 'calls the process method of the TwoFactorAuthenticatorActivator' do - CASino::TwoFactorAuthenticatorActivatorProcessor.any_instance.should_receive(:process) do - @controller.render nothing: true + context 'with an existing ticket-granting ticket' do + let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket } + let(:user) { ticket_granting_ticket.user } + let(:id) { two_factor_authenticator.id } + let(:otp) { '123456' } + let(:params) { { otp: otp, id: id } } + + before(:each) do + sign_in(ticket_granting_ticket) + end + + context 'with an invalid authenticator' do + context 'with an expired authenticator' do + let(:two_factor_authenticator) { FactoryGirl.create :two_factor_authenticator, :inactive, user: user } + + before(:each) do + two_factor_authenticator.created_at = 10.hours.ago + two_factor_authenticator.save! + end + + it 'redirects to the two-factor authenticator new page' do + post :create, request_options + response.should redirect_to(new_two_factor_authenticator_path) + end + + it 'adds a error message' do + post :create, request_options + flash[:error].should == I18n.t('two_factor_authenticators.invalid_two_factor_authenticator') + end + end + + context 'with a authenticator of another user' do + let(:two_factor_authenticator) { FactoryGirl.create :two_factor_authenticator, :inactive } + + before(:each) do + two_factor_authenticator.created_at = 10.hours.ago + two_factor_authenticator.save! + end + + it 'redirects to the two-factor authenticator new page' do + post :create, request_options + response.should redirect_to(new_two_factor_authenticator_path) + end + end + end + + context 'with a valid authenticator' do + let(:two_factor_authenticator) { FactoryGirl.create :two_factor_authenticator, :inactive, user: user } + + context 'with a valid OTP' do + before(:each) do + ROTP::TOTP.any_instance.should_receive(:verify_with_drift).with(otp, 30).and_return(true) + end + + it 'redirects to the session overview' do + post :create, request_options + response.should redirect_to(sessions_path) + end + + it 'adds a notice' do + post :create, request_options + flash[:notice].should == I18n.t('two_factor_authenticators.successfully_activated') + end + + it 'does activate the authenticator' do + post :create, request_options + two_factor_authenticator.reload.should be_active + end + + context 'when another two-factor authenticator was active' do + let!(:other_two_factor_authenticator) { FactoryGirl.create :two_factor_authenticator, user: user } + + it 'does activate the authenticator' do + post :create, request_options + two_factor_authenticator.reload.should be_active + end + + it 'does delete the other authenticator' do + post :create, request_options + lambda do + other_two_factor_authenticator.reload + end.should raise_error(ActiveRecord::RecordNotFound) + end + end + + end + + context 'with an invalid OTP' do + before(:each) do + ROTP::TOTP.any_instance.should_receive(:verify_with_drift).with(otp, 30).and_return(false) + end + + it 'rerenders the new page' do + post :create, request_options + response.should render_template(:new) + end + + it 'adds a error message' do + post :create, request_options + flash[:error].should == I18n.t('two_factor_authenticators.invalid_one_time_password') + end + + it 'assigns the two-factor authenticator' do + post :create, request_options + assigns(:two_factor_authenticator).should be_kind_of(CASino::TwoFactorAuthenticator) + end + + it 'does not activate the authenticator' do + post :create, request_options + two_factor_authenticator.reload.should_not be_active + end + end + end + end + + context 'without a ticket-granting ticket' do + it 'redirects to the login page' do + post :create, request_options + response.should redirect_to(login_path) end - post :create, use_route: :casino end end describe 'DELETE "destroy"' do - let(:id) { '123' } - 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| - params[:id].should == id - cookies[:tgt].should == tgt - user_agent.should == request.user_agent - @controller.render nothing: true - end - delete :destroy, id:id, use_route: :casino + context 'with an existing ticket-granting ticket' do + let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket } + let(:user) { ticket_granting_ticket.user } + let(:params) { { id: two_factor_authenticator.id } } + + before(:each) do + sign_in(ticket_granting_ticket) + end + + context 'with a valid two-factor authenticator' do + let!(:two_factor_authenticator) { FactoryGirl.create :two_factor_authenticator, user: user } + let!(:other_two_factor_authenticator) { FactoryGirl.create :two_factor_authenticator } + + it 'redirects to the session overview' do + delete :destroy, request_options + response.should redirect_to(sessions_path) + end + + it 'adds a notice' do + delete :destroy, request_options + flash[:notice].should == I18n.t('two_factor_authenticators.successfully_deleted') + end + + it 'deletes the two-factor authenticator' do + delete :destroy, request_options + lambda do + two_factor_authenticator.reload + end.should raise_error(ActiveRecord::RecordNotFound) + end + + it 'does not delete other two-factor authenticators' do + lambda do + delete :destroy, request_options + end.should change(CASino::TwoFactorAuthenticator, :count).by(-1) + end + end + + context 'with a two-factor authenticator of another user' do + let!(:two_factor_authenticator) { FactoryGirl.create :two_factor_authenticator } + + it 'redirects to the session overview' do + delete :destroy, request_options + response.should redirect_to(sessions_path) + end + + it 'does not delete two-factor authenticators' do + lambda do + delete :destroy, request_options + end.should_not change(CASino::TwoFactorAuthenticator, :count) + end + end + end + + context 'without a ticket-granting ticket' do + it 'redirects to the login page' do + delete :destroy, request_options + response.should redirect_to(login_path) + end end end end diff --git a/spec/dummy/config/cas.yml b/spec/dummy/config/cas.yml index c0c27f23..2915edc4 100644 --- a/spec/dummy/config/cas.yml +++ b/spec/dummy/config/cas.yml @@ -18,6 +18,9 @@ defaults: &defaults testuser: password: "foobar123" name: "Test User" + game: + - "StarCraft 2" + - "Doto" development: <<: *defaults diff --git a/spec/dummy/config/environments/development.rb b/spec/dummy/config/environments/development.rb index 4db6d364..19920c4a 100644 --- a/spec/dummy/config/environments/development.rb +++ b/spec/dummy/config/environments/development.rb @@ -22,10 +22,6 @@ # Only use best-standards-support built into browsers config.action_dispatch.best_standards_support = :builtin - # Log the query plan for queries taking more than this (works - # with SQLite, MySQL, and PostgreSQL) - config.active_record.auto_explain_threshold_in_seconds = 0.5 - # Do not compress assets config.assets.compress = false diff --git a/spec/dummy/db/migrate/20130910094259_create_base_models.casino.rb b/spec/dummy/db/migrate/20140831214845_create_core_schema.casino.rb similarity index 50% rename from spec/dummy/db/migrate/20130910094259_create_base_models.casino.rb rename to spec/dummy/db/migrate/20140831214845_create_core_schema.casino.rb index f5102da3..23c3df74 100644 --- a/spec/dummy/db/migrate/20130910094259_create_base_models.casino.rb +++ b/spec/dummy/db/migrate/20140831214845_create_core_schema.casino.rb @@ -1,16 +1,33 @@ -# This migration comes from casino (originally 20130809135401) -class CreateBaseModels < ActiveRecord::Migration - def change - # Login Tickets - create_table :casino_login_tickets do |t| +# This migration comes from casino (originally 20130809135400) +# In order to support pre-2.0 installations of CASino that included CASinoCore, +# we must rebuild the un-namespaced CASinoCore schema so that we can upgrade +class CreateCoreSchema < ActiveRecord::Migration + CoreTables = %w{login_tickets proxy_granting_tickets proxy_tickets service_rules service_tickets ticket_granting_tickets two_factor_authenticators users} + + def up + CoreTables.each do |table_name| + if !ActiveRecord::Base.connection.table_exists? table_name + send "create_#{table_name}" + end + end + end + + def down + # No-op + # Handled by 20130809135401_rename_base_models.rb + end + + def create_login_tickets + create_table :login_tickets do |t| t.string :ticket, :null => false t.timestamps end - add_index :casino_login_tickets, :ticket, :unique => true + add_index :login_tickets, :ticket, :unique => true + end - # Proxy Granting Tickets - create_table :casino_proxy_granting_tickets do |t| + def create_proxy_granting_tickets + create_table :proxy_granting_tickets do |t| t.string :ticket, :null => false t.string :iou, :null => false t.integer :granter_id, :null => false @@ -19,12 +36,13 @@ def change t.timestamps end - add_index :casino_proxy_granting_tickets, :ticket, :unique => true - add_index :casino_proxy_granting_tickets, :iou, :unique => true - add_index :casino_proxy_granting_tickets, [:granter_type, :granter_id], :name => "index_casino_proxy_granting_tickets_on_granter", :unique => true + add_index :proxy_granting_tickets, :ticket, :unique => true + add_index :proxy_granting_tickets, :iou, :unique => true + add_index :proxy_granting_tickets, [:granter_type, :granter_id], :name => "index_proxy_granting_tickets_on_granter", :unique => true + end - # Proxy Tickets - create_table :casino_proxy_tickets do |t| + def create_proxy_tickets + create_table :proxy_tickets do |t| t.string :ticket, :null => false t.string :service, :null => false t.boolean :consumed, :default => false, :null => false @@ -32,11 +50,12 @@ def change t.timestamps end - add_index :casino_proxy_tickets, :ticket, :unique => true - add_index :casino_proxy_tickets, :proxy_granting_ticket_id + add_index :proxy_tickets, :ticket, :unique => true + add_index :proxy_tickets, :proxy_granting_ticket_id + end - # Service Rules - create_table :casino_service_rules do |t| + def create_service_rules + create_table :service_rules do |t| t.boolean :enabled, :default => true, :null => false t.integer :order, :default => 10, :null => false t.string :name, :null => false @@ -45,10 +64,11 @@ def change t.timestamps end - add_index :casino_service_rules, :url, :unique => true + add_index :service_rules, :url, :unique => true + end - # Service Tickets - create_table :casino_service_tickets do |t| + def create_service_tickets + create_table :service_tickets do |t| t.string :ticket, :null => false t.string :service, :null => false t.integer :ticket_granting_ticket_id @@ -57,11 +77,12 @@ def change t.timestamps end - add_index :casino_service_tickets, :ticket, :unique => true - add_index :casino_service_tickets, :ticket_granting_ticket_id + add_index :service_tickets, :ticket, :unique => true + add_index :service_tickets, :ticket_granting_ticket_id + end - # Ticket Granting Tickets - create_table :casino_ticket_granting_tickets do |t| + def create_ticket_granting_tickets + create_table :ticket_granting_tickets do |t| t.string :ticket, :null => false t.string :user_agent t.integer :user_id, :null => false @@ -70,26 +91,28 @@ def change t.timestamps end - add_index :casino_ticket_granting_tickets, :ticket, :unique => true + add_index :ticket_granting_tickets, :ticket, :unique => true + end - # Two Factor Authenticators - create_table :casino_two_factor_authenticators do |t| + def create_two_factor_authenticators + create_table :two_factor_authenticators do |t| t.integer :user_id, :null => false t.string :secret, :null => false t.boolean :active, :default => false, :null => false t.timestamps end - add_index :casino_two_factor_authenticators, :user_id + add_index :two_factor_authenticators, :user_id + end - # Users - create_table :casino_users do |t| + def create_users + create_table :users do |t| t.string :authenticator, :null => false t.string :username, :null => false t.text :extra_attributes t.timestamps end - add_index :casino_users, [:authenticator, :username], :unique => true + add_index :users, [:authenticator, :username], :unique => true end -end +end \ No newline at end of file diff --git a/spec/dummy/db/migrate/20140831214846_rename_base_models.casino.rb b/spec/dummy/db/migrate/20140831214846_rename_base_models.casino.rb new file mode 100644 index 00000000..c30a4f03 --- /dev/null +++ b/spec/dummy/db/migrate/20140831214846_rename_base_models.casino.rb @@ -0,0 +1,102 @@ +# This migration comes from casino (originally 20130809135401) +class RenameBaseModels < ActiveRecord::Migration + def up + # Login Tickets + rename_table :login_tickets, :casino_login_tickets + unless index_exists?(:casino_login_tickets, :ticket) + add_index :casino_login_tickets, :ticket, :unique => true + end + + # Proxy Granting Tickets + rename_table :proxy_granting_tickets, :casino_proxy_granting_tickets + unless index_exists?(:casino_proxy_granting_tickets, :ticket) + add_index :casino_proxy_granting_tickets, :ticket, :unique => true + end + unless index_exists?(:casino_proxy_granting_tickets, :iou) + add_index :casino_proxy_granting_tickets, :iou, :unique => true + end + unless index_exists?(:casino_proxy_granting_tickets, :name => "index_casino_proxy_granting_tickets_on_granter") + # Uses a custom index name because the generated one exceeds the size limit + add_index :casino_proxy_granting_tickets, [:granter_type, :granter_id], :name => "index_casino_proxy_granting_tickets_on_granter", :unique => true + end + + # Proxy Tickets + rename_table :proxy_tickets, :casino_proxy_tickets + unless index_exists?(:casino_proxy_tickets, :ticket) + add_index :casino_proxy_tickets, :ticket, :unique => true + end + unless index_exists?(:casino_proxy_tickets, :proxy_granting_ticket_id) + add_index :casino_proxy_tickets, :proxy_granting_ticket_id + end + + # Service Rules + rename_table :service_rules, :casino_service_rules + unless index_exists?(:casino_service_rules, :url) + add_index :casino_service_rules, :url, :unique => true + end + + # Service Tickets + rename_table :service_tickets, :casino_service_tickets + unless index_exists?(:casino_service_tickets, :ticket) + add_index :casino_service_tickets, :ticket, :unique => true + end + unless index_exists?(:casino_service_tickets, :ticket_granting_ticket_id) + add_index :casino_service_tickets, :ticket_granting_ticket_id + end + + # Ticket Granting Tickets + rename_table :ticket_granting_tickets, :casino_ticket_granting_tickets + unless index_exists?(:casino_ticket_granting_tickets, :ticket) + add_index :casino_ticket_granting_tickets, :ticket, :unique => true + end + + # Two-Factor Authenticators + rename_table :two_factor_authenticators, :casino_two_factor_authenticators + unless index_exists?(:casino_two_factor_authenticators, :user_id) + add_index :casino_two_factor_authenticators, :user_id + end + + # Users + rename_table :users, :casino_users + unless index_exists?(:casino_users, [:authenticator, :username]) + add_index :casino_users, [:authenticator, :username], :unique => true + end + end + + def down + remove_index :casino_login_tickets, :ticket + drop_table :casino_login_tickets + + # Proxy Granting Tickets + remove_index :casino_proxy_granting_tickets, :ticket, :unique => true + remove_index :casino_proxy_granting_tickets, :iou, :unique => true + remove_index :casino_proxy_granting_tickets, [:granter_type, :granter_id], :name => "index_casino_proxy_granting_tickets_on_granter", :unique => true + drop_table :casino_proxy_granting_tickets + + # Proxy Tickets + remove_index :casino_proxy_tickets, :ticket, :unique => true + remove_index :casino_proxy_tickets, :proxy_granting_ticket_id + drop_table :casino_proxy_tickets + + # Service Rules + remove_index :casino_service_rules, :url, :unique => true + drop_table :casino_service_rules + + # Service Tickets + remove_index :casino_service_tickets, :ticket, :unique => true + remove_index :casino_service_tickets, :ticket_granting_ticket_id + drop_table :casino_service_tickets + + # Ticket Granting Tickets + remove_index :casino_ticket_granting_tickets, :ticket, :unique => true + drop_table :casino_ticket_granting_tickets + + # Two-Factor Authenticators + remove_index :casino_two_factor_authenticators, :user_id + drop_table :casino_two_factor_authenticators + + # Users + remove_index :casino_users, [:authenticator, :username], :unique => true + drop_table :casino_users + end +end diff --git a/spec/dummy/db/migrate/20140831214847_cleanup_indexes.casino.rb b/spec/dummy/db/migrate/20140831214847_cleanup_indexes.casino.rb new file mode 100644 index 00000000..1f393f0d --- /dev/null +++ b/spec/dummy/db/migrate/20140831214847_cleanup_indexes.casino.rb @@ -0,0 +1,28 @@ +# This migration comes from casino (originally 20131022110146) +class CleanupIndexes < ActiveRecord::Migration + def change + # delete some leftovers in migrated CASino 1.x installations + remove_deprecated_index_if_exists :login_tickets, [:ticket] + remove_deprecated_index_if_exists :proxy_granting_tickets, [:granter_type, :granter_id] + remove_deprecated_index_if_exists :proxy_granting_tickets, [:iou] + remove_deprecated_index_if_exists :proxy_tickets, [:proxy_granting_ticket_id] + remove_deprecated_index_if_exists :proxy_tickets, [:ticket] + remove_deprecated_index_if_exists :service_rules, [:url] + remove_deprecated_index_if_exists :service_tickets, [:ticket] + remove_deprecated_index_if_exists :service_tickets, [:ticket_granting_ticket_id] + remove_deprecated_index_if_exists :ticket_granting_tickets, [:ticket] + remove_deprecated_index_if_exists :two_factor_authenticators, [:user_id] + remove_deprecated_index_if_exists :users, [:authenticator, :username] + end + + private + def remove_deprecated_index_if_exists(old_table_name, column_names) + table_name = :"casino_#{old_table_name}" + index_name = :"index_#{old_table_name}_on_#{column_names.join('_and_')}" + if index_name_exists?(table_name, index_name, false) + remove_index table_name, name: index_name + else + puts "index #{index_name} on #{table_name} not found" + end + end +end diff --git a/spec/dummy/db/migrate/20140831214848_fix_long_index_names.casino.rb b/spec/dummy/db/migrate/20140831214848_fix_long_index_names.casino.rb new file mode 100644 index 00000000..b90ce4f1 --- /dev/null +++ b/spec/dummy/db/migrate/20140831214848_fix_long_index_names.casino.rb @@ -0,0 +1,13 @@ +# This migration comes from casino (originally 20131022110246) +class FixLongIndexNames < ActiveRecord::Migration + def change + # Long names prevent us from doing some migrations, because the resulting + # temporary index names would be longer than 64 characters: + # Index name 'temp_index_altered_casino_proxy_tickets_on_proxy_granting_ticket_id' on table + # 'altered_casino_proxy_tickets' is too long; the limit is 64 characters + remove_index :casino_service_tickets, :ticket_granting_ticket_id + remove_index :casino_proxy_tickets, :proxy_granting_ticket_id + add_index :casino_service_tickets, :ticket_granting_ticket_id, name: 'casino_service_tickets_on_tgt_id' + add_index :casino_proxy_tickets, :proxy_granting_ticket_id, name: 'casino_proxy_tickets_on_pgt_id' + end +end diff --git a/spec/dummy/db/migrate/20140831214849_change_service_to_text.casino.rb b/spec/dummy/db/migrate/20140831214849_change_service_to_text.casino.rb new file mode 100644 index 00000000..979aa5d9 --- /dev/null +++ b/spec/dummy/db/migrate/20140831214849_change_service_to_text.casino.rb @@ -0,0 +1,7 @@ +# This migration comes from casino (originally 20131022110346) +class ChangeServiceToText < ActiveRecord::Migration + def change + change_column :casino_proxy_tickets, :service, :text + change_column :casino_service_tickets, :service, :text + end +end diff --git a/spec/dummy/db/migrate/20140831214850_change_user_agent_to_text.casino.rb b/spec/dummy/db/migrate/20140831214850_change_user_agent_to_text.casino.rb new file mode 100644 index 00000000..436a41f8 --- /dev/null +++ b/spec/dummy/db/migrate/20140831214850_change_user_agent_to_text.casino.rb @@ -0,0 +1,6 @@ +# This migration comes from casino (originally 20140821142611) +class ChangeUserAgentToText < ActiveRecord::Migration + def change + change_column :casino_ticket_granting_tickets, :user_agent, :text + end +end diff --git a/spec/dummy/db/migrate/20140831214851_fix_length_of_text_fields.casino.rb b/spec/dummy/db/migrate/20140831214851_fix_length_of_text_fields.casino.rb new file mode 100644 index 00000000..4230023e --- /dev/null +++ b/spec/dummy/db/migrate/20140831214851_fix_length_of_text_fields.casino.rb @@ -0,0 +1,8 @@ +# This migration comes from casino (originally 20140827183611) +class FixLengthOfTextFields < ActiveRecord::Migration + def change + change_column :casino_proxy_tickets, :service, :text, :limit => nil + change_column :casino_service_tickets, :service, :text, :limit => nil + change_column :casino_ticket_granting_tickets, :user_agent, :text, :limit => nil + end +end diff --git a/spec/dummy/db/migrate/20140831214852_create_auth_token_tickets.casino.rb b/spec/dummy/db/migrate/20140831214852_create_auth_token_tickets.casino.rb new file mode 100644 index 00000000..6c33660f --- /dev/null +++ b/spec/dummy/db/migrate/20140831214852_create_auth_token_tickets.casino.rb @@ -0,0 +1,11 @@ +# This migration comes from casino (originally 20140831205255) +class CreateAuthTokenTickets < ActiveRecord::Migration + def change + create_table :casino_auth_token_tickets do |t| + t.string :ticket, :null => false + + t.timestamps + end + add_index :casino_auth_token_tickets, :ticket, :unique => true + end +end diff --git a/spec/dummy/db/schema.rb b/spec/dummy/db/schema.rb index fc84778a..ab0ea663 100644 --- a/spec/dummy/db/schema.rb +++ b/spec/dummy/db/schema.rb @@ -9,99 +9,108 @@ # from scratch. The latter is a flawed and unsustainable approach (the more migrations # you'll amass, the slower it'll run and the greater likelihood for issues). # -# It's strongly recommended to check this file into your version control system. +# It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(:version => 20130910094259) do +ActiveRecord::Schema.define(version: 20140831214852) do - create_table "casino_login_tickets", :force => true do |t| - t.string "ticket", :null => false - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + create_table "casino_auth_token_tickets", force: true do |t| + t.string "ticket", null: false + t.datetime "created_at" + t.datetime "updated_at" end - add_index "casino_login_tickets", ["ticket"], :name => "index_casino_login_tickets_on_ticket", :unique => true + add_index "casino_auth_token_tickets", ["ticket"], name: "index_casino_auth_token_tickets_on_ticket", unique: true - create_table "casino_proxy_granting_tickets", :force => true do |t| - t.string "ticket", :null => false - t.string "iou", :null => false - t.integer "granter_id", :null => false - t.string "pgt_url", :null => false - t.string "granter_type", :null => false - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + create_table "casino_login_tickets", force: true do |t| + t.string "ticket", null: false + t.datetime "created_at" + t.datetime "updated_at" end - add_index "casino_proxy_granting_tickets", ["granter_type", "granter_id"], :name => "index_casino_proxy_granting_tickets_on_granter", :unique => true - add_index "casino_proxy_granting_tickets", ["iou"], :name => "index_casino_proxy_granting_tickets_on_iou", :unique => true - add_index "casino_proxy_granting_tickets", ["ticket"], :name => "index_casino_proxy_granting_tickets_on_ticket", :unique => true - - create_table "casino_proxy_tickets", :force => true do |t| - t.string "ticket", :null => false - t.string "service", :null => false - t.boolean "consumed", :default => false, :null => false - t.integer "proxy_granting_ticket_id", :null => false - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + add_index "casino_login_tickets", ["ticket"], name: "index_casino_login_tickets_on_ticket", unique: true + + create_table "casino_proxy_granting_tickets", force: true do |t| + t.string "ticket", null: false + t.string "iou", null: false + t.integer "granter_id", null: false + t.string "pgt_url", null: false + t.string "granter_type", null: false + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "casino_proxy_granting_tickets", ["granter_type", "granter_id"], name: "index_casino_proxy_granting_tickets_on_granter", unique: true + add_index "casino_proxy_granting_tickets", ["granter_type", "granter_id"], name: "index_proxy_granting_tickets_on_granter", unique: true + add_index "casino_proxy_granting_tickets", ["iou"], name: "index_casino_proxy_granting_tickets_on_iou", unique: true + add_index "casino_proxy_granting_tickets", ["ticket"], name: "index_casino_proxy_granting_tickets_on_ticket", unique: true + + create_table "casino_proxy_tickets", force: true do |t| + t.string "ticket", null: false + t.text "service", null: false + t.boolean "consumed", default: false, null: false + t.integer "proxy_granting_ticket_id", null: false + t.datetime "created_at" + t.datetime "updated_at" end - add_index "casino_proxy_tickets", ["proxy_granting_ticket_id"], :name => "index_casino_proxy_tickets_on_proxy_granting_ticket_id" - add_index "casino_proxy_tickets", ["ticket"], :name => "index_casino_proxy_tickets_on_ticket", :unique => true - - create_table "casino_service_rules", :force => true do |t| - t.boolean "enabled", :default => true, :null => false - t.integer "order", :default => 10, :null => false - t.string "name", :null => false - t.string "url", :null => false - t.boolean "regex", :default => false, :null => false - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + add_index "casino_proxy_tickets", ["proxy_granting_ticket_id"], name: "casino_proxy_tickets_on_pgt_id" + add_index "casino_proxy_tickets", ["ticket"], name: "index_casino_proxy_tickets_on_ticket", unique: true + + create_table "casino_service_rules", force: true do |t| + t.boolean "enabled", default: true, null: false + t.integer "order", default: 10, null: false + t.string "name", null: false + t.string "url", null: false + t.boolean "regex", default: false, null: false + t.datetime "created_at" + t.datetime "updated_at" end - add_index "casino_service_rules", ["url"], :name => "index_casino_service_rules_on_url", :unique => true + add_index "casino_service_rules", ["url"], name: "index_casino_service_rules_on_url", unique: true - create_table "casino_service_tickets", :force => true do |t| - t.string "ticket", :null => false - t.string "service", :null => false + create_table "casino_service_tickets", force: true do |t| + t.string "ticket", null: false + t.text "service", null: false t.integer "ticket_granting_ticket_id" - t.boolean "consumed", :default => false, :null => false - t.boolean "issued_from_credentials", :default => false, :null => false - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.boolean "consumed", default: false, null: false + t.boolean "issued_from_credentials", default: false, null: false + t.datetime "created_at" + t.datetime "updated_at" end - add_index "casino_service_tickets", ["ticket"], :name => "index_casino_service_tickets_on_ticket", :unique => true - add_index "casino_service_tickets", ["ticket_granting_ticket_id"], :name => "index_casino_service_tickets_on_ticket_granting_ticket_id" - - create_table "casino_ticket_granting_tickets", :force => true do |t| - t.string "ticket", :null => false - t.string "user_agent" - t.integer "user_id", :null => false - t.boolean "awaiting_two_factor_authentication", :default => false, :null => false - t.boolean "long_term", :default => false, :null => false - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + add_index "casino_service_tickets", ["ticket"], name: "index_casino_service_tickets_on_ticket", unique: true + add_index "casino_service_tickets", ["ticket_granting_ticket_id"], name: "casino_service_tickets_on_tgt_id" + + create_table "casino_ticket_granting_tickets", force: true do |t| + t.string "ticket", null: false + t.text "user_agent" + t.integer "user_id", null: false + t.boolean "awaiting_two_factor_authentication", default: false, null: false + t.boolean "long_term", default: false, null: false + t.datetime "created_at" + t.datetime "updated_at" end - add_index "casino_ticket_granting_tickets", ["ticket"], :name => "index_casino_ticket_granting_tickets_on_ticket", :unique => true + add_index "casino_ticket_granting_tickets", ["ticket"], name: "index_casino_ticket_granting_tickets_on_ticket", unique: true - create_table "casino_two_factor_authenticators", :force => true do |t| - t.integer "user_id", :null => false - t.string "secret", :null => false - t.boolean "active", :default => false, :null => false - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + create_table "casino_two_factor_authenticators", force: true do |t| + t.integer "user_id", null: false + t.string "secret", null: false + t.boolean "active", default: false, null: false + t.datetime "created_at" + t.datetime "updated_at" end - add_index "casino_two_factor_authenticators", ["user_id"], :name => "index_casino_two_factor_authenticators_on_user_id" + add_index "casino_two_factor_authenticators", ["user_id"], name: "index_casino_two_factor_authenticators_on_user_id" - create_table "casino_users", :force => true do |t| - t.string "authenticator", :null => false - t.string "username", :null => false + create_table "casino_users", force: true do |t| + t.string "authenticator", null: false + t.string "username", null: false t.text "extra_attributes" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at" + t.datetime "updated_at" end - add_index "casino_users", ["authenticator", "username"], :name => "index_casino_users_on_authenticator_and_username", :unique => true + add_index "casino_users", ["authenticator", "username"], name: "index_casino_users_on_authenticator_and_username", unique: true end diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index 960d07ef..ffd92c85 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -65,13 +65,4 @@ it { should have_button('Login') } it { should have_text('Incorrect username or password') } end - - context 'with german locale' do - before do - page.driver.header 'Accept-Language', 'de' - visit login_path - end - - it { should have_text('Benutzername') } - end end diff --git a/spec/model/auth_token_ticket_spec.rb b/spec/model/auth_token_ticket_spec.rb new file mode 100644 index 00000000..b883b81a --- /dev/null +++ b/spec/model/auth_token_ticket_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe CASino::AuthTokenTicket do + describe '.cleanup' do + it 'deletes expired auth token tickets' do + ticket = described_class.new ticket: 'ATT-12345' + ticket.save! + ticket.created_at = 10.minutes.ago + ticket.save! + lambda do + described_class.cleanup + end.should change(described_class, :count).by(-1) + described_class.find_by_ticket('ATT-12345').should be_falsey + end + end + + describe '#to_s' do + it 'returns the ticket identifier' do + ticket = described_class.new ticket: 'ATT-12345' + "#{ticket}".should == ticket.ticket + end + end +end diff --git a/spec/processor/api/login_credential_acceptor_spec.rb b/spec/processor/api/login_credential_acceptor_spec.rb deleted file mode 100644 index ed5432b3..00000000 --- a/spec/processor/api/login_credential_acceptor_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'spec_helper' - -describe CASino::API::LoginCredentialAcceptorProcessor do - describe '#process' do - let(:listener) { Object.new } - let(:processor) { described_class.new(listener) } - let(:user_agent) { 'ThisIsATestBrwoser 1.0' } - - context 'with invalid credentials' do - let(:login_data) { { username: 'testuser', password: 'wrong' } } - - before(:each) do - listener.stub(:invalid_login_credentials_via_api) - end - - it 'calls the #invalid_login_credentials_via_api method on the listener' do - listener.should_receive(:invalid_login_credentials_via_api) - processor.process(login_data, user_agent).should be_falsey - end - - it 'does not generate a ticket-granting ticket' do - expect { - processor.process(login_data, user_agent) - }.to_not change(CASino::TicketGrantingTicket, :count) - end - end - - context 'with valid credentials' do - let(:login_data) { { username: 'testuser', password: 'foobar123' } } - - before(:each) do - listener.stub(:user_logged_in_via_api) - end - - it 'calls the #user_logged_in_via_api method on the listener' do - listener.should_receive(:user_logged_in_via_api).with(/^TGC\-/) - processor.process(login_data, user_agent) - end - - it 'generates a ticket-granting ticket' do - expect { - processor.process(login_data, user_agent) - }.to change(CASino::TicketGrantingTicket, :count).by(1) - end - - it 'sets the user-agent in the ticket-granting ticket' do - processor.process(login_data, user_agent) - CASino::TicketGrantingTicket.last.user_agent.should == user_agent - end - end - end -end diff --git a/spec/processor/api/logout_spec.rb b/spec/processor/api/logout_spec.rb deleted file mode 100644 index 50ce1433..00000000 --- a/spec/processor/api/logout_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'spec_helper' - -describe CASino::API::LogoutProcessor do - describe '#process' do - let(:listener) { Object.new } - let(:processor) { described_class.new(listener) } - - context 'with an existing ticket-granting ticket' do - let(:ticket_granting_ticket) { FactoryGirl.create(:ticket_granting_ticket) } - let(:user_agent) { ticket_granting_ticket.user_agent } - - it 'deletes the ticket-granting ticket' do - listener.should_receive(:user_logged_out_via_api) - processor.process(ticket_granting_ticket.ticket, user_agent) - CASino::TicketGrantingTicket.where(id: ticket_granting_ticket.id).first.should == nil - end - - it 'calls the #user_logged_out_via_api method on the listener' do - listener.should_receive(:user_logged_out_via_api) - processor.process(ticket_granting_ticket, user_agent) - end - - end - - context 'with an invalid 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_via_api) - processor.process(tgt) - end - end - end -end diff --git a/spec/processor/api/service_ticket_provider_spec.rb b/spec/processor/api/service_ticket_provider_spec.rb deleted file mode 100644 index 489d2064..00000000 --- a/spec/processor/api/service_ticket_provider_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -require 'spec_helper' - -describe CASino::API::ServiceTicketProviderProcessor do - describe '#process' do - let(:listener) { Object.new } - let(:processor) { described_class.new(listener) } - - let(:service) { 'http://example.org/' } - let(:parameters) { { service: service } } - - context 'with an invalid ticket-granting ticket' do - let(:ticket_granting_ticket) { 'TGT-INVALID' } - - it 'calls the #invalid_tgt_via_api method on the listener' do - listener.should_receive(:invalid_ticket_granting_ticket_via_api) - processor.process(ticket_granting_ticket, parameters).should be_falsey - end - end - - context 'with a valid ticket-granting ticket' do - let(:ticket_granting_ticket) { FactoryGirl.create(:ticket_granting_ticket) } - let(:ticket) { ticket_granting_ticket.ticket } - let(:user_agent) { ticket_granting_ticket.user_agent } - - context 'with a not allowed service' do - before(:each) do - FactoryGirl.create :service_rule, :regex, url: '^https://.*' - end - let(:service) { 'http://www.example.org/' } - - it 'calls the #service_not_allowed method on the listener' do - listener.should_receive(:service_not_allowed_via_api).with(service) - processor.process(ticket, parameters, user_agent) - end - end - - it 'calls the #granted_service_ticket_via_api method on the listener' do - listener.should_receive(:granted_service_ticket_via_api).with(/^ST\-/) - processor.process(ticket, parameters, user_agent) - end - - it 'generates a ticket-granting ticket' do - listener.should_receive(:granted_service_ticket_via_api).with(/^ST\-/) - expect { - processor.process(ticket, parameters, user_agent) - }.to change(CASino::ServiceTicket, :count).by(1) - end - - context 'without a service' do - let(:parameters) { { } } - - it 'calls the #no_service_provided_via_api method on the listener' do - listener.should_receive(:no_service_provided_via_api) - processor.process(ticket, parameters, user_agent) - end - end - - end - end -end - diff --git a/spec/processor/legacy_validator_spec.rb b/spec/processor/legacy_validator_spec.rb deleted file mode 100644 index 4a004615..00000000 --- a/spec/processor/legacy_validator_spec.rb +++ /dev/null @@ -1,78 +0,0 @@ -require 'spec_helper' - -describe CASino::LegacyValidatorProcessor do - describe '#process' do - let(:listener) { Object.new } - let(:processor) { described_class.new(listener) } - let(:service_ticket) { FactoryGirl.create :service_ticket } - let(:parameters) { { service: service_ticket.service, ticket: service_ticket.ticket }} - let(:username) { service_ticket.ticket_granting_ticket.user.username } - - before(:each) do - listener.stub(:validation_failed) - listener.stub(:validation_succeeded) - end - - context 'with an unconsumed service ticket' do - context 'without renew flag' do - it 'consumes the service ticket' do - processor.process(parameters) - service_ticket.reload - service_ticket.consumed.should == true - end - - it 'calls the #validation_succeeded method on the listener' do - listener.should_receive(:validation_succeeded).with("yes\n#{username}\n") - processor.process(parameters) - end - end - - context 'with renew flag' do - let(:parameters_with_renew) { parameters.merge renew: 'true' } - - context 'with a service ticket without issued_from_credentials flag' do - it 'consumes the service ticket' do - processor.process(parameters_with_renew) - service_ticket.reload - service_ticket.consumed.should == true - end - - it 'calls the #validation_failed method on the listener' do - listener.should_receive(:validation_failed).with("no\n\n") - processor.process(parameters_with_renew) - end - end - - context 'with a service ticket with issued_from_credentials flag' do - before(:each) do - service_ticket.issued_from_credentials = true - service_ticket.save! - end - - it 'consumes the service ticket' do - processor.process(parameters_with_renew) - service_ticket.reload - service_ticket.consumed.should == true - end - - it 'calls the #validation_succeeded method on the listener' do - listener.should_receive(:validation_succeeded).with("yes\n#{username}\n") - processor.process(parameters_with_renew) - end - end - end - end - - context 'with a consumed service ticket' do - before(:each) do - service_ticket.consumed = true - service_ticket.save! - end - - it 'calls the #validation_failed method on the listener' do - listener.should_receive(:validation_failed).with("no\n\n") - processor.process(parameters) - end - end - end -end diff --git a/spec/processor/login_credential_acceptor_spec.rb b/spec/processor/login_credential_acceptor_spec.rb deleted file mode 100644 index 56c845c5..00000000 --- a/spec/processor/login_credential_acceptor_spec.rb +++ /dev/null @@ -1,164 +0,0 @@ -require 'spec_helper' - -describe CASino::LoginCredentialAcceptorProcessor do - describe '#process' do - let(:listener) { Object.new } - let(:processor) { described_class.new(listener) } - - 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)) - processor.process - end - end - - context 'with an expired login ticket' do - 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)) - processor.process(lt: expired_login_ticket.ticket) - 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)) - processor.process(lt: login_ticket.ticket) - end - end - - context 'with valid credentials' do - let(:service) { 'https://www.example.org' } - let(:username) { 'testuser' } - let(:authenticator) { 'static' } - let(:login_data) { { lt: login_ticket.ticket, username: username, password: 'foobar123', service: service } } - - before(:each) do - listener.stub(:user_logged_in) - end - - context 'with rememberMe set' do - let(:login_data_with_remember_me) { login_data.merge(rememberMe: true) } - - 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(login_data_with_remember_me) - end - - it 'creates a long-term ticket-granting ticket' do - processor.process(login_data_with_remember_me) - tgt = CASino::TicketGrantingTicket.last - tgt.long_term.should == true - end - end - - context 'with two-factor authentication enabled' do - let!(:user) { CASino::User.create! username: username, authenticator: authenticator } - let!(:two_factor_authenticator) { FactoryGirl.create :two_factor_authenticator, user: user } - - it 'calls the `#two_factor_authentication_pending` method on the listener' do - listener.should_receive(:two_factor_authentication_pending).with(/^TGC\-/) - processor.process(login_data) - end - end - - context 'with a not allowed service' do - before(:each) do - FactoryGirl.create :service_rule, :regex, url: '^https://.*' - end - let(:service) { 'http://www.example.org/' } - - it 'calls the #service_not_allowed method on the listener' do - listener.should_receive(:service_not_allowed).with(service) - processor.process(login_data) - end - end - - context 'when all authenticators raise an error' do - before(:each) do - CASino::StaticAuthenticator.any_instance.stub(:validate) do - raise CASino::Authenticator::AuthenticatorError, 'error123' - end - end - - it 'calls the #invalid_login_credentials method on the listener' do - listener.should_receive(:invalid_login_credentials).with(kind_of(CASino::LoginTicket)) - processor.process(login_data) - end - end - - context 'without a service' do - let(:service) { nil } - - it 'calls the #user_logged_in method on the listener' do - listener.should_receive(:user_logged_in).with(nil, /^TGC\-/) - processor.process(login_data) - end - - it 'generates a ticket-granting ticket' do - lambda do - processor.process(login_data) - end.should change(CASino::TicketGrantingTicket, :count).by(1) - end - - context 'when the user does not exist yet' do - it 'generates exactly one user' do - lambda do - processor.process(login_data) - end.should change(CASino::User, :count).by(1) - end - - it 'sets the users attributes' do - processor.process(login_data) - user = CASino::User.last - user.username.should == username - user.authenticator.should == authenticator - end - end - - context 'when the user already exists' do - it 'does not regenerate the user' do - CASino::User.create! username: username, authenticator: authenticator - lambda do - processor.process(login_data) - end.should_not change(CASino::User, :count) - end - - it 'updates the extra attributes' do - user = CASino::User.create! username: username, authenticator: authenticator - lambda do - processor.process(login_data) - user.reload - end.should change(user, :extra_attributes) - end - end - end - - context 'with a service' do - let(:service) { 'https://www.example.com' } - - it 'calls the #user_logged_in method on the listener' do - listener.should_receive(:user_logged_in).with(/^#{service}\/\?ticket=ST\-/, /^TGC\-/) - processor.process(login_data) - end - - it 'generates a service ticket' do - lambda do - processor.process(login_data) - end.should change(CASino::ServiceTicket, :count).by(1) - end - - it 'generates a ticket-granting ticket' do - lambda do - processor.process(login_data) - end.should change(CASino::TicketGrantingTicket, :count).by(1) - end - end - end - end - end -end diff --git a/spec/processor/login_credential_requestor_spec.rb b/spec/processor/login_credential_requestor_spec.rb deleted file mode 100644 index 379d6afe..00000000 --- a/spec/processor/login_credential_requestor_spec.rb +++ /dev/null @@ -1,145 +0,0 @@ -require 'spec_helper' - -describe CASino::LoginCredentialRequestorProcessor do - describe '#process' do - let(:listener) { Object.new } - let(:processor) { described_class.new(listener) } - - context 'with a not allowed service' do - before(:each) do - FactoryGirl.create :service_rule, :regex, url: '^https://.*' - end - let(:service) { 'http://www.example.org/' } - let(:params) { { service: service } } - - it 'calls the #service_not_allowed method on the listener' do - listener.should_receive(:service_not_allowed).with(service) - processor.process(params) - end - end - - 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)) - processor.process - end - - context 'with gateway parameter' do - context 'with a service' do - let(:service) { 'http://example.com/' } - let(:params) { { service: service, gateway: 'true' } } - - it 'calls the #user_logged_in method on the listener' do - listener.should_receive(:user_logged_in).with(service) - processor.process(params) - end - end - - context 'without a service' do - 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)) - processor.process - end - end - end - end - - context 'when logged in' do - let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket } - 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 - end - - context 'with a service' do - let(:service) { 'http://example.com/' } - let(:params) { { service: service } } - - 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) - end - - it 'generates a service ticket' do - lambda do - processor.process(params, cookies, user_agent) - end.should change(CASino::ServiceTicket, :count).by(1) - end - - 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)) - processor.process(params.merge({ renew: 'true' }), cookies) - end - end - end - - context 'with a service with nested attributes' do - let(:service) { 'http://example.com/?a%5B%5D=test&a%5B%5D=example' } - let(:params) { { service: service } } - - 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) - end - end - - context 'with a broken service' do - let(:service) { '%3Atest' } - let(:params) { { service: service } } - - it 'calls the #user_logged_in method on the listener' do - listener.should_receive(:user_logged_in).with(nil) - processor.process(params, cookies, 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) - end - - it 'does not generate a service ticket' do - lambda do - processor.process(nil, cookies, user_agent) - 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 -end diff --git a/spec/processor/logout_other_sessions_spec.rb b/spec/processor/logout_other_sessions_spec.rb deleted file mode 100644 index 4bc80a81..00000000 --- a/spec/processor/logout_other_sessions_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -require 'spec_helper' - -describe CASino::OtherSessionsDestroyerProcessor do - describe '#process' do - let(:listener) { Object.new } - let(:processor) { described_class.new(listener) } - let(:cookies) { { tgt: tgt } } - let(:url) { nil } - let(:params) { { :service => url } unless url.nil? } - - before(:each) do - listener.stub(:other_sessions_destroyed) - 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 } - let(:tgt) { ticket_granting_ticket.ticket } - let(:user_agent) { ticket_granting_ticket.user_agent } - - it 'deletes all other ticket-granting tickets' do - lambda do - processor.process(params, cookies, 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) - end - - context 'with an URL' do - let(:url) { 'http://www.example.com' } - - 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) - end - end - end - - context 'with an invlaid ticket-granting ticket' do - let(:tgt) { 'TGT-lalala' } - - it 'calls the #other_sessions_destroyed method on the listener' do - listener.should_receive(:other_sessions_destroyed).with(nil) - processor.process(params, cookies) - end - end - end -end diff --git a/spec/processor/logout_spec.rb b/spec/processor/logout_spec.rb deleted file mode 100644 index 5ff186d1..00000000 --- a/spec/processor/logout_spec.rb +++ /dev/null @@ -1,72 +0,0 @@ -require 'spec_helper' - -describe CASino::LogoutProcessor do - describe '#process' do - let(:listener) { Object.new } - let(:processor) { described_class.new(listener) } - let(:cookies) { { tgt: tgt } } - 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(:user_agent) { ticket_granting_ticket.user_agent } - - it 'deletes the ticket-granting ticket' do - processor.process(params, cookies, 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) - end - - context 'with an URL' do - let(:url) { 'http://www.example.com' } - - 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) - end - end - - context 'with a service' do - let(:params) { { :service => url } } - let(:url) { 'http://www.example.org' } - - 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) - end - end - - context '(not whitelisted)' do - before(:each) do - FactoryGirl.create :service_rule, :regex, url: '^https://.*' - end - - 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) - 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/processor_concern/service_tickets_spec.rb b/spec/processor/processor_concern/service_tickets_spec.rb deleted file mode 100644 index 301d6d94..00000000 --- a/spec/processor/processor_concern/service_tickets_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -require 'spec_helper' - -describe CASino::ProcessorConcern::ServiceTickets do - let(:class_with_mixin) { - Class.new do - include CASino::ProcessorConcern::ServiceTickets - end - } - subject { - class_with_mixin.new - } - - describe '#acquire_service_ticket' do - let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket } - let(:service) { 'http://www.example.com/' } - - context 'with a ticket-granting ticket with existing service tickets' do - let!(:service_ticket) { FactoryGirl.create :service_ticket, ticket_granting_ticket: ticket_granting_ticket, service: service } - let!(:other_service_ticket) { FactoryGirl.create :service_ticket, ticket_granting_ticket: ticket_granting_ticket } - - it 'does not change the service tickets count' do - expect do - subject.acquire_service_ticket(ticket_granting_ticket, service) - end.to_not change(CASino::ServiceTicket, :count) - end - - it 'deletes the old service ticket' do - subject.acquire_service_ticket(ticket_granting_ticket, service) - expect { service_ticket.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - end - - context 'with a service url another ticket-granting ticket has a service ticket for' do - let!(:service_ticket) { FactoryGirl.create :service_ticket, ticket_granting_ticket: ticket_granting_ticket, service: service } - let!(:other_ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket } - - it 'does change the service tickets count' do - expect do - subject.acquire_service_ticket(other_ticket_granting_ticket, service) - end.to change(CASino::ServiceTicket, :count).by(1) - end - - it 'does not delete the other service ticket' do - subject.acquire_service_ticket(other_ticket_granting_ticket, service) - expect { service_ticket.reload }.not_to raise_error - end - end - end -end diff --git a/spec/processor/proxy_ticket_provider_spec.rb b/spec/processor/proxy_ticket_provider_spec.rb deleted file mode 100644 index 6c8aa2ca..00000000 --- a/spec/processor/proxy_ticket_provider_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -require 'spec_helper' - -describe CASino::ProxyTicketProviderProcessor do - describe '#process' do - let(:listener) { Object.new } - let(:processor) { described_class.new(listener) } - let(:params) { { targetService: 'this_does_not_have_to_be_a_url' } } - - before(:each) do - listener.stub(:request_failed) - listener.stub(:request_succeeded) - end - - context 'without proxy-granting ticket' do - it 'calls the #request_failed method on the listener' do - listener.should_receive(:request_failed) - processor.process(params) - end - - it 'does not create a proxy ticket' do - lambda do - processor.process(params) - end.should_not change(CASino::ProxyTicket, :count) - end - end - - context 'with a not-existing proxy-granting ticket' do - let(:params_with_deleted_pgt) { params.merge(pgt: 'PGT-123453789') } - - it 'calls the #request_failed method on the listener' do - listener.should_receive(:request_failed) - processor.process(params_with_deleted_pgt) - end - - it 'does not create a proxy ticket' do - lambda do - processor.process(params_with_deleted_pgt) - end.should_not change(CASino::ProxyTicket, :count) - end - end - - context 'with a proxy-granting ticket' do - let(:proxy_granting_ticket) { FactoryGirl.create :proxy_granting_ticket } - let(:params_with_valid_pgt) { params.merge(pgt: proxy_granting_ticket.ticket) } - - it 'calls the #request_succeeded method on the listener' do - listener.should_receive(:request_succeeded) - processor.process(params_with_valid_pgt) - end - - it 'does not create a proxy ticket' do - lambda do - processor.process(params_with_valid_pgt) - end.should change(proxy_granting_ticket.proxy_tickets, :count).by(1) - end - - it 'includes the proxy ticket in the response' do - listener.should_receive(:request_succeeded) do |response| - proxy_ticket = CASino::ProxyTicket.last - response.should =~ /#{proxy_ticket.ticket}<\/cas:proxyTicket>/ - end - processor.process(params_with_valid_pgt) - end - end - end -end diff --git a/spec/processor/proxy_ticket_validator_spec.rb b/spec/processor/proxy_ticket_validator_spec.rb deleted file mode 100644 index 2da4c504..00000000 --- a/spec/processor/proxy_ticket_validator_spec.rb +++ /dev/null @@ -1,65 +0,0 @@ -require 'spec_helper' - -describe CASino::ProxyTicketValidatorProcessor do - let(:listener) { Object.new } - let(:processor) { described_class.new(listener) } - - describe '#process' do - let(:regex_success) { /\A\s*#{proxy_ticket.proxy_granting_ticket.pgt_url}<\/cas:proxy>\s*<\/cas:proxies>/ } - - it 'calls the #validation_succeeded method on the listener' do - listener.should_receive(:validation_succeeded).with(regex_success) - processor.process(parameters) - end - - it 'includes the proxy in the response' do - listener.should_receive(:validation_succeeded).with(regex_proxy) - processor.process(parameters) - end - - context 'with an expired proxy ticket' do - before(:each) do - CASino::ProxyTicket.any_instance.stub(:expired?).and_return(true) - end - - it 'calls the #validation_failed method on the listener' do - listener.should_receive(:validation_failed) - processor.process(parameters) - end - end - - context 'with an other service' do - let(:parameters_with_other_service) { parameters.merge(service: 'this_is_another_service') } - - it 'calls the #validation_failed method on the listener' do - listener.should_receive(:validation_failed) - processor.process(parameters_with_other_service) - end - end - - context 'without an existing ticket' do - let(:parameters_without_existing_ticket) { { ticket: 'PT-1234', service: 'https://www.example.com/' } } - - it 'calls the #validation_failed method on the listener' do - listener.should_receive(:validation_failed) - processor.process(parameters_without_existing_ticket) - end - end - end - end -end diff --git a/spec/processor/second_factor_authenticaton_acceptor_spec.rb b/spec/processor/second_factor_authenticaton_acceptor_spec.rb deleted file mode 100644 index f0acd207..00000000 --- a/spec/processor/second_factor_authenticaton_acceptor_spec.rb +++ /dev/null @@ -1,94 +0,0 @@ -require 'spec_helper' - -describe CASino::SecondFactorAuthenticationAcceptorProcessor do - describe '#process' do - let(:listener) { Object.new } - 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 } - let(:tgt) { ticket_granting_ticket.ticket } - let(:user_agent) { ticket_granting_ticket.user_agent } - let(:otp) { '123456' } - let(:service) { 'http://www.example.com/testing' } - let(:params) { { tgt: tgt, otp: otp, service: service }} - - context 'with an active authenticator' do - let!(:two_factor_authenticator) { FactoryGirl.create :two_factor_authenticator, user: user } - - context 'with a valid OTP' do - before(:each) do - ROTP::TOTP.any_instance.should_receive(:verify_with_drift).with(otp, 30).and_return(true) - 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) - end - - it 'does activate the ticket-granting ticket' do - processor.process(params, user_agent) - ticket_granting_ticket.reload - ticket_granting_ticket.should_not be_awaiting_two_factor_authentication - end - - context 'with a long-term ticket-granting ticket' do - before(:each) do - ticket_granting_ticket.update_attributes! long_term: true - end - - 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) - end - end - - context 'with a not allowed service' do - before(:each) do - FactoryGirl.create :service_rule, :regex, url: '^https://.*' - end - let(:service) { 'http://www.example.org/' } - - 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) - end - end - end - - context 'with an invalid OTP' do - before(:each) do - ROTP::TOTP.any_instance.should_receive(:verify_with_drift).with(otp, 30).and_return(false) - end - - 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) - end - - it 'does not activate the ticket-granting ticket' do - processor.process(params, user_agent) - ticket_granting_ticket.reload - ticket_granting_ticket.should be_awaiting_two_factor_authentication - end - end - end - end - - context 'with an invalid ticket-granting ticket' do - let(:tgt) { 'TGT-lalala' } - 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({tgt: tgt}, user_agent) - end - end - end -end \ No newline at end of file diff --git a/spec/processor/session_destroyer_spec.rb b/spec/processor/session_destroyer_spec.rb deleted file mode 100644 index 1a08cbc1..00000000 --- a/spec/processor/session_destroyer_spec.rb +++ /dev/null @@ -1,75 +0,0 @@ -require 'spec_helper' - -describe CASino::SessionDestroyerProcessor do - describe '#process' do - let(:listener) { Object.new } - 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 } - let(:service_ticket) { FactoryGirl.create :service_ticket, ticket_granting_ticket: ticket_granting_ticket } - let(:consumed_service_ticket) { FactoryGirl.create :service_ticket, :consumed, ticket_granting_ticket: ticket_granting_ticket } - let(:params) { { id: ticket_granting_ticket.id } } - - it 'deletes exactly one ticket-granting ticket' do - ticket_granting_ticket - owner_ticket_granting_ticket - lambda do - processor.process(params, cookies, user_agent) - end.should change(CASino::TicketGrantingTicket, :count).by(-1) - end - - it 'deletes the ticket-granting ticket' do - processor.process(params, cookies, 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) - 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 - lambda do - processor.process(params, cookies, 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) - end - end - - context 'when trying to delete ticket-granting ticket of another user' do - let(:ticket_granting_ticket) { FactoryGirl.create :ticket_granting_ticket } - let(:params) { { id: ticket_granting_ticket.id } } - - it 'does not delete a ticket-granting ticket' do - owner_ticket_granting_ticket - ticket_granting_ticket - lambda do - processor.process(params, cookies, 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) - end - end - end -end \ No newline at end of file diff --git a/spec/processor/session_overview_spec.rb b/spec/processor/session_overview_spec.rb deleted file mode 100644 index 1e48c53c..00000000 --- a/spec/processor/session_overview_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -require 'spec_helper' - -describe CASino::SessionOverviewProcessor do - describe '#process' do - let(:listener) { Object.new } - 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 } - 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) - 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) - end - end - - context 'with an invalid ticket-granting ticket' do - let(:tgt) { 'TGT-lalala' } - 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) - end - end - end -end \ No newline at end of file diff --git a/spec/processor/ticket_validator_spec.rb b/spec/processor/ticket_validator_spec.rb deleted file mode 100644 index 2c3bfcb5..00000000 --- a/spec/processor/ticket_validator_spec.rb +++ /dev/null @@ -1,214 +0,0 @@ -require 'spec_helper' - -[CASino::ServiceTicketValidatorProcessor, CASino::ProxyTicketValidatorProcessor].each do |class_under_test| - describe class_under_test do - describe '#process' do - let(:listener) { Object.new } - let(:processor) { described_class.new(listener) } - let(:service_ticket) { FactoryGirl.create :service_ticket } - let(:parameters) { { service: service_ticket.service, ticket: service_ticket.ticket }} - let(:regex_failure) { /\A\ 1234 }) - end - - after(:each) do - CASino::User.any_instance.unstub(:extra_attributes) - end - - it 'includes the extra attributes' do - listener.should_receive(:validation_succeeded).with(/1234<\/cas\:id\>/) - processor.process(parameters) - end - end - - context 'issued from a long_term ticket-granting ticket' do - before(:each) do - tgt = service_ticket.ticket_granting_ticket - tgt.long_term = true - tgt.save! - end - - it 'calls the #validation_succeeded method on the listener' do - listener.should_receive(:validation_succeeded).with( - /true<\/cas\:longTermAuthenticationRequestTokenUsed>/ - ) - processor.process(parameters) - end - end - - context 'without renew flag' do - it 'consumes the service ticket' do - processor.process(parameters) - service_ticket.reload - service_ticket.consumed.should == true - end - - it 'calls the #validation_succeeded method on the listener' do - listener.should_receive(:validation_succeeded).with(regex_success) - processor.process(parameters) - end - end - - context 'with empty query values' do - it 'calls the #validation_succeeded method on the listener' do - listener.should_receive(:validation_succeeded).with(regex_success) - processor.process(parameters.merge(service: "#{service_ticket.service}/?")) - end - end - - context 'with renew flag' do - let(:parameters_with_renew) { parameters.merge renew: 'true' } - - context 'with a service ticket without issued_from_credentials flag' do - it 'consumes the service ticket' do - processor.process(parameters_with_renew) - service_ticket.reload - service_ticket.consumed.should == true - end - - it 'calls the #validation_failed method on the listener' do - listener.should_receive(:validation_failed).with(regex_failure) - processor.process(parameters_with_renew) - end - end - - context 'with a service ticket with issued_from_credentials flag' do - before(:each) do - service_ticket.issued_from_credentials = true - service_ticket.save! - end - - it 'consumes the service ticket' do - processor.process(parameters_with_renew) - service_ticket.reload - service_ticket.consumed.should == true - end - - it 'calls the #validation_succeeded method on the listener' do - listener.should_receive(:validation_succeeded).with(regex_success) - processor.process(parameters_with_renew) - end - end - end - - context 'with proxy-granting ticket callback server' do - let(:pgt_url) { 'https://www.example.org' } - let(:parameters_with_pgt_url) { parameters.merge pgtUrl: pgt_url } - - before(:each) do - stub_request(:get, /#{pgt_url}\/\?pgtId=[^&]+&pgtIou=[^&]+/) - end - - context 'not using https' do - let(:pgt_url) { 'http://www.example.org' } - - it 'calls the #validation_succeeded method on the listener' do - listener.should_receive(:validation_succeeded).with(regex_success) - processor.process(parameters_with_pgt_url) - end - - it 'does not create a proxy-granting ticket' do - lambda do - processor.process(parameters_with_pgt_url) - end.should_not change(service_ticket.proxy_granting_tickets, :count) - end - end - - it 'calls the #validation_succeeded method on the listener' do - listener.should_receive(:validation_succeeded).with(regex_success) - processor.process(parameters_with_pgt_url) - end - - it 'includes the PGTIOU in the response' do - listener.should_receive(:validation_succeeded).with(/\\n?\s*PGTIOU-.+/) - processor.process(parameters_with_pgt_url) - end - - it 'creates a proxy-granting ticket' do - lambda do - processor.process(parameters_with_pgt_url) - end.should change(service_ticket.proxy_granting_tickets, :count).by(1) - end - - it 'contacts the callback server' do - processor.process(parameters_with_pgt_url) - proxy_granting_ticket = CASino::ProxyGrantingTicket.last - WebMock.should have_requested(:get, 'https://www.example.org').with(query: { - pgtId: proxy_granting_ticket.ticket, - pgtIou: proxy_granting_ticket.iou - }) - end - - context 'when callback server gives an error' do - before(:each) do - stub_request(:get, /#{pgt_url}.*/).to_return status: 404 - end - - it 'calls the #validation_succeeded method on the listener' do - listener.should_receive(:validation_succeeded).with(regex_success) - processor.process(parameters_with_pgt_url) - end - - it 'does not create a proxy-granting ticket' do - lambda do - processor.process(parameters_with_pgt_url) - end.should_not change(service_ticket.proxy_granting_tickets, :count) - end - end - - context 'when callback server is unreachable' do - before(:each) do - stub_request(:get, /#{pgt_url}.*/).to_raise(Timeout::Error) - end - - it 'calls the #validation_succeeded method on the listener' do - listener.should_receive(:validation_succeeded).with(regex_success) - processor.process(parameters_with_pgt_url) - end - - it 'does not create a proxy-granting ticket' do - lambda do - processor.process(parameters_with_pgt_url) - end.should_not change(service_ticket.proxy_granting_tickets, :count) - end - end - end - end - - context 'with a consumed service ticket' do - before(:each) do - service_ticket.consumed = true - service_ticket.save! - end - - it 'calls the #validation_failed method on the listener' do - listener.should_receive(:validation_failed).with(regex_failure) - processor.process(parameters) - end - end - end - end -end diff --git a/spec/processor/two_factor_authenticator_activator_spec.rb b/spec/processor/two_factor_authenticator_activator_spec.rb deleted file mode 100644 index bf106b39..00000000 --- a/spec/processor/two_factor_authenticator_activator_spec.rb +++ /dev/null @@ -1,122 +0,0 @@ -require 'spec_helper' - -describe CASino::TwoFactorAuthenticatorActivatorProcessor do - describe '#process' do - let(:listener) { Object.new } - 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' } - let(:params) { { otp: otp, id: id } } - - context 'with an invalid authenticator' do - context 'with an expired authenticator' do - let(:two_factor_authenticator) { FactoryGirl.create :two_factor_authenticator, :inactive, user: user } - - before(:each) do - two_factor_authenticator.created_at = 10.hours.ago - two_factor_authenticator.save! - end - - 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) - end - end - - context 'with a authenticator of another user' do - let(:two_factor_authenticator) { FactoryGirl.create :two_factor_authenticator, :inactive } - - before(:each) do - two_factor_authenticator.created_at = 10.hours.ago - two_factor_authenticator.save! - end - - 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) - end - end - end - - context 'with a valid authenticator' do - let(:two_factor_authenticator) { FactoryGirl.create :two_factor_authenticator, :inactive, user: user } - - context 'with a valid OTP' do - before(:each) do - ROTP::TOTP.any_instance.should_receive(:verify_with_drift).with(otp, 30).and_return(true) - end - - 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) - end - - it 'does activate the authenticator' do - processor.process(params, cookies, user_agent) - two_factor_authenticator.reload - two_factor_authenticator.should be_active - end - - context 'when another two-factor authenticator was active' do - let!(:other_two_factor_authenticator) { FactoryGirl.create :two_factor_authenticator, user: user } - - it 'does activate the authenticator' do - processor.process(params, cookies, 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) - lambda do - other_two_factor_authenticator.reload - end.should raise_error(ActiveRecord::RecordNotFound) - end - end - - end - - context 'with an invalid OTP' do - before(:each) do - ROTP::TOTP.any_instance.should_receive(:verify_with_drift).with(otp, 30).and_return(false) - end - - 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) - end - - it 'does not activate the authenticator' do - processor.process(params, cookies, user_agent) - two_factor_authenticator.reload - two_factor_authenticator.should_not be_active - end - end - end - end - - context 'with an invalid ticket-granting ticket' do - let(:tgt) { 'TGT-lalala' } - 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(nil, cookies, user_agent) - end - end - end -end \ No newline at end of file diff --git a/spec/processor/two_factor_authenticator_destroyer_spec.rb b/spec/processor/two_factor_authenticator_destroyer_spec.rb deleted file mode 100644 index 2c1762a3..00000000 --- a/spec/processor/two_factor_authenticator_destroyer_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -require 'spec_helper' - -describe CASino::TwoFactorAuthenticatorDestroyerProcessor do - describe '#process' do - let(:listener) { Object.new } - 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 } - let(:user) { ticket_granting_ticket.user } - let(:tgt) { ticket_granting_ticket.ticket } - let(:user_agent) { ticket_granting_ticket.user_agent } - let(:params) { { id: two_factor_authenticator.id } } - - context 'with a valid two-factor authenticator' do - let!(:two_factor_authenticator) { FactoryGirl.create :two_factor_authenticator, user: user } - - 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) - end - - it 'deletes the two-factor authenticator' do - processor.process(params, cookies, user_agent) - lambda do - two_factor_authenticator.reload - end.should raise_error(ActiveRecord::RecordNotFound) - end - - it 'does not delete other two-factor authenticators' do - other = FactoryGirl.create :two_factor_authenticator - lambda do - processor.process(params, cookies, user_agent) - end.should change(CASino::TwoFactorAuthenticator, :count).by(-1) - end - end - - context 'with a two-factor authenticator of another user' do - let!(:two_factor_authenticator) { FactoryGirl.create :two_factor_authenticator } - - 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) - end - - it 'does not delete two-factor authenticators' do - lambda do - processor.process(params, cookies, 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' } - 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) - end - end - end -end \ No newline at end of file diff --git a/spec/processor/two_factor_authenticator_overview_spec.rb b/spec/processor/two_factor_authenticator_overview_spec.rb deleted file mode 100644 index 8657f0d4..00000000 --- a/spec/processor/two_factor_authenticator_overview_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'spec_helper' - -describe CASino::TwoFactorAuthenticatorOverviewProcessor do - describe '#process' do - let(:listener) { Object.new } - 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 } - 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) - end - end - - context 'with an inactive two-factor authenticator' do - let!(:two_factor_authenticator) { FactoryGirl.create :two_factor_authenticator, :inactive, user: user } - - it 'does not include the inactive authenticator' do - listener.should_receive(:two_factor_authenticators_found).with([]) - processor.process(cookies, user_agent) - end - end - - context 'with a two-factor authenticator registered' do - let(:two_factor_authenticator) { FactoryGirl.create :two_factor_authenticator, user: user } - let!(:other_two_factor_authenticator) { FactoryGirl.create :two_factor_authenticator } - - 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) - end - end - end - - context 'with an invalid ticket-granting ticket' do - let(:tgt) { 'TGT-lalala' } - 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) - end - end - end -end \ No newline at end of file diff --git a/spec/processor/two_factor_authenticator_registrator_spec.rb b/spec/processor/two_factor_authenticator_registrator_spec.rb deleted file mode 100644 index 416ca4dd..00000000 --- a/spec/processor/two_factor_authenticator_registrator_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -require 'spec_helper' - -describe CASino::TwoFactorAuthenticatorRegistratorProcessor do - describe '#process' do - let(:listener) { Object.new } - 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 - lambda do - processor.process(cookies, user_agent) - end.should change(CASino::TwoFactorAuthenticator, :count).by(1) - end - - it 'calls #two_factor_authenticator_created on the listener' do - listener.should_receive(:two_factor_authenticator_registered) do |authenticator| - authenticator.should == CASino::TwoFactorAuthenticator.last - end - processor.process(cookies, user_agent) - end - - it 'creates an inactive two-factor authenticator' do - processor.process(cookies, 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' } - 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) - end - end - end -end \ No newline at end of file diff --git a/spec/services/auth_token_validation_service_spec.rb b/spec/services/auth_token_validation_service_spec.rb new file mode 100644 index 00000000..1f5c2266 --- /dev/null +++ b/spec/services/auth_token_validation_service_spec.rb @@ -0,0 +1,83 @@ +require 'spec_helper' + +describe CASino::AuthTokenValidationService do + let(:token) { 'le_token' } + let(:signature) { 'le_signature' } + + subject { described_class.new(token, signature) } + + context 'without any token signers' do + before(:each) do + Dir.stub(:glob).with(CASino::AuthTokenValidationService::AUTH_TOKEN_SIGNERS_GLOB).and_return(nil) + end + + its(:user_data) { should == nil } + its(:validation_result) { should == nil } + end + + context 'with token signers' do + let(:signer_path) { '/test.pem' } + let(:signer_path_content) { 'this_is_le_certificate' } + let(:digest) { 'le_digest' } + let(:rsa_stub) do + double(OpenSSL::PKey::RSA).tap do |mock| + mock.stub(:verify).with(digest, signature, token).and_return(signature_valid) + end + end + + before(:each) do + Dir.stub(:glob).with(CASino::AuthTokenValidationService::AUTH_TOKEN_SIGNERS_GLOB) do |&block| + block.call(signer_path) + end + File.stub(:read).with(signer_path).and_return(signer_path_content) + OpenSSL::Digest::SHA256.stub(:new).and_return(digest) + OpenSSL::PKey::RSA.stub(:new).with(signer_path_content).and_return(rsa_stub) + end + + context 'with an invalid signature' do + let(:signature_valid) { false } + + its(:user_data) { should == nil } + its(:validation_result) { should == nil } + end + + context 'with a valid signature' do + let(:signature_valid) { true } + + before(:each) do + CASino::AuthTokenTicket.stub(:consume).and_return(ticket_valid) + end + + context 'with an invalid ticket' do + let(:ticket_valid) { false } + + its(:user_data) { should == nil } + its(:validation_result) { should == nil } + end + + context 'with a valid ticket' do + let(:ticket_valid) { true } + + before(:each) do + JSON.stub(:parse).and_return(token_data) + end + + context 'with invalid user data' do + let(:token_data) { { authenticator: 'test', username: 'example' } } + + its(:user_data) { should == nil } + its(:validation_result) { should == nil } + end + + context 'with valid user data' do + let(:token_data) { { authenticator: 'static', username: 'testuser' } } + let(:user_data) { { username: 'testuser', extra_attributes: { "name" => "Test User", "game" => [ "StarCraft 2", "Doto" ] } } } + let(:validation_result) { { authenticator: 'static', user_data: user_data } } + + its(:user_data) { should == user_data } + its(:validation_result) { should == validation_result } + end + end + end + end +end diff --git a/spec/support/sign_in.rb b/spec/support/sign_in.rb new file mode 100644 index 00000000..b78c6049 --- /dev/null +++ b/spec/support/sign_in.rb @@ -0,0 +1,4 @@ +def sign_in(ticket_granting_ticket) + request.cookies[:tgt] = ticket_granting_ticket.ticket + request.user_agent = ticket_granting_ticket.user_agent +end