ror-rubycas-server 1.0.a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. data/CHANGELOG +292 -0
  2. data/Gemfile +2 -0
  3. data/LICENSE +26 -0
  4. data/README.textile +129 -0
  5. data/Rakefile +1 -0
  6. data/bin/rubycas-server +16 -0
  7. data/lib/casserver.rb +11 -0
  8. data/lib/casserver/authenticators/active_directory_ldap.rb +19 -0
  9. data/lib/casserver/authenticators/authlogic_crypto_providers/aes256.rb +43 -0
  10. data/lib/casserver/authenticators/authlogic_crypto_providers/bcrypt.rb +92 -0
  11. data/lib/casserver/authenticators/authlogic_crypto_providers/md5.rb +34 -0
  12. data/lib/casserver/authenticators/authlogic_crypto_providers/sha1.rb +59 -0
  13. data/lib/casserver/authenticators/authlogic_crypto_providers/sha512.rb +50 -0
  14. data/lib/casserver/authenticators/base.rb +67 -0
  15. data/lib/casserver/authenticators/client_certificate.rb +47 -0
  16. data/lib/casserver/authenticators/google.rb +58 -0
  17. data/lib/casserver/authenticators/ldap.rb +147 -0
  18. data/lib/casserver/authenticators/ntlm.rb +88 -0
  19. data/lib/casserver/authenticators/open_id.rb +22 -0
  20. data/lib/casserver/authenticators/sql.rb +133 -0
  21. data/lib/casserver/authenticators/sql_authlogic.rb +93 -0
  22. data/lib/casserver/authenticators/sql_encrypted.rb +75 -0
  23. data/lib/casserver/authenticators/sql_md5.rb +19 -0
  24. data/lib/casserver/authenticators/sql_rest_auth.rb +85 -0
  25. data/lib/casserver/authenticators/test.rb +22 -0
  26. data/lib/casserver/cas.rb +315 -0
  27. data/lib/casserver/localization.rb +91 -0
  28. data/lib/casserver/model.rb +270 -0
  29. data/lib/casserver/options_hash.rb +44 -0
  30. data/lib/casserver/server.rb +706 -0
  31. data/lib/casserver/utils.rb +32 -0
  32. data/lib/casserver/views/_login_form.erb +42 -0
  33. data/lib/casserver/views/layout.erb +18 -0
  34. data/lib/casserver/views/login.erb +30 -0
  35. data/lib/casserver/views/proxy.builder +12 -0
  36. data/lib/casserver/views/proxy_validate.builder +25 -0
  37. data/lib/casserver/views/service_validate.builder +18 -0
  38. data/lib/casserver/views/validate.erb +2 -0
  39. data/po/de_DE/rubycas-server.po +127 -0
  40. data/po/es_ES/rubycas-server.po +123 -0
  41. data/po/fr_FR/rubycas-server.po +128 -0
  42. data/po/ja_JP/rubycas-server.po +126 -0
  43. data/po/pl_PL/rubycas-server.po +123 -0
  44. data/po/pt_BR/rubycas-server.po +123 -0
  45. data/po/ru_RU/rubycas-server.po +118 -0
  46. data/po/rubycas-server.pot +112 -0
  47. data/po/zh_CN/rubycas-server.po +113 -0
  48. data/po/zh_TW/rubycas-server.po +113 -0
  49. data/public/themes/cas.css +121 -0
  50. data/public/themes/notice.png +0 -0
  51. data/public/themes/ok.png +0 -0
  52. data/public/themes/simple/bg.png +0 -0
  53. data/public/themes/simple/favicon.png +0 -0
  54. data/public/themes/simple/login_box_bg.png +0 -0
  55. data/public/themes/simple/logo.png +0 -0
  56. data/public/themes/simple/theme.css +28 -0
  57. data/public/themes/urbacon/bg.png +0 -0
  58. data/public/themes/urbacon/login_box_bg.png +0 -0
  59. data/public/themes/urbacon/logo.png +0 -0
  60. data/public/themes/urbacon/theme.css +33 -0
  61. data/public/themes/warning.png +0 -0
  62. data/resources/init.d.sh +58 -0
  63. data/rubycas-server.gemspec +57 -0
  64. data/setup.rb +1585 -0
  65. data/spec/alt_config.yml +50 -0
  66. data/spec/authenticators/ldap_spec.rb +53 -0
  67. data/spec/casserver_spec.rb +141 -0
  68. data/spec/database.yml +5 -0
  69. data/spec/default_config.yml +73 -0
  70. data/spec/model_spec.rb +42 -0
  71. data/spec/options_hash_spec.rb +146 -0
  72. data/spec/spec.opts +4 -0
  73. data/spec/spec_helper.rb +90 -0
  74. data/spec/utils_spec.rb +53 -0
  75. data/tasks/bundler.rake +4 -0
  76. data/tasks/db/migrate.rake +12 -0
  77. data/tasks/localization.rake +13 -0
  78. data/tasks/spec.rake +10 -0
  79. metadata +356 -0
@@ -0,0 +1,58 @@
1
+ require 'casserver/authenticators/base'
2
+ require 'uri'
3
+ require 'net/http'
4
+ require 'net/https'
5
+ require 'timeout'
6
+
7
+ # Validates Google accounts against Google's authentication service -- in other
8
+ # words, this authenticator allows users to log in to CAS using their
9
+ # Gmail/Google accounts.
10
+ class CASServer::Authenticators::Google < CASServer::Authenticators::Base
11
+ def validate(credentials)
12
+ read_standard_credentials(credentials)
13
+
14
+ return false if @username.blank? || @password.blank?
15
+
16
+ auth_data = {
17
+ 'Email' => @username,
18
+ 'Passwd' => @password,
19
+ 'service' => 'xapi',
20
+ 'source' => 'RubyCAS-Server',
21
+ 'accountType' => 'HOSTED_OR_GOOGLE'
22
+ }
23
+
24
+ url = URI.parse('https://www.google.com/accounts/ClientLogin')
25
+ if @options[:proxy]
26
+ http = Net::HTTP.Proxy(@options[:proxy][:host], @options[:proxy][:port], @options[:proxy][:username], @options[:proxy][:password]).new(url.host, url.port)
27
+ else
28
+ http = Net::HTTP.new(url.host, url.port)
29
+ end
30
+ http.use_ssl = true
31
+
32
+ # TODO: make the timeout configurable
33
+ wait_seconds = 10
34
+ begin
35
+ timeout(wait_seconds) do
36
+ res = http.start do |conn|
37
+ req = Net::HTTP::Post.new(url.path)
38
+ req.set_form_data(auth_data,'&')
39
+ conn.request(req)
40
+ end
41
+
42
+ case res
43
+ when Net::HTTPSuccess
44
+ true
45
+ when Net::HTTPForbidden
46
+ false
47
+ else
48
+ $LOG.error("Unexpected response from Google while validating credentials: #{res.inspect} ==> #{res.body}.")
49
+ raise CASServer::AuthenticatorError, "Unexpected response received from Google while validating credentials."
50
+ end
51
+ end
52
+ rescue Timeout::Error
53
+ $LOG.error("Google did not respond to the credential validation request. We waited for #{wait_seconds.inspect} seconds before giving up.")
54
+ raise CASServer::AuthenticatorError, "Timeout while waiting for Google to validate credentials."
55
+ end
56
+
57
+ end
58
+ end
@@ -0,0 +1,147 @@
1
+ require 'casserver/authenticators/base'
2
+
3
+ begin
4
+ require 'net/ldap'
5
+ rescue LoadError
6
+ require 'rubygems'
7
+ begin
8
+ gem 'net-ldap', '~> 0.1.1'
9
+ rescue Gem::LoadError
10
+ $stderr.puts
11
+ $stderr.puts "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
12
+ $stderr.puts
13
+ $stderr.puts "To use the LDAP/AD authenticator, you must first install the 'net-ldap' gem."
14
+ $stderr.puts " See http://github.com/RoryO/ruby-net-ldap for details."
15
+ $stderr.puts
16
+ $stderr.puts "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
17
+ exit 1
18
+ end
19
+ require 'net/ldap'
20
+ end
21
+
22
+ # Basic LDAP authenticator. Should be compatible with OpenLDAP and other similar LDAP servers,
23
+ # although it hasn't been officially tested. See example config file for details on how
24
+ # to configure it.
25
+ class CASServer::Authenticators::LDAP < CASServer::Authenticators::Base
26
+ def validate(credentials)
27
+ read_standard_credentials(credentials)
28
+
29
+ return false if @password.blank?
30
+
31
+ raise CASServer::AuthenticatorError, "Cannot validate credentials because the authenticator hasn't yet been configured" unless @options
32
+ raise CASServer::AuthenticatorError, "Invalid LDAP authenticator configuration!" unless @options[:ldap]
33
+ raise CASServer::AuthenticatorError, "You must specify a server host in the LDAP configuration!" unless @options[:ldap][:host] || @options[:ldap][:server]
34
+
35
+ raise CASServer::AuthenticatorError, "The username '#{@username}' contains invalid characters." if (@username =~ /[*\(\)\0\/]/)
36
+
37
+ preprocess_username
38
+
39
+ @ldap = Net::LDAP.new
40
+
41
+
42
+ @options[:ldap][:host] ||= @options[:ldap][:server]
43
+ @ldap.host = @options[:ldap][:host]
44
+ @ldap.port = @options[:ldap][:port] if @options[:ldap][:port]
45
+ @ldap.encryption(@options[:ldap][:encryption].intern) if @options[:ldap][:encryption]
46
+
47
+ begin
48
+ if @options[:ldap][:auth_user]
49
+ bind_success = bind_by_username_with_preauthentication
50
+ else
51
+ bind_success = bind_by_username
52
+ end
53
+
54
+ return false unless bind_success
55
+
56
+ entry = find_user
57
+ extract_extra_attributes(entry)
58
+
59
+ return true
60
+ rescue Net::LDAP::LdapError => e
61
+ raise CASServer::AuthenticatorError,
62
+ "LDAP authentication failed with '#{e}'. Check your authenticator configuration."
63
+ end
64
+ end
65
+
66
+ protected
67
+ def default_username_attribute
68
+ "cn"
69
+ end
70
+
71
+ private
72
+ # Add prefix to username, if :username_prefix was specified in the :ldap config.
73
+ def preprocess_username
74
+ @username = @options[:ldap][:username_prefix] + @username if @options[:ldap][:username_prefix]
75
+ end
76
+
77
+ # Attempt to bind with the LDAP server using the username and password entered by
78
+ # the user. If a :filter was specified in the :ldap config, the filter will be
79
+ # added to the LDAP query for the username.
80
+ def bind_by_username
81
+ username_attribute = options[:ldap][:username_attribute] || default_username_attribute
82
+
83
+ @ldap.bind_as(:base => @options[:ldap][:base], :password => @password, :filter => user_filter)
84
+ end
85
+
86
+ # If an auth_user is specified, we will connect ("pre-authenticate") with the
87
+ # LDAP server using the authenticator account, and then attempt to bind as the
88
+ # user who is actually trying to authenticate. Note that you need to set up
89
+ # the special authenticator account first. Also, auth_user must be the authenticator
90
+ # user's full CN, which is probably not the same as their username.
91
+ #
92
+ # This pre-authentication process is necessary because binding can only be done
93
+ # using the CN, so having just the username is not enough. We connect as auth_user,
94
+ # and then try to find the target user's CN based on the given username. Then we bind
95
+ # as the target user to validate their credentials.
96
+ def bind_by_username_with_preauthentication
97
+ raise CASServer::AuthenticatorError, "A password must be specified in the configuration for the authenticator user!" unless
98
+ @options[:ldap][:auth_password]
99
+
100
+ @ldap.authenticate(@options[:ldap][:auth_user], @options[:ldap][:auth_password])
101
+
102
+ @ldap.bind_as(:base => @options[:ldap][:base], :password => @password, :filter => user_filter)
103
+ end
104
+
105
+ # Combine the filter for finding the user with the optional extra filter specified in the config
106
+ # (if any).
107
+ def user_filter
108
+ username_attribute = options[:ldap][:username_attribute] || default_username_attribute
109
+
110
+ filter = Array(username_attribute).map { |ua| Net::LDAP::Filter.eq(ua, @username) }.reduce(:|)
111
+ unless @options[:ldap][:filter].blank?
112
+ filter &= Net::LDAP::Filter.construct(@options[:ldap][:filter])
113
+ end
114
+
115
+ filter
116
+ end
117
+
118
+ # Finds the user based on the user_filter (this is called after authentication).
119
+ # We do this to make it possible to extract extra_attributes.
120
+ def find_user
121
+ results = @ldap.search( :base => options[:ldap][:base], :filter => user_filter)
122
+ return results.first
123
+ end
124
+
125
+ def extract_extra_attributes(ldap_entry)
126
+ @extra_attributes = {}
127
+ extra_attributes_to_extract.each do |attr|
128
+ v = ldap_entry[attr]
129
+ next if !v || (v.respond_to?(:empty?) && v.empty?)
130
+ if v.kind_of?(Array)
131
+ @extra_attributes[attr] = []
132
+ ldap_entry[attr].each do |a|
133
+ @extra_attributes[attr] << a.to_s
134
+ end
135
+ else
136
+ @extra_attributes[attr] = v.to_s
137
+ end
138
+ end
139
+
140
+ if @extra_attributes.empty?
141
+ $LOG.warn("#{self.class}: Did not read any extra_attributes for user #{@username.inspect} even though an :extra_attributes option was provided.")
142
+ else
143
+ $LOG.debug("#{self.class}: Read the following extra_attributes for user #{@username.inspect}: #{@extra_attributes.inspect}")
144
+ end
145
+ ldap_entry
146
+ end
147
+ end
@@ -0,0 +1,88 @@
1
+ # THIS AUTHENTICATOR DOES NOT WORK (not even close!)
2
+ #
3
+ # I started working on this but run into a wall, so I am commiting what I've got
4
+ # done and leaving it here with hopes of one day finishing it.
5
+ #
6
+ # The main problem is that although I've got the Lan Manager/NTLM password hash,
7
+ # I'm not sure what to do with it. i.e. I need to check it against the AD or SMB
8
+ # server or something... maybe faking an SMB share connection and using the LM
9
+ # response for authentication might do the trick?
10
+
11
+ require 'casserver/authenticators/base'
12
+
13
+ # Ruby/NTLM package from RubyForge
14
+ require 'net/ntlm'
15
+
16
+ module CASServer
17
+ module Authenticators
18
+ class NTLM
19
+ # This will have to be somehow called by the top of the 'get' method
20
+ # in the Login controller (maybe via a hook?)... if this code fails
21
+ # then the controller should fall back to some other method of authentication
22
+ # (probably AD/LDAP or something).
23
+ def filter_for_top_of_login_get_controller_method
24
+ $LOG.debug @env.inspect
25
+ if @env['HTTP_AUTHORIZATION'] =~ /NTLM ([^\s]+)/
26
+ # if we're here, then the client has sent back a Type1 or Type3 message
27
+ # in reply to our NTLM challenge or our Type2 message
28
+ data_raw = Base64.decode64($~[1])
29
+ $LOG.debug "T1 RAW: #{t1_raw}"
30
+ t = Net::NTLM::Message::Message.parse(t1_raw)
31
+ if t.kind_of? Net::NTLM::Type1
32
+ t1 = t
33
+ elsif t.kind_of? Net::NTLM::Type3
34
+ t3 = t
35
+ else
36
+ raise "Invalid NTLM reply from client."
37
+ end
38
+
39
+ if t1
40
+ $LOG.debug "T1: #{t1.inspect}"
41
+
42
+ # now put together a Type2 message asking for the client to send
43
+ # back NTLM credentials (LM hash and such)
44
+ t2 = Net::NTLM::Message::Type2.new
45
+ t2.set_flag :UNICODE
46
+ t2.set_flag :NTLM
47
+ t2.context = 0x0000000000000000 # this can probably just be left unassigned
48
+ t2.challenge = 0x0123456789abcdef # this should be a random 8-byte integer
49
+
50
+ $LOG.debug "T2: #{t2.inspect}"
51
+ $LOG.debug "T2: #{t2.serialize}"
52
+ headers["WWW-Authenticate"] = "NTLM #{t2.encode64}"
53
+
54
+ # the client should respond to this with a Type3 message...
55
+ r('401', '', headers)
56
+ return
57
+ else
58
+ # NOTE: for some reason the server never receives the T3 response, even though monitoring
59
+ # the HTTP traffic I can see that the client does send it back... there's probably
60
+ # another bug hiding somewhere here
61
+
62
+ lm_response = t3.lm_response
63
+ ntlm_response = t3.ntlm_response
64
+ username = t3.user
65
+ # this is where we run up against a wall... we need some way to check the lm and/or ntlm
66
+ # reponse against the authentication server (probably Active Directory)... maybe a samba
67
+ # call would do it?
68
+ $LOG.debug "T3 LM: #{lm_response.inspect}"
69
+ $LOG.debug "T3 NTLM: #{ntlm_response.inspect}"
70
+
71
+ # assuming the authentication was successful, we'll now need to do something in the
72
+ # controller acting as if we'd received correct login credentials (i.e. proceed as if
73
+ # CAS authentication was successful).... if authentication failed, then we should
74
+ # just fall back to old-school web-based authentication, asking the user to enter
75
+ # their username and password the normal CAS way
76
+ end
77
+ else
78
+ # this sends the initial NTLM challenge, asking the browser
79
+ # to send back a Type1 message
80
+ headers['WWW-Authenticate'] = "NTLM"
81
+ headers['Connection'] = "Close"
82
+ r('401', '', headers)
83
+ return
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,22 @@
1
+ require 'casserver/authenticators/base'
2
+
3
+ require 'openid'
4
+ require 'openid/extensions/sreg'
5
+ require 'openid/extensions/pape'
6
+ require 'openid/store/memory'
7
+
8
+
9
+ # CURRENTLY UNIMPLEMENTED
10
+ # This is just starter code.
11
+ class CASServer::Authenticators::OpenID < CASServer::Authenticators::Base
12
+
13
+ def validate(credentials)
14
+ raise NotImplementedError, "The OpenID authenticator is not yet implemented. "+
15
+ "See http://code.google.com/p/rubycas-server/issues/detail?id=36 if you are interested in helping this along."
16
+
17
+ read_standard_credentials(credentials)
18
+
19
+ store = OpenID::Store::Memory.new
20
+ consumer = OpenID::Consumer.new({}, store)
21
+ end
22
+ end
@@ -0,0 +1,133 @@
1
+ require 'casserver/authenticators/base'
2
+
3
+ begin
4
+ require 'active_record'
5
+ rescue LoadError
6
+ require 'rubygems'
7
+ require 'active_record'
8
+ end
9
+
10
+ # Authenticates against a plain SQL table.
11
+ #
12
+ # This assumes that all of your users are stored in a table that has a 'username'
13
+ # column and a 'password' column. When the user logs in, CAS conects to the
14
+ # database and looks for a matching username/password in the users table. If a
15
+ # matching username and password is found, authentication is successful.
16
+ #
17
+ # Any database backend supported by ActiveRecord can be used.
18
+ #
19
+ # Config example:
20
+ #
21
+ # authenticator:
22
+ # class: CASServer::Authenticators::SQL
23
+ # database:
24
+ # adapter: mysql
25
+ # database: some_database_with_users_table
26
+ # username: root
27
+ # password:
28
+ # server: localhost
29
+ # user_table: users
30
+ # username_column: username
31
+ # password_column: password
32
+ #
33
+ # When replying to a CAS client's validation request, the server will normally
34
+ # provide the client with the authenticated user's username. However it is now
35
+ # possible for the server to provide the client with additional attributes.
36
+ # You can configure the SQL authenticator to provide data from additional
37
+ # columns in the users table by listing the names of the columns under the
38
+ # 'extra_attributes' option. Note though that this functionality is experimental.
39
+ # It should work with RubyCAS-Client, but may or may not work with other CAS
40
+ # clients.
41
+ #
42
+ # For example, with this configuration, the 'full_name' and 'access_level'
43
+ # columns will be provided to your CAS clients along with the username:
44
+ #
45
+ # authenticator:
46
+ # class: CASServer::Authenticators::SQL
47
+ # database:
48
+ # adapter: mysql
49
+ # database: some_database_with_users_table
50
+ # user_table: users
51
+ # username_column: username
52
+ # password_column: password
53
+ # ignore_type_column: true # indicates if you want to ignore Single Table Inheritance 'type' field
54
+ # extra_attributes: full_name, access_level
55
+ #
56
+ class CASServer::Authenticators::SQL < CASServer::Authenticators::Base
57
+ def self.setup(options)
58
+ raise CASServer::AuthenticatorError, "Invalid authenticator configuration!" unless options[:database]
59
+
60
+ user_model_name = "CASUser_#{options[:auth_index]}"
61
+ $LOG.debug "CREATING USER MODEL #{user_model_name}"
62
+
63
+ class_eval %{
64
+ class #{user_model_name} < ActiveRecord::Base
65
+ end
66
+ }
67
+
68
+ @user_model = const_get(user_model_name)
69
+ @user_model.establish_connection(options[:database])
70
+ @user_model.set_table_name(options[:user_table] || 'users')
71
+ @user_model.inheritance_column = 'no_inheritance_column' if options[:ignore_type_column]
72
+ end
73
+
74
+ def self.user_model
75
+ @user_model
76
+ end
77
+
78
+ def validate(credentials)
79
+ read_standard_credentials(credentials)
80
+ raise_if_not_configured
81
+
82
+ user_model = self.class.user_model
83
+
84
+ username_column = @options[:username_column] || 'username'
85
+ password_column = @options[:password_column] || 'password'
86
+
87
+ $LOG.debug "#{self.class}: [#{user_model}] " + "Connection pool size: #{user_model.connection_pool.instance_variable_get(:@checked_out).length}/#{user_model.connection_pool.instance_variable_get(:@connections).length}"
88
+ results = user_model.find(:all, :conditions => ["#{username_column} = ? AND #{password_column} = ?", @username, @password])
89
+ user_model.connection_pool.checkin(user_model.connection)
90
+
91
+ if results.size > 0
92
+ $LOG.warn("#{self.class}: Multiple matches found for user #{@username.inspect}") if results.size > 1
93
+
94
+ unless @options[:extra_attributes].blank?
95
+ if results.size > 1
96
+ $LOG.warn("#{self.class}: Unable to extract extra_attributes because multiple matches were found for #{@username.inspect}")
97
+ else
98
+ user = results.first
99
+
100
+ extract_extra(user)
101
+ log_extra
102
+ end
103
+ end
104
+
105
+ return true
106
+ else
107
+ return false
108
+ end
109
+ end
110
+
111
+ protected
112
+
113
+ def raise_if_not_configured
114
+ raise CASServer::AuthenticatorError.new(
115
+ "Cannot validate credentials because the authenticator hasn't yet been configured"
116
+ ) unless @options
117
+ end
118
+
119
+ def extract_extra user
120
+ @extra_attributes = {}
121
+ extra_attributes_to_extract.each do |col|
122
+ @extra_attributes[col] = user.send(col)
123
+ end
124
+ end
125
+
126
+ def log_extra
127
+ if @extra_attributes.empty?
128
+ $LOG.warn("#{self.class}: Did not read any extra_attributes for user #{@username.inspect} even though an :extra_attributes option was provided.")
129
+ else
130
+ $LOG.debug("#{self.class}: Read the following extra_attributes for user #{@username.inspect}: #{@extra_attributes.inspect}")
131
+ end
132
+ end
133
+ end