Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Extendable strategies interface (part of omniauth authenticator solution) #109

Closed
wants to merge 20 commits into from

3 participants

@Slotos

Summary of changes:

  • Moved code that creates user session into separate method, making it possible to use it in sinatra extensions
  • Moved logout code into separate method, using it in session creation method to trigger single sign out upon identity change
  • Added login_links accessor, using it to gather and display login links for oauth providers
  • Using bundler with bin/rubycas-server Debatable
  • Activated sessions to track service and renew parameters through omniauth sequence, resetting them on login page visit. I remember meeting one use case that would redirect user to previously saved service which could seem illogical, however I've dismissed it at the time since it required direct url visiting, which is outside of regular user scenarios. Can't remember the case now. Needs review.
  • Some minor changes, like path filters based on params hash, avoiding "no authenticators" error when there are only strategies etc.

Implemented generic omniauth (1.0+) strategy: https://github.com/Slotos/rubycas-strategy-omniauth. Tested it with facebook and twitter, but it should work with krb5 and cas omniauth strategies as well. Omniauth 1.0 version of LDAP authenticator should work too, given there is one.

Remote-to-local mapping expects SQL database and two tables: user table and token table. Extending functionality for other storage and schema options would be a good idea, once such need arises.

What I wish to do but may not have time to do it:

  • Move all authenticators into strategies. Display login form only when at least one strategy have registered itself in login/pass authenticators list.

Another example of behaviour implemented with this interface would be https://github.com/Slotos/rubycas-strategy-impostor.

No whitespace diff link

@Slotos

I can't bring myself to delete this line. THE WORLD SHOULD KNOW!

Slotos added some commits
@Slotos Slotos Removing redundant code and provinding simplistic test coverage
def self.init_matchers!
  self.class_eval{}
end

Never forget !!
40b5400
@Slotos Slotos Adding ability to provide your own `require` and `register` strings f…
…or matchers.
3f5300a
@Slotos Slotos Returning from init_authenticators! is there are none while matchers …
…are present
90d9c17
@Slotos Slotos Modified confirm_authentication method to accept username and service…
… arguments instead of relying on instance variables
391b90b
@Slotos Slotos Purging `self.reconfigure!`. It was for config testing which is now h…
…andled differently.
ca2333a
@Slotos Slotos Adding route filters that allow me to remove omniauth specific routes…
… from rubycas-server code.
2c3e262
@Slotos Slotos confirm_attributes! sets cookies to uri_path path, regardless of loca…
…tion from where it was called.
b293bc6
@Slotos Slotos Revised strategy loading a bit 76c14de
@Slotos Slotos Updated specs.
TODO: Check service URI even when user is logged in.
31d0367
@Slotos Slotos Cleaning up pry. Friday ._. 1f86eec
@Slotos Slotos Cosmetic method name change in wake of offloading all session manglin…
…g to that method.
de16184
@Slotos Slotos Updating strategies config format to allow for strategy reuse c863d1a
@Slotos Slotos Minor changes:
- using bundler in bin/rubycas-server
- bringing config.ru in line with recent main repository changes
- not saving renew parameter in session anymore, its effect is immediate, not delayed, unlike service
b9240e1
@Slotos Slotos Establishing new session will log out old one if present. 782a74e
@Slotos Slotos oauth_links -> login_links
In addition turning login links into array of links, leaving placing and wrapping up to theme.
7cb2675
@Slotos Slotos Altered placement of login links. d725540
@Slotos Slotos Return of session[:renew].
Without it omniauth failure will result in redirect to service without renewing session.
986b48c
@Slotos Slotos Merge branch 'master' of git://github.com/rubycas/rubycas-server cf07371
@Slotos Slotos Updating r18n to 1.0.1.
Backstory: for some weird reason bundles would enforce version 1.0.1 on me. Had to change localization.rb to make it work.
59776e1
@zuk
Owner

Well this is interesting... gonna try to get to this next week.

@barttenbrinke

Wow this is really cool. I was looking at the token implementation and that whould be a really nice solution for the problem I'm having atm.

@Slotos

@zuk Do you need feedback on my part perchance?

@barttenbrinke

@zuk need help reviewing? I'd really like rubycas to get some active development going again.

@zuk
Owner
@Slotos Slotos closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jun 6, 2012
  1. @Slotos

    Implementing ability to require matchers which are sinatra extensions…

    Slotos authored
    … themselves
    
    Experimental (working but without test coverage) twitter and facebook matchers are available at:
    https://github.com/Slotos/rubycas-facebook-matcher
    https://github.com/Slotos/rubycas-twitter-matcher
Commits on Jun 13, 2012
  1. @Slotos

    Removing redundant code and provinding simplistic test coverage

    Slotos authored
    def self.init_matchers!
      self.class_eval{}
    end
    
    Never forget !!
Commits on Jun 14, 2012
  1. @Slotos
  2. @Slotos
Commits on Jun 15, 2012
  1. @Slotos

    Modified confirm_authentication method to accept username and service…

    Slotos authored
    … arguments instead of relying on instance variables
Commits on Jun 19, 2012
  1. @Slotos
  2. @Slotos

    Adding route filters that allow me to remove omniauth specific routes…

    Slotos authored
    … from rubycas-server code.
Commits on Jun 20, 2012
  1. @Slotos

    confirm_attributes! sets cookies to uri_path path, regardless of loca…

    Slotos authored
    …tion from where it was called.
Commits on Jun 22, 2012
  1. @Slotos

    Revised strategy loading a bit

    Slotos authored
  2. @Slotos

    Updated specs.

    Slotos authored
    TODO: Check service URI even when user is logged in.
  3. @Slotos

    Cleaning up pry. Friday ._.

    Slotos authored
Commits on Jun 26, 2012
  1. @Slotos
Commits on Jul 4, 2012
  1. @Slotos
  2. @Slotos

    Minor changes:

    Slotos authored
    - using bundler in bin/rubycas-server
    - bringing config.ru in line with recent main repository changes
    - not saving renew parameter in session anymore, its effect is immediate, not delayed, unlike service
  3. @Slotos
  4. @Slotos

    oauth_links -> login_links

    Slotos authored
    In addition turning login links into array of links, leaving placing and wrapping up to theme.
  5. @Slotos
  6. @Slotos

    Return of session[:renew].

    Slotos authored
    Without it omniauth failure will result in redirect to service without renewing session.
Commits on Jul 5, 2012
  1. @Slotos
  2. @Slotos

    Updating r18n to 1.0.1.

    Slotos authored
    Backstory: for some weird reason bundles would enforce version 1.0.1 on me. Had to change localization.rb to make it work.
This page is out of date. Refresh to see the latest.
View
1  .gitignore
@@ -2,6 +2,7 @@
config.yml
*.db
*.sqlite3
+*.sqlite
*.swp
*~
*.pidaproject
View
1  Gemfile
@@ -1,3 +1,2 @@
source "http://rubygems.org"
gemspec
-
View
14 Gemfile.lock
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
- rubycas-server (1.1.0)
+ rubycas-server (1.1.1)
activerecord (>= 2.3.12, < 3.1)
activesupport (>= 2.3.12, < 3.1)
crypt-isaac (~> 0.9.1)
@@ -46,7 +46,7 @@ GEM
multi_json (1.3.2)
net-ldap (0.1.1)
nokogiri (1.5.2)
- r18n-core (0.4.14)
+ r18n-core (1.0.1)
rack (1.4.1)
rack-protection (1.2.0)
rack
@@ -69,11 +69,11 @@ GEM
multi_json (~> 1.0)
rubyzip
sinatra (1.3.2)
- rack (>= 1.3.6, ~> 1.3)
+ rack (~> 1.3, >= 1.3.6)
rack-protection (~> 1.2)
- tilt (>= 1.3.3, ~> 1.3)
- sinatra-r18n (0.4.14)
- r18n-core (= 0.4.14)
+ tilt (~> 1.3, >= 1.3.3)
+ sinatra-r18n (1.0.1)
+ r18n-core (= 1.0.1)
sinatra (>= 1.3)
sqlite3 (1.3.6)
tilt (1.3.3)
@@ -85,7 +85,7 @@ PLATFORMS
ruby
DEPENDENCIES
- activeresource (< 3.1, >= 2.3.12)
+ activeresource (>= 2.3.12, < 3.1)
capybara (= 1.1.2)
net-ldap (~> 0.1.1)
rack-test
View
1  bin/rubycas-server
@@ -4,6 +4,7 @@
$KCODE = 'u' if RUBY_VERSION < '1.9'
require 'rubygems'
+require 'bundler/setup'
$:.unshift File.dirname(__FILE__) + "/../lib"
View
3  lib/casserver/localization.rb
@@ -5,8 +5,7 @@ module Localization
def self.included(mod)
mod.module_eval do
register Sinatra::R18n
- set :default_locale, 'en'
- set :translations, File.dirname(__FILE__) + "/../../locales"
+ ::R18n.default_places { File.dirname( File.dirname self.root ) + "/locales" }
end
end
end
View
290 lib/casserver/server.rb
@@ -8,6 +8,8 @@
module CASServer
class Server < Sinatra::Base
+ enable :sessions
+
if ENV['CONFIG_FILE']
CONFIG_FILE = ENV['CONFIG_FILE']
elsif !(c_file = File.dirname(__FILE__) + "/../../config.yml").nil? && File.exist?(c_file)
@@ -15,10 +17,43 @@ class Server < Sinatra::Base
else
CONFIG_FILE = "/etc/rubycas-server/config.yml"
end
-
+
include CASServer::CAS # CAS protocol helpers
include Localization
+ def self.login_links
+ @login_links ||= []
+ end
+
+ def self.add_login_link(text)
+ begin
+ @login_links << text
+ rescue NoMethodError
+ @login_links = [text]
+ end
+ end
+
+ # get "/oauth/failure", :when_params => {:provider => "facebook"}
+ def self.when_params(*args)
+ desired_params = Hash[args]
+
+ condition {
+ desired_params.delete_if do |k,v|
+ params[k.to_s] == v
+ end
+ desired_params.empty?
+ }
+ end
+
+ # get "/something", with_params => "renew"
+ # get "/something", with_params => ["renew", "fluttershy]
+ def self.with_params(*args)
+ args = [args] unless args.kind_of?(::Array)
+ condition {
+ (args.map(&:to_s) - params.keys.map(&:to_s)).empty?
+ }
+ end
+
# Use :public_folder for Sinatra >= 1.3, and :public for older versions.
def self.use_public_folder?
Sinatra.const_defined?("VERSION") && Gem::Version.new(Sinatra::VERSION) >= Gem::Version.new("1.3.0")
@@ -32,7 +67,7 @@ def self.use_public_folder?
:maximum_unused_login_ticket_lifetime => 5.minutes,
:maximum_unused_service_ticket_lifetime => 5.minutes, # CAS Protocol Spec, sec. 3.2.1 (recommended expiry time)
:maximum_session_lifetime => 2.days, # all tickets are deleted after this period of time
- :log => {:file => 'casserver.log', :level => 'DEBUG'},
+ :log => {:file => 'log/casserver.log', :level => 'DEBUG'},
:uri_path => ""
)
set :config, config
@@ -48,7 +83,7 @@ def static!
public_dir = Server.use_public_folder? ? settings.public_folder : settings.public
return if public_dir.nil?
public_dir = File.expand_path(public_dir)
-
+
path = File.expand_path(public_dir + unescape(request.path_info.gsub(/^#{settings.config[:uri_path]}/,'')))
return if path[0, public_dir.length] != public_dir
return unless File.file?(path)
@@ -62,17 +97,17 @@ def self.run!(options={})
handler = detect_rack_handler
handler_name = handler.name.gsub(/.*::/, '')
-
+
puts "== RubyCAS-Server is starting up " +
"on port #{config[:port] || port} for #{environment} with backup from #{handler_name}" unless handler_name =~/cgi/i
-
+
begin
opts = handler_options
rescue Exception => e
print_cli_message e, :error
raise e
end
-
+
handler.run self, opts do |server|
[:INT, :TERM].each { |sig| trap(sig) { quit!(server, handler_name) } }
set :running, true
@@ -86,12 +121,12 @@ def self.quit!(server, handler_name)
server.respond_to?(:stop!) ? server.stop! : server.stop
puts "\n== RubyCAS-Server is shutting down" unless handler_name =~/cgi/i
end
-
+
def self.print_cli_message(msg, type = :info)
if respond_to?(:config) && config && config[:quiet]
return
end
-
+
if type == :error
io = $stderr
prefix = "!!! "
@@ -99,7 +134,7 @@ def self.print_cli_message(msg, type = :info)
io = $stdout
prefix = ">>> "
end
-
+
io.puts
io.puts "#{prefix}#{msg}"
io.puts
@@ -109,18 +144,18 @@ def self.load_config_file(config_file)
begin
config_file = File.open(config_file)
rescue Errno::ENOENT => e
-
+
print_cli_message "Config file #{config_file} does not exist!", :error
print_cli_message "Would you like the default config file copied to #{config_file.inspect}? [y/N]"
if gets.strip.downcase == 'y'
require 'fileutils'
default_config = File.dirname(__FILE__) + '/../../config/config.example.yml'
-
+
if !File.exists?(File.dirname(config_file))
print_cli_message "Creating config directory..."
FileUtils.mkdir_p(File.dirname(config_file), :verbose => true)
end
-
+
print_cli_message "Copying #{default_config.inspect} to #{config_file.inspect}..."
FileUtils.cp(default_config, config_file, :verbose => true)
print_cli_message "The default config has been copied. You should now edit it and try starting again."
@@ -136,18 +171,13 @@ def self.load_config_file(config_file)
print_cli_message "Config file #{config_file.inspect} could not be read!", :error
raise e
end
-
- config.merge! HashWithIndifferentAccess.new(YAML.load(config_file))
+
+ # Adding ERB parser to allow for heroku deployment
+ config.merge! HashWithIndifferentAccess.new(YAML.load(ERB.new(config_file.read).result))
set :server, config[:server] || 'webrick'
- end
-
- def self.reconfigure!(config)
- config.each do |key, val|
- self.config[key] = val
- end
- init_database!
- init_logger!
- init_authenticators!
+
+ # Makes it possible to access it from extensions
+ set :config, config
end
def self.handler_options
@@ -164,7 +194,7 @@ def self.handler_ssl_options
cert_path = config[:ssl_cert]
key_path = config[:ssl_key] || config[:ssl_cert]
-
+
unless cert_path.nil? && key_path.nil?
raise "The ssl_cert and ssl_key options cannot be used with mongrel. You will have to run your " +
" server behind a reverse proxy if you want SSL under mongrel." if
@@ -195,14 +225,40 @@ def self.handler_ssl_options
end
end
+ def self.init_strategies!
+ strategies = config[:strategies] || config[:strategy]
+ return unless strategies
+ strategies = [strategies] unless strategies.is_a?(::Array)
+
+ strategies.each do |conf|
+ set :workhorse, conf
+
+ begin
+ require "rubycas-strategy-#{conf[:strategy].downcase}"
+ rescue LoadError => e
+ $LOG.debug "Failed require with error #{e}, attempting to load #{conf[:strategy]} strategy anyway"
+ end
+
+ register "CASServer::Strategy::#{conf[:strategy]}".constantize
+
+ set :workhorse, nil
+ end
+
+ set :login_links, login_links
+ end
+
def self.init_authenticators!
auth = []
-
+
if config[:authenticator].nil?
- print_cli_message "No authenticators have been configured. Please double-check your config file (#{CONFIG_FILE.inspect}).", :error
- exit 1
+ if config[:strategy].nil?
+ print_cli_message "No authenticators or matchers have been configured. Please double-check your config file (#{CONFIG_FILE.inspect}).", :error
+ exit 1
+ else
+ return
+ end
end
-
+
begin
# attempt to instantiate the authenticator
config[:authenticator] = [config[:authenticator]] unless config[:authenticator].instance_of? Array
@@ -254,7 +310,7 @@ def self.init_logger!
end
$LOG.level = Logger.const_get(config[:log][:level]) if config[:log][:level]
end
-
+
if config[:db_log]
if $LOG && config[:db_log][:file]
$LOG.debug "Redirecting ActiveRecord log to #{config[:log][:file]}"
@@ -277,7 +333,7 @@ def self.init_database!
ActiveRecord::Base.logger = prev_db_log
print_cli_message "Your database is now up to date."
end
-
+
ActiveRecord::Base.establish_connection(config[:database])
end
@@ -286,6 +342,7 @@ def self.init_database!
init_logger!
init_database!
init_authenticators!
+ init_strategies!
end
before do
@@ -304,7 +361,7 @@ def self.init_database!
# The #.#.# comments (e.g. "2.1.3") refer to section numbers in the CAS protocol spec
# under http://www.ja-sig.org/products/cas/overview/protocol/index.html
-
+
# 2.1 :: Login
# 2.1.1
@@ -321,6 +378,9 @@ def self.init_database!
@renew = params['renew']
@gateway = params['gateway'] == 'true' || params['gateway'] == '1'
+ session[:service] = @service
+ session[:renew] = @renew
+
if tgc = request.cookies['tgt']
tgt, tgt_error = validate_ticket_granting_ticket(tgc)
end
@@ -339,6 +399,12 @@ def self.init_database!
:message => t.error.unable_to_authenticate}
end
+ # TODO: Move strategy specific messages into strategy gems and implement persistent message store
+ if params[:oauth_error]
+ @message = {:type => 'mistake',
+ :message => "#{params[:oauth_strategy].to_s.capitalize} #{t.error.oauth_failure} #{params[:oauth_error]}" }
+ end
+
begin
if @service
if @renew
@@ -401,11 +467,11 @@ def self.init_database!
end
end
-
+
# 2.2
post "#{uri_path}/login" do
Utils::log_controller_action(self.class, params)
-
+
# 2.2.1 (optional)
@service = clean_service_url(params['service'])
@@ -416,7 +482,7 @@ def self.init_database!
# Remove leading and trailing widespace from username.
@username.strip! if @username
-
+
if @username && settings.config[:downcase_username]
$LOG.debug("Converting username #{@username.inspect} to lowercase because 'downcase_username' option is enabled.")
@username.downcase!
@@ -434,7 +500,7 @@ def self.init_database!
@lt = generate_login_ticket.ticket
$LOG.debug("Logging in with username: #{@username}, lt: #{@lt}, service: #{@service}, auth: #{settings.auth.inspect}")
-
+
credentials_are_valid = false
extra_attributes = {}
successful_authenticator = nil
@@ -462,36 +528,10 @@ def self.init_database!
auth_index += 1
end
-
+
if credentials_are_valid
$LOG.info("Credentials for username '#{@username}' successfully validated using #{successful_authenticator.class.name}.")
- $LOG.debug("Authenticator provided additional user attributes: #{extra_attributes.inspect}") unless extra_attributes.blank?
-
- # 3.6 (ticket-granting cookie)
- tgt = generate_ticket_granting_ticket(@username, extra_attributes)
- response.set_cookie('tgt', tgt.to_s)
-
- $LOG.debug("Ticket granting cookie '#{tgt.inspect}' granted to #{@username.inspect}")
-
- if @service.blank?
- $LOG.info("Successfully authenticated user '#{@username}' at '#{tgt.client_hostname}'. No service param was given, so we will not redirect.")
- @message = {:type => 'confirmation', :message => t.notice.success_logged_in}
- else
- @st = generate_service_ticket(@service, @username, tgt)
-
- begin
- service_with_ticket = service_uri_with_ticket(@service, @st)
-
- $LOG.info("Redirecting authenticated user '#{@username}' at '#{@st.client_hostname}' to service '#{@service}'")
- redirect service_with_ticket, 303 # response code 303 means "See Other" (see Appendix B in CAS Protocol spec)
- rescue URI::InvalidURIError
- $LOG.error("The service '#{@service}' is not a valid URI!")
- @message = {
- :type => 'mistake',
- :message => t.error.invalid_target_service
- }
- end
- end
+ establish_session!(@username, @service, extra_attributes)
else
$LOG.warn("Invalid credentials given for user '#{@username}'")
@message = {:type => 'mistake', :message => t.error.incorrect_username_or_password}
@@ -528,38 +568,10 @@ def self.init_database!
@gateway = params['gateway'] == 'true' || params['gateway'] == '1'
- tgt = CASServer::Model::TicketGrantingTicket.find_by_ticket(request.cookies['tgt'])
+ do_logout!(request.cookies['tgt'])
response.delete_cookie 'tgt'
- if tgt
- CASServer::Model::TicketGrantingTicket.transaction do
- $LOG.debug("Deleting Service/Proxy Tickets for '#{tgt}' for user '#{tgt.username}'")
- tgt.granted_service_tickets.each do |st|
- send_logout_notification_for_service_ticket(st) if config[:enable_single_sign_out]
- # TODO: Maybe we should do some special handling if send_logout_notification_for_service_ticket fails?
- # (the above method returns false if the POST results in a non-200 HTTP response).
- $LOG.debug "Deleting #{st.class.name.demodulize} #{st.ticket.inspect} for service #{st.service}."
- st.destroy
- end
-
- pgts = CASServer::Model::ProxyGrantingTicket.find(:all,
- :conditions => [CASServer::Model::Base.connection.quote_table_name(CASServer::Model::ServiceTicket.table_name)+".username = ?", tgt.username],
- :include => :service_ticket)
- pgts.each do |pgt|
- $LOG.debug("Deleting Proxy-Granting Ticket '#{pgt}' for user '#{pgt.service_ticket.username}'")
- pgt.destroy
- end
-
- $LOG.debug("Deleting #{tgt.class.name.demodulize} '#{tgt}' for user '#{tgt.username}'")
- tgt.destroy
- end
-
- $LOG.info("User '#{tgt.username}' logged out.")
- else
- $LOG.warn("User tried to log out without a valid ticket-granting ticket.")
- end
-
@message = {:type => 'confirmation', :message => t.notice.success_logged_out}
@message[:message] += t.notice.click_to_continue if @continue_url
@@ -574,10 +586,10 @@ def self.init_database!
render @template_engine, :login
end
end
-
-
+
+
# Handler for obtaining login tickets.
- # This is useful when you want to build a custom login form located on a
+ # This is useful when you want to build a custom login form located on a
# remote server. Your form will have to include a valid login ticket
# value, and this can be fetched from the CAS server using the POST handler.
@@ -612,20 +624,20 @@ def self.init_database!
# 2.4.1
get "#{uri_path}/validate" do
CASServer::Utils::log_controller_action(self.class, params)
-
+
# required
@service = clean_service_url(params['service'])
@ticket = params['ticket']
# optional
@renew = params['renew']
-
- st, @error = validate_service_ticket(@service, @ticket)
+
+ st, @error = validate_service_ticket(@service, @ticket)
@success = st && !@error
-
+
@username = st.username if @success
-
+
status response_status_from_error(@error) if @error
-
+
render @template_engine, :validate, :layout => false
end
@@ -659,8 +671,8 @@ def self.init_database!
render :builder, :proxy_validate
end
-
-
+
+
# 2.6
# 2.6.1
@@ -721,8 +733,6 @@ def self.init_database!
render :builder, :proxy
end
-
-
# Helpers
def response_status_from_error(error)
@@ -754,5 +764,75 @@ def compile_template(engine, data, options, views)
raise unless @custom_views
super engine, data, options, views
end
+
+ def establish_session!(username, service = nil, *args)
+ extra_attributes = args.extract_options!
+ $LOG.debug("Authenticator provided additional user attributes: #{extra_attributes.inspect}") unless extra_attributes.blank?
+
+ # 3.6 (ticket-granting cookie)
+ tgt = generate_ticket_granting_ticket(username, extra_attributes)
+ response.set_cookie('tgt',
+ :value => tgt.to_s,
+ :path => "/#{settings.uri_path}"
+ )
+
+ $LOG.debug("Ticket granting cookie '#{tgt.inspect}' granted to #{username.inspect}")
+
+ # Establishing new identity, log out previous one, notifying services if single_sign_out is enabled
+ $LOG.debug("Initiating log out for overriden session '#{request.cookies['tgt'].inspect}'")
+ do_logout!(request.cookies['tgt'])
+
+ if service.blank?
+ $LOG.info("Successfully authenticated user '#{username}' at '#{tgt.client_hostname}'. No service param was given, so we will not redirect.")
+ @message = {:type => 'confirmation', :message => t.notice.success_logged_in}
+ else
+ @st = generate_service_ticket(service, username, tgt)
+
+ begin
+ service_with_ticket = service_uri_with_ticket(service, @st)
+
+ $LOG.info("Redirecting authenticated user '#{username}' at '#{@st.client_hostname}' to service '#{service}'")
+ redirect service_with_ticket, 303 # response code 303 means "See Other" (see Appendix B in CAS Protocol spec)
+ rescue URI::InvalidURIError
+ $LOG.error("The service '#{service}' is not a valid URI!")
+ @message = {
+ :type => 'mistake',
+ :message => t.error.invalid_target_service
+ }
+ end
+ end
+ end
+
+ def do_logout!(ticket)
+ tgt = CASServer::Model::TicketGrantingTicket.find_by_ticket(ticket)
+
+ if tgt
+ CASServer::Model::TicketGrantingTicket.transaction do
+ $LOG.debug("Deleting Service/Proxy Tickets for '#{tgt}' for user '#{tgt.username}'")
+ tgt.granted_service_tickets.each do |st|
+ send_logout_notification_for_service_ticket(st) if config[:enable_single_sign_out]
+ # TODO: Maybe we should do some special handling if send_logout_notification_for_service_ticket fails?
+ # (the above method returns false if the POST results in a non-200 HTTP response).
+ $LOG.debug "Deleting #{st.class.name.demodulize} #{st.ticket.inspect} for service #{st.service}."
+ st.destroy
+ end
+
+ pgts = CASServer::Model::ProxyGrantingTicket.find(:all,
+ :conditions => [CASServer::Model::Base.connection.quote_table_name(CASServer::Model::ServiceTicket.table_name)+".username = ?", tgt.username],
+ :include => :service_ticket)
+ pgts.each do |pgt|
+ $LOG.debug("Deleting Proxy-Granting Ticket '#{pgt}' for user '#{pgt.service_ticket.username}'")
+ pgt.destroy
+ end
+
+ $LOG.debug("Deleting #{tgt.class.name.demodulize} '#{tgt}' for user '#{tgt.username}'")
+ tgt.destroy
+ end
+
+ $LOG.info("User '#{tgt.username}' logged out.")
+ else
+ $LOG.warn("User tried to log out without a valid ticket-granting ticket.")
+ end
+ end
end
end
View
61 lib/casserver/views/login.erb
@@ -1,30 +1,37 @@
<%# coding: UTF-8 -%>
-<table id="login-box">
- <tr>
- <td colspan="2">
- <div id="headline-container">
- <strong><%= escape_html @organization %></strong>
- <%= t.label.central_login_title %>
- </div>
- </td>
- </tr>
+<div id="login-box">
+ <table>
+ <tr>
+ <td colspan="2">
+ <div id="headline-container">
+ <strong><%= escape_html @organization %></strong>
+ <%= t.label.central_login_title %>
+ </div>
+ </td>
+ </tr>
- <% if @message %>
- <tr>
- <td colspan="2" id="messagebox-container">
- <div class="messagebox <%= escape_html @message[:type] %>">
- <%= escape_html @message[:message] %>
- </div>
- </td>
- </tr>
- <% end %>
+ <% if @message %>
+ <tr>
+ <td colspan="2" id="messagebox-container">
+ <div class="messagebox <%= escape_html @message[:type] %>">
+ <%= escape_html @message[:message] %>
+ </div>
+ </td>
+ </tr>
+ <% end %>
- <tr>
- <td id="logo-container">
- <img id="logo" src="<%= escape_html @uri_path %>/themes/<%= @theme %>/logo.png" />
- </td>
- <td id="login-form-container">
- <%= erb(:_login_form, :layout => false) %>
- </td>
- </tr>
-</table>
+ <tr>
+ <td id="logo-container">
+ <img id="logo" src="<%= escape_html @uri_path %>/themes/<%= @theme %>/logo.png" />
+ </td>
+ <td id="login-form-container">
+ <%= erb(:_login_form, :layout => false) %>
+ </td>
+ </tr>
+ </table>
+ <div id="login-links-container">
+ <% settings.login_links.each do |link| %>
+ <%= erb link %>
+ <% end %>
+ </div>
+</div>
View
1  locales/de.yml
@@ -9,6 +9,7 @@ error:
invalid_target_service: "Das Ziel-Service, welches Ihr Browsers geliefert hat, scheint ungültig zu sein. Bitte wenden Sie sich an Ihren Systemadministrator, um Hilfe zu erhalten."
unable_to_authenticate: "Client und Server sind nicht in der Lage eine Authentifizierung auszuhandeln. Bitte versuchen Sie, sich zu einem späteren Zeitpunkt erneut anzumelden."
no_service_parameter_given: "Der Server kann diese Gateway-Anfrage nicht erfüllen, da keine Service-Parameter übergeben wurden."
+ oauth_failure: "OAuth provider denied access, the error was:"
notice:
logged_in_as: "Sie sind derzeit angemeldet als '%1'. Sollten dies nicht Sie sein, melden Sie sich bitte unten an."
View
1  locales/en.yml
@@ -9,6 +9,7 @@ error:
invalid_target_service: "The target service your browser supplied appears to be invalid. Please contact your system administrator for help."
unable_to_authenticate: "The client and server are unable to negotiate authentication. Please try logging in again later."
no_service_parameter_given: "The server cannot fulfill this gateway request because no service parameter was given."
+ oauth_failure: "OAuth provider denied access, the error was:"
notice:
logged_in_as: "You are currently logged in as '%1'. If this is not you, please log in below."
View
1  locales/es.yml
@@ -9,6 +9,7 @@ error:
invalid_target_service: "El servicio de destino proporcionado por su navegador parece inválido. Por favor contacte al administrador."
unable_to_authenticate: "El cliente y el servidor no pueden negociar su autentificación. Por favor intente luego."
no_service_parameter_given: "El servidor no puede atender este pedido porque no se especificó el servicio de destino."
+ oauth_failure: "OAuth provider denied access, the error was:"
notice:
logged_in_as: "Actualmente está logueado como '%1'. Si este no es usted, por favor ingrese sus datos debajo."
View
5 locales/es_ar.yml
@@ -3,15 +3,16 @@ error:
login_ticket_already_used: "El ticket de login que ha provisto ya ha sido utilizado. Por favor intente ingresando sus credenciales nuevamente."
login_timeout: "Usted ha tardado mucho en ingresar sus credenciales. Por favor reintente nuevamente."
invalid_login_ticket: "El ticket de login que ha provisto es inválido. Puede que haya un problema con el sistema de autentificación."
- login_ticket_needs_post_request: "Para generar un ticket de acceso, usted debe hacer una petición POST."
+ login_ticket_needs_post_request: "Para generar un ticket de acceso, usted debe hacer una petición POST."
incorrect_username_or_password: "El email o contraseña ingresados son incorrectos."
invalid_submit_to_uri: "No se ha podido adivinar el URI de acceso al CAS. Suministre un submitToURI como parámetro con su solicitud."
invalid_target_service: "El servicio de destino proporcionado por su navegador parece inválido. Por favor contacte al administrador."
unable_to_authenticate: "El cliente y el servidor no pueden negociar su autentificación. Por favor intente luego."
no_service_parameter_given: "El servidor no puede atender este pedido porque no se especificó el servicio de destino."
+ oauth_failure: "OAuth provider denied access, the error was:"
notice:
- logged_in_as: "Actualmente está logueado como '%1'. Si este no es usted, por favor ingrese sus datos debajo."
+ logged_in_as: "Actualmente está logueado como '%1'. Si este no es usted, por favor ingrese sus datos debajo."
click_to_continue: "Por favor, haga click en el siguiente link para continuar:"
success_logged_out: "Ha finalizado correctamente su sesión de usuario."
success_logged_in: "Has ingresado satisfactoreamente."
View
1  locales/fr.yml
@@ -9,6 +9,7 @@ error:
invalid_target_service: "Le service cible que votre navigateur a fourni semble incorrect. Veuillez contacter votre administrateur système pour assistance."
unable_to_authenticate: "Le client et le serveur ne peuvent négocier l'identification. Veuillez réessayer ultérieurement."
no_service_parameter_given: "Le serveur ne peut pas répondre à cette demande (aucun paramètre de service n'a été donné)."
+ oauth_failure: "OAuth provider denied access, the error was:"
notice:
logged_in_as: "Vous êtes actuellement connecté en tant que '%1'. Si ce n'est pas vous, veuillez vous connecter ci-dessous."
View
1  locales/jp.yml
@@ -9,6 +9,7 @@ error:
invalid_target_service: "ブラウザが示す対象サービスは無効のようです。システム管理者に連絡してください。"
unable_to_authenticate: "クライアントとサーバー間で認証ができませんでした。しばらくしてから再度ログインしてください。"
no_service_parameter_given: "サービスパラメーターが指定されていないので、サーバーはゲートウェイリクエストを満たす事ができません。"
+ oauth_failure: "OAuth provider denied access, the error was:"
notice:
logged_in_as: "'%1'としてログインしています。違うユーザーでログインするには下に入力してくだ さい。"
View
1  locales/pl.yml
@@ -9,6 +9,7 @@ error:
invalid_target_service: "Podany parametr 'service' jest nie prawidłowy. Skontaktuj się z administratorem systemu aby uzyskać pomoc."
unable_to_authenticate: "System nie jest wstanie przeprowadzić autentykacji. Proszę spróbować poźniej"
no_service_parameter_given: "System nie może spełnić żądania ponieważ brakuje parametru 'service'."
+ oauth_failure: "OAuth provider denied access, the error was:"
notice:
logged_in_as: "Jesteś aktualnie zalogowany jako '%1'. Jeżeli to nie jesteś ty, zaloguj się tutaj."
View
1  locales/pt.yml
@@ -9,6 +9,7 @@ error:
invalid_target_service: "O seu navegador está aparentemente com problemas. Por favor, contate o administrador do sistema para obter ajuda."
unable_to_authenticate: "O cliente e o servidor não puderam efetuar a autenticação. por favor, tente novamente mais tarde."
no_service_parameter_given: "O servidor não pode completar a solicitação porque não foi enviado o paramêtro do serviço."
+ oauth_failure: "OAuth provider denied access, the error was:"
notice:
logged_in_as: "Você está logado como '%1'. Se este não for você, Por favor, faça o login a baixo."
View
1  locales/ru.yml
@@ -9,6 +9,7 @@ error:
invalid_target_service: "Сервис, указанный вашим браузером, неверен. Свяжитесь с вашим системным администратором."
unable_to_authenticate: "Клиент и сервер не могут провести проверку прав. Попробуйте войти позже."
no_service_parameter_given: "Этот сервер не может выполнить этот запрос, поскольку не были указаны праметры сервиса."
+ oauth_failure: "OAuth источник отказал в доступе с ошибкой:"
notice:
logged_in_as: "Вы авторизированы как '%s'."
View
1  locales/zh.yml
@@ -9,6 +9,7 @@ error:
invalid_target_service: "你的浏览器提供的网址是无效的,请联系你的系统管理员"
unable_to_authenticate: "现在无法认证,请稍后重试"
no_service_parameter_given: "无法完成网关请求,因为没有提供服务的参数"
+ oauth_failure: "OAuth provider denied access, the error was:"
notice:
logged_in_as: "你正以 '%1' 的身份登入。如果这不是你,请重新登录"
View
1  locales/zh_tw.yml
@@ -9,6 +9,7 @@ error:
invalid_target_service: "你的瀏覽器傳送的服務網址是無效的,請向你的系統管理員尋求協助"
unable_to_authenticate: "現在無法認證,請稍候再嘗試登入"
no_service_parameter_given: "無法完成 gateway request 因為沒有提供 service 的參數"
+ oauth_failure: "OAuth provider denied access, the error was:"
notice:
logged_in_as: "你正以 '%1' 的身分登入。如果這不是你,請重新登入"
View
8 spec/spec_helper.rb
@@ -58,15 +58,15 @@ def follow_redirects!
# This called in specs' `before` block.
# Due to the way Sinatra applications are loaded,
# we're forced to delay loading of the server code
-# until the start of each test so that certain
+# until the start of each test so that certain
# configuraiton options can be changed (e.g. `uri_path`)
def load_server(config_file)
ENV['CONFIG_FILE'] = config_file
-
+
silence_warnings do
load File.dirname(__FILE__) + '/../lib/casserver/server.rb'
end
-
+
CASServer::Server.enable(:raise_errors)
CASServer::Server.disable(:show_exceptions)
@@ -81,7 +81,7 @@ def reset_spec_database
CASServer::Server.config[:database] && CASServer::Server.config[:database][:database]
FileUtils.rm_f(CASServer::Server.config[:database][:database])
-
+
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.logger.level = Logger::ERROR
ActiveRecord::Migration.verbose = false
View
53 spec/strategy_config.yml
@@ -0,0 +1,53 @@
+server: webrick
+port: 6543
+#ssl_cert: test.pem
+#uri_path: /cas
+#bind_address: 0.0.0.0
+
+# database:
+# adapter: mysql
+# database: casserver
+# username: root
+# password:
+# host: localhost
+# reconnect: true
+database:
+ adapter: sqlite3
+ database: spec/casserver_spec.db
+
+disable_auto_migrations: true
+
+quiet: true
+
+authenticator:
+ class: CASServer::Authenticators::Test
+ password: spec_password
+
+strategies:
+ strategy: Dummy
+
+theme: simple
+
+organization: "RSPEC-TEST"
+
+infoline: "This is an rspec test."
+
+#custom_views: /path/to/custom/views
+
+default_locale: en
+
+log:
+ file: casserver_spec.log
+ level: DEBUG
+
+#db_log:
+# file: casserver_spec_db.log
+
+enable_single_sign_out: true
+
+#maximum_unused_login_ticket_lifetime: 300
+#maximum_unused_service_ticket_lifetime: 300
+
+#maximum_session_lifetime: 172800
+
+#downcase_username: true
View
13 spec/strategy_dummy.rb
@@ -0,0 +1,13 @@
+module CASServer
+ module Strategy
+ module Dummy
+ def self.registered(app)
+
+ app.get '/confirm_authentication' do
+ establish_session!("someone", params[:service])
+ end
+
+ end
+ end
+ end
+end
View
49 spec/strategy_spec.rb
@@ -0,0 +1,49 @@
+# encoding: UTF-8
+require File.dirname(__FILE__) + '/spec_helper'
+require File.dirname(__FILE__) + '/strategy_dummy'
+
+include Rack::Test::Methods
+
+$LOG = Logger.new(File.basename(__FILE__).gsub('.rb','.log'))
+
+RSpec.configure do |config|
+ config.include Capybara::DSL
+end
+
+describe 'CASServer strategies' do
+ before :all do
+ app = load_server(File.dirname(__FILE__) + "/strategy_config.yml")
+ @browser = Rack::Test::Session.new( Rack::MockSession.new( app ) )
+ end
+
+ describe "login_links writer/accessor" do
+ it "should be empty initially" do
+ CASServer::Server.login_links.should eq([])
+ end
+
+ it "should provide push accessor to push string into it" do
+ string = "TEST STRING PLEASE IGNORE"
+ CASServer::Server.add_login_link string
+ CASServer::Server.login_links.should include(string)
+ end
+ end
+
+ describe "establish_session" do
+ it "should set tgc" do
+ @browser.get '/confirm_authentication'
+ @browser.instance_variable_get(:@rack_mock_session).cookie_jar["tgt"].should =~ /^TGC-[0-9rA-Z]+$/
+ end
+
+ it "should redirect to service if service is given" do
+ service = "http://somewhere.else/"
+ visit "/confirm_authentication?service=#{service}"
+ page.current_url.should =~ Regexp.new("^#{service}\\?ticket=ST-[0-9rA-Z]+$")
+ end
+
+ it "should not redirect to service if service is not a valid URI" do
+ service = CGI.escape("Hey, I'm not an URI, seriously!")
+ @browser.get "/confirm_authentication?service=#{service}"
+ @browser.last_response.should_not be_redirect
+ end
+ end
+end
Something went wrong with that request. Please try again.