synapse-rubycas-server 1.1.3alpha

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 (88) hide show
  1. checksums.yaml +15 -0
  2. data/CHANGELOG +353 -0
  3. data/Gemfile +12 -0
  4. data/LICENSE +26 -0
  5. data/README.md +38 -0
  6. data/Rakefile +3 -0
  7. data/bin/rubycas-server +30 -0
  8. data/config/config.example.yml +552 -0
  9. data/config/unicorn.rb +88 -0
  10. data/config.ru +11 -0
  11. data/db/migrate/001_create_initial_structure.rb +47 -0
  12. data/db/migrate/002_add_indexes_for_performance.rb +15 -0
  13. data/lib/casserver/authenticators/active_directory_ldap.rb +17 -0
  14. data/lib/casserver/authenticators/active_resource.rb +113 -0
  15. data/lib/casserver/authenticators/authlogic_crypto_providers/aes256.rb +43 -0
  16. data/lib/casserver/authenticators/authlogic_crypto_providers/bcrypt.rb +92 -0
  17. data/lib/casserver/authenticators/authlogic_crypto_providers/md5.rb +34 -0
  18. data/lib/casserver/authenticators/authlogic_crypto_providers/sha1.rb +59 -0
  19. data/lib/casserver/authenticators/authlogic_crypto_providers/sha512.rb +50 -0
  20. data/lib/casserver/authenticators/base.rb +70 -0
  21. data/lib/casserver/authenticators/client_certificate.rb +47 -0
  22. data/lib/casserver/authenticators/google.rb +62 -0
  23. data/lib/casserver/authenticators/ldap.rb +131 -0
  24. data/lib/casserver/authenticators/ntlm.rb +88 -0
  25. data/lib/casserver/authenticators/open_id.rb +19 -0
  26. data/lib/casserver/authenticators/sql.rb +158 -0
  27. data/lib/casserver/authenticators/sql_authlogic.rb +93 -0
  28. data/lib/casserver/authenticators/sql_bcrypt.rb +17 -0
  29. data/lib/casserver/authenticators/sql_encrypted.rb +75 -0
  30. data/lib/casserver/authenticators/sql_md5.rb +19 -0
  31. data/lib/casserver/authenticators/sql_rest_auth.rb +82 -0
  32. data/lib/casserver/authenticators/test.rb +21 -0
  33. data/lib/casserver/base.rb +13 -0
  34. data/lib/casserver/cas.rb +324 -0
  35. data/lib/casserver/core_ext/directory_user.rb +81 -0
  36. data/lib/casserver/core_ext/securerandom.rb +17 -0
  37. data/lib/casserver/core_ext/string.rb +22 -0
  38. data/lib/casserver/core_ext.rb +12 -0
  39. data/lib/casserver/model/consumable.rb +31 -0
  40. data/lib/casserver/model/ticket.rb +19 -0
  41. data/lib/casserver/model.rb +248 -0
  42. data/lib/casserver/server.rb +796 -0
  43. data/lib/casserver/utils.rb +20 -0
  44. data/lib/casserver/views/_login_form.erb +42 -0
  45. data/lib/casserver/views/layout.erb +18 -0
  46. data/lib/casserver/views/login.erb +30 -0
  47. data/lib/casserver/views/proxy.builder +13 -0
  48. data/lib/casserver/views/proxy_validate.builder +31 -0
  49. data/lib/casserver/views/service_validate.builder +24 -0
  50. data/lib/casserver/views/validate.erb +2 -0
  51. data/lib/casserver.rb +19 -0
  52. data/locales/de.yml +27 -0
  53. data/locales/en.yml +26 -0
  54. data/locales/es.yml +26 -0
  55. data/locales/es_ar.yml +26 -0
  56. data/locales/fr.yml +26 -0
  57. data/locales/it.yml +26 -0
  58. data/locales/jp.yml +26 -0
  59. data/locales/pl.yml +26 -0
  60. data/locales/pt.yml +26 -0
  61. data/locales/ru.yml +26 -0
  62. data/locales/zh.yml +26 -0
  63. data/locales/zh_tw.yml +26 -0
  64. data/public/themes/cas.css +126 -0
  65. data/public/themes/notice.png +0 -0
  66. data/public/themes/ok.png +0 -0
  67. data/public/themes/simple/bg.png +0 -0
  68. data/public/themes/simple/favicon.png +0 -0
  69. data/public/themes/simple/login_box_bg.png +0 -0
  70. data/public/themes/simple/logo.png +0 -0
  71. data/public/themes/simple/theme.css +28 -0
  72. data/public/themes/warning.png +0 -0
  73. data/resources/init.d.sh +58 -0
  74. data/spec/casserver/authenticators/active_resource_spec.rb +116 -0
  75. data/spec/casserver/authenticators/ldap_spec.rb +57 -0
  76. data/spec/casserver/cas_spec.rb +148 -0
  77. data/spec/casserver/model_spec.rb +42 -0
  78. data/spec/casserver/utils_spec.rb +24 -0
  79. data/spec/casserver_spec.rb +221 -0
  80. data/spec/config/alt_config.yml +50 -0
  81. data/spec/config/default_config.yml +56 -0
  82. data/spec/core_ext/string_spec.rb +28 -0
  83. data/spec/spec.opts +4 -0
  84. data/spec/spec_helper.rb +126 -0
  85. data/tasks/bundler.rake +4 -0
  86. data/tasks/db/migrate.rake +12 -0
  87. data/tasks/spec.rake +10 -0
  88. metadata +405 -0
@@ -0,0 +1,15 @@
1
+ class AddIndexesForPerformance < ActiveRecord::Migration
2
+ def self.up
3
+ add_index :casserver_lt, :ticket
4
+ add_index :casserver_st, :ticket
5
+ add_index :casserver_tgt, :ticket
6
+ add_index :casserver_pgt, :ticket
7
+ end
8
+
9
+ def self.down
10
+ remove_index :casserver_pgt, :ticket
11
+ remove_index :casserver_st, :ticket
12
+ remove_index :casserver_lt, :ticket
13
+ remove_index :casserver_tgt, :ticket
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ # Slightly modified version of the LDAP authenticator for Microsoft's ActiveDirectory.
2
+ # The only difference is that the default_username_attribute for AD is 'sAMAccountName'
3
+ # rather than 'uid'.
4
+ class CASServer::Authenticators::ActiveDirectoryLDAP < CASServer::Authenticators::LDAP
5
+ protected
6
+ def default_username_attribute
7
+ "sAMAccountName"
8
+ end
9
+
10
+ def extract_extra_attributes(ldap_entry)
11
+ super(ldap_entry)
12
+ if @extra_attributes["objectGUID"]
13
+ @extra_attributes["guid"] = @extra_attributes["objectGUID"].to_s.unpack("H*").to_s
14
+ end
15
+ ldap_entry
16
+ end
17
+ end
@@ -0,0 +1,113 @@
1
+ begin
2
+ require 'active_resource'
3
+ rescue LoadError
4
+ $stderr.puts "To use the ActiveResource authenticator, you must first install gems from active_resource group. See: Gemfile"
5
+ end
6
+
7
+ module CASServer
8
+ module Authenticators
9
+
10
+ module Helpers
11
+ class Identity < ActiveResource::Base
12
+
13
+ # define method_name accessor
14
+ cattr_accessor(:method_name)
15
+ self.method_name = :authenticate
16
+
17
+ def self.method_type
18
+ @@method_type ||= :post
19
+ end
20
+
21
+ def self.method_type= type
22
+ methods = [:get, :post, :put, :delete]
23
+ raise ArgumentError, "Method type should be one of #{methods.map { |m| m.to_s.upcase }.join(', ')}" unless methods.include? type.to_sym
24
+ @@method_type = type
25
+ end
26
+
27
+ # Autenticate an identity using the given method
28
+ # @param [Hash] credentials
29
+ def self.authenticate(credentials = {})
30
+ response = send(method_type, method_name, credentials)
31
+ new.from_authentication_data(response)
32
+ end
33
+
34
+ # Used to load object attributes from the given response
35
+ def from_authentication_data response
36
+ load_attributes_from_response(response)
37
+ end
38
+ end
39
+ end
40
+
41
+ class ActiveResource < Base
42
+
43
+ # This is called at server startup.
44
+ # Any class-wide initializiation for the authenticator should be done here.
45
+ # (e.g. establish database connection).
46
+ # You can leave this empty if you don't need to set up anything.
47
+ def self.setup(options)
48
+ raise AuthenticatorError, 'You must define at least site option' unless options[:site]
49
+ # apply options to active resource object
50
+ options.each do |method, arg|
51
+ Helpers::Identity.send "#{method}=", arg if Helpers::Identity.respond_to? "#{method}="
52
+ end
53
+ $LOG.info "ActiveResource configuration loaded"
54
+ end
55
+
56
+ # Override this to implement your authentication credential validation.
57
+ # This is called each time the user tries to log in. The credentials hash
58
+ # holds the credentials as entered by the user (generally under :username
59
+ # and :password keys; :service and :request are also included by default)
60
+ #
61
+ # Note that the standard credentials can be read in to instance variables
62
+ # by calling #read_standard_credentials.
63
+ def validate(credentials)
64
+ begin
65
+ $LOG.debug("Starting Active Resource authentication")
66
+ result = Helpers::Identity.authenticate(credentials.except(:request))
67
+ extract_extra_attributes(result) if result
68
+ !!result
69
+ rescue ::ActiveResource::ConnectionError => e
70
+ if e.response.blank? # band-aid for ARes 2.3.x -- craps out if to_s is called without a response
71
+ e = e.class.to_s
72
+ end
73
+ $LOG.warn("Error during authentication: #{e}")
74
+ false
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def extract_extra_attributes(resource)
81
+ @extra_attributes = {}
82
+ $LOG.debug("Parsing extra attributes")
83
+ if @options[:extra_attributes]
84
+ extra_attributes_to_extract.each do |attr|
85
+ @extra_attributes[attr] = resource.send(attr).to_s
86
+ end
87
+ else
88
+ @extra_attributes = resource.attributes
89
+ end
90
+ # do filtering
91
+ extra_attributes_to_filter.each do |attr|
92
+ @extra_attributes.delete(attr)
93
+ end
94
+ end
95
+
96
+ # extract attributes to filter from the given configuration
97
+ def extra_attributes_to_filter
98
+ # default value if not set
99
+ return ['password'] unless @options[:filter_attributes]
100
+ # parse option value
101
+ if @options[:filter_attributes].kind_of? Array
102
+ attrs = @options[:filter_attributes]
103
+ elsif @options[:filter_attributes].kind_of? String
104
+ attrs = @options[:filter_attributes].split(',').collect { |col| col.strip }
105
+ else
106
+ $LOG.error("Can't figure out attribute list from #{@options[:filter_attributes].inspect}. This must be an Array of column names or a comma-separated list.")
107
+ attrs = []
108
+ end
109
+ attrs
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,43 @@
1
+ require "openssl"
2
+
3
+ module Authlogic
4
+ module CryptoProviders
5
+ # This encryption method is reversible if you have the supplied key. So in order to use this encryption method you must supply it with a key first.
6
+ # In an initializer, or before your application initializes, you should do the following:
7
+ #
8
+ # Authlogic::CryptoProviders::AES256.key = "my really long and unique key, preferrably a bunch of random characters"
9
+ #
10
+ # My final comment is that this is a strong encryption method, but its main weakness is that its reversible. If you do not need to reverse the hash
11
+ # then you should consider Sha512 or BCrypt instead.
12
+ #
13
+ # Keep your key in a safe place, some even say the key should be stored on a separate server.
14
+ # This won't hurt performance because the only time it will try and access the key on the separate server is during initialization, which only
15
+ # happens once. The reasoning behind this is if someone does compromise your server they won't have the key also. Basically, you don't want to
16
+ # store the key with the lock.
17
+ class AES256
18
+ class << self
19
+ attr_writer :key
20
+
21
+ def encrypt(*tokens)
22
+ aes.encrypt
23
+ aes.key = @key
24
+ [aes.update(tokens.join) + aes.final].pack("m").chomp
25
+ end
26
+
27
+ def matches?(crypted, *tokens)
28
+ aes.decrypt
29
+ aes.key = @key
30
+ (aes.update(crypted.unpack("m").first) + aes.final) == tokens.join
31
+ rescue OpenSSL::CipherError
32
+ false
33
+ end
34
+
35
+ private
36
+ def aes
37
+ raise ArgumentError.new("You must provide a key like #{name}.key = my_key before using the #{name}") if @key.blank?
38
+ @aes ||= OpenSSL::Cipher::Cipher.new("AES-256-ECB")
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,92 @@
1
+ begin
2
+ require "bcrypt"
3
+ rescue LoadError
4
+ end
5
+
6
+ module Authlogic
7
+ module CryptoProviders
8
+ # For most apps Sha512 is plenty secure, but if you are building an app that stores nuclear launch codes you might want to consier BCrypt. This is an extremely
9
+ # secure hashing algorithm, mainly because it is slow. A brute force attack on a BCrypt encrypted password would take much longer than a brute force attack on a
10
+ # password encrypted with a Sha algorithm. Keep in mind you are sacrificing performance by using this, generating a password takes exponentially longer than any
11
+ # of the Sha algorithms. I did some benchmarking to save you some time with your decision:
12
+ #
13
+ # require "bcrypt"
14
+ # require "digest"
15
+ # require "benchmark"
16
+ #
17
+ # Benchmark.bm(18) do |x|
18
+ # x.report("BCrypt (cost = 10:") { 100.times { BCrypt::Password.create("mypass", :cost => 10) } }
19
+ # x.report("BCrypt (cost = 2:") { 100.times { BCrypt::Password.create("mypass", :cost => 2) } }
20
+ # x.report("Sha512:") { 100.times { Digest::SHA512.hexdigest("mypass") } }
21
+ # x.report("Sha1:") { 100.times { Digest::SHA1.hexdigest("mypass") } }
22
+ # end
23
+ #
24
+ # user system total real
25
+ # BCrypt (cost = 10): 10.780000 0.060000 10.840000 ( 11.100289)
26
+ # BCrypt (cost = 2): 0.180000 0.000000 0.180000 ( 0.181914)
27
+ # Sha512: 0.000000 0.000000 0.000000 ( 0.000829)
28
+ # Sha1: 0.000000 0.000000 0.000000 ( 0.000395)
29
+ #
30
+ # You can play around with the cost to get that perfect balance between performance and security.
31
+ #
32
+ # Decided BCrypt is for you? Just insall the bcrypt gem:
33
+ #
34
+ # gem install bcrypt-ruby
35
+ #
36
+ # Tell acts_as_authentic to use it:
37
+ #
38
+ # acts_as_authentic do |c|
39
+ # c.crypto_provider = Authlogic::CryptoProviders::BCrypt
40
+ # end
41
+ #
42
+ # You are good to go!
43
+ class BCrypt
44
+ class << self
45
+ # This is the :cost option for the BCrpyt library. The higher the cost the more secure it is and the longer is take the generate a hash. By default this is 10.
46
+ # Set this to whatever you want, play around with it to get that perfect balance between security and performance.
47
+ def cost
48
+ @cost ||= 10
49
+ end
50
+ attr_writer :cost
51
+
52
+ # Creates a BCrypt hash for the password passed.
53
+ def encrypt(*tokens)
54
+ ::BCrypt::Password.create(join_tokens(tokens), :cost => cost)
55
+ end
56
+
57
+ # Does the hash match the tokens? Uses the same tokens that were used to encrypt.
58
+ def matches?(hash, *tokens)
59
+ $LOG.debug hash
60
+ $LOG.debug tokens.inspect
61
+
62
+ hash = new_from_hash(hash)
63
+ return false if hash.blank?
64
+ hash == join_tokens(tokens)
65
+ end
66
+
67
+ # This method is used as a flag to tell Authlogic to "resave" the password upon a successful login, using the new cost
68
+ def cost_matches?(hash)
69
+ hash = new_from_hash(hash)
70
+ if hash.blank?
71
+ false
72
+ else
73
+ hash.cost == cost
74
+ end
75
+ end
76
+
77
+ private
78
+ def join_tokens(tokens)
79
+ tokens.flatten.join
80
+ end
81
+
82
+ def new_from_hash(hash)
83
+ begin
84
+ ::BCrypt::Password.new(hash)
85
+ rescue ::BCrypt::Errors::InvalidHash
86
+ return nil
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,34 @@
1
+ require "digest/md5"
2
+
3
+ module Authlogic
4
+ module CryptoProviders
5
+ # This class was made for the users transitioning from md5 based systems.
6
+ # I highly discourage using this crypto provider as it superbly inferior
7
+ # to your other options.
8
+ #
9
+ # Please use any other provider offered by Authlogic.
10
+ class MD5
11
+ class << self
12
+ attr_accessor :join_token
13
+
14
+ # The number of times to loop through the encryption.
15
+ def stretches
16
+ @stretches ||= 1
17
+ end
18
+ attr_writer :stretches
19
+
20
+ # Turns your raw password into a MD5 hash.
21
+ def encrypt(*tokens)
22
+ digest = tokens.flatten.join(join_token)
23
+ stretches.times { digest = Digest::MD5.hexdigest(digest) }
24
+ digest
25
+ end
26
+
27
+ # Does the crypted password match the tokens? Uses the same tokens that were used to encrypt.
28
+ def matches?(crypted, *tokens)
29
+ encrypt(*tokens) == crypted
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,59 @@
1
+ require "digest/sha1"
2
+
3
+ module Authlogic
4
+ module CryptoProviders
5
+ # This class was made for the users transitioning from restful_authentication.
6
+ # I highly discourage using this crypto provider as it inferior to your other options.
7
+ # Please use any other provider offered by Authlogic.
8
+ class Sha1
9
+ class << self
10
+ def join_token
11
+ @join_token ||= "--"
12
+ end
13
+ attr_writer :join_token
14
+
15
+ def digest_format=(format)
16
+ @digest_format = format
17
+ end
18
+
19
+ # This is for "old style" authentication with a custom format of digest
20
+ def digest(tokens)
21
+ if @digest_format
22
+ @digest_format.
23
+ gsub('PASSWORD', tokens.first).
24
+ gsub('SALT', tokens.last)
25
+ else
26
+ tokens.join(join_token)
27
+ end
28
+ end
29
+
30
+ # The number of times to loop through the encryption.
31
+ # This is ten because that is what restful_authentication defaults to.
32
+
33
+ def stretches
34
+ @stretches ||= 10
35
+ end
36
+ attr_writer :stretches
37
+
38
+ # Turns your raw password into a Sha1 hash.
39
+ def encrypt(*tokens)
40
+ tokens = tokens.flatten
41
+
42
+ if stretches > 1
43
+ hash = tokens.shift
44
+ stretches.times { hash = Digest::SHA1.hexdigest([hash, *tokens].join(join_token)) }
45
+ else
46
+ hash = Digest::SHA1.hexdigest( digest(tokens) )
47
+ end
48
+
49
+ hash
50
+ end
51
+
52
+ # Does the crypted password match the tokens? Uses the same tokens that were used to encrypt.
53
+ def matches?(crypted, *tokens)
54
+ encrypt(*tokens) == crypted
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,50 @@
1
+ require "digest/sha2"
2
+
3
+ module Authlogic
4
+ # The acts_as_authentic method has a crypto_provider option. This allows you to use any type of encryption you like.
5
+ # Just create a class with a class level encrypt and matches? method. See example below.
6
+ #
7
+ # === Example
8
+ #
9
+ # class MyAwesomeEncryptionMethod
10
+ # def self.encrypt(*tokens)
11
+ # # the tokens passed will be an array of objects, what type of object is irrelevant,
12
+ # # just do what you need to do with them and return a single encrypted string.
13
+ # # for example, you will most likely join all of the objects into a single string and then encrypt that string
14
+ # end
15
+ #
16
+ # def self.matches?(crypted, *tokens)
17
+ # # return true if the crypted string matches the tokens.
18
+ # # depending on your algorithm you might decrypt the string then compare it to the token, or you might
19
+ # # encrypt the tokens and make sure it matches the crypted string, its up to you
20
+ # end
21
+ # end
22
+ module CryptoProviders
23
+ # = Sha512
24
+ #
25
+ # Uses the Sha512 hash algorithm to encrypt passwords.
26
+ class Sha512
27
+ class << self
28
+ attr_accessor :join_token
29
+
30
+ # The number of times to loop through the encryption. This is ten because that is what restful_authentication defaults to.
31
+ def stretches
32
+ @stretches ||= 20
33
+ end
34
+ attr_writer :stretches
35
+
36
+ # Turns your raw password into a Sha512 hash.
37
+ def encrypt(*tokens)
38
+ digest = tokens.flatten.join(join_token)
39
+ stretches.times { digest = Digest::SHA512.hexdigest(digest) }
40
+ digest
41
+ end
42
+
43
+ # Does the crypted password match the tokens? Uses the same tokens that were used to encrypt.
44
+ def matches?(crypted, *tokens)
45
+ encrypt(*tokens) == crypted
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,70 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext'
3
+
4
+ module CASServer
5
+ module Authenticators
6
+ class Base
7
+ attr_accessor :options
8
+ attr_reader :username # make this accessible so that we can pick up any
9
+ # transformations done within the authenticator
10
+
11
+ # This is called at server startup.
12
+ # Any class-wide initializiation for the authenticator should be done here.
13
+ # (e.g. establish database connection).
14
+ # You can leave this empty if you don't need to set up anything.
15
+ def self.setup(options)
16
+ end
17
+
18
+ # This is called prior to #validate (i.e. each time the user tries to log in).
19
+ # Any per-instance initialization for the authenticator should be done here.
20
+ #
21
+ # By default this makes the authenticator options hash available for #validate
22
+ # under @options and initializes @extra_attributes to an empty hash.
23
+ def configure(options)
24
+ raise ArgumentError, "options must be a HashWithIndifferentAccess" unless options.kind_of? HashWithIndifferentAccess
25
+ @options = options.dup
26
+ @extra_attributes = {}
27
+ end
28
+
29
+ # Override this to implement your authentication credential validation.
30
+ # This is called each time the user tries to log in. The credentials hash
31
+ # holds the credentials as entered by the user (generally under :username
32
+ # and :password keys; :service and :request are also included by default)
33
+ #
34
+ # Note that the standard credentials can be read in to instance variables
35
+ # by calling #read_standard_credentials.
36
+ def validate(credentials)
37
+ raise NotImplementedError, "This method must be implemented by a class extending #{self.class}"
38
+ end
39
+
40
+ def extra_attributes
41
+ @extra_attributes
42
+ end
43
+
44
+ protected
45
+ def read_standard_credentials(credentials)
46
+ @username = credentials[:username]
47
+ @password = credentials[:password]
48
+ @service = credentials[:service]
49
+ @request = credentials[:request]
50
+ end
51
+
52
+ def extra_attributes_to_extract
53
+ if @options[:extra_attributes].kind_of? Array
54
+ attrs = @options[:extra_attributes]
55
+ elsif @options[:extra_attributes].kind_of? String
56
+ attrs = @options[:extra_attributes].split(',').collect{|col| col.strip}
57
+ else
58
+ $LOG.error("Can't figure out attribute list from #{@options[:extra_attributes].inspect}. This must be an Array of column names or a comma-separated list.")
59
+ attrs = []
60
+ end
61
+
62
+ $LOG.debug("#{self.class.name} will try to extract the following extra_attributes: #{attrs.inspect}")
63
+ return attrs
64
+ end
65
+ end
66
+ end
67
+
68
+ class AuthenticatorError < Exception
69
+ end
70
+ end
@@ -0,0 +1,47 @@
1
+ require 'casserver/authenticators/base'
2
+
3
+ # NOT YET IMPLEMENTED
4
+ #
5
+ # This authenticator will authenticate the user based on a client SSL certificate.
6
+ #
7
+ # You will probably want to use this along with another authenticator, chaining
8
+ # it so that if the client does not provide a certificate, the server can
9
+ # fall back to some other authentication mechanism.
10
+ #
11
+ # Here's an example of how to use two chained authenticators in the config.yml
12
+ # file. The server will first use the ClientCertificate authenticator, and
13
+ # only fall back to the SQL authenticator of the first one fails:
14
+ #
15
+ # authenticator:
16
+ # -
17
+ # class: CASServer::Authenticators::ClientCertificate
18
+ # -
19
+ # class: CASServer::Authenticators::SQL
20
+ # database:
21
+ # adapter: mysql
22
+ # database: some_database_with_users_table
23
+ # user: root
24
+ # password:
25
+ # server: localhost
26
+ # user_table: user
27
+ # username_column: username
28
+ # password_column: password
29
+ #
30
+ class CASServer::Authenticators::ClientCertificate < CASServer::Authenticators::Base
31
+ def validate(credentials)
32
+ read_standard_credentials(credentials)
33
+
34
+ @client_cert = credentials[:request]['SSL_CLIENT_CERT']
35
+
36
+ # note that I haven't actually tested to see if SSL_CLIENT_CERT gets
37
+ # filled with data when a client cert is provided, but this should be
38
+ # the case at least in theory :)
39
+
40
+ return false if @client_cert.blank?
41
+
42
+ # IMPLEMENT SSL CERTIFICATE VALIDATION CODE HERE
43
+ raise NotImplementedError, "#{self.class.name}#validate NOT YET IMPLEMENTED!"
44
+
45
+ return true # if SSL certificate is valid, false otherwise
46
+ end
47
+ end
@@ -0,0 +1,62 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'net/https'
4
+ require 'timeout'
5
+
6
+ # Validates Google accounts against Google's authentication service -- in other
7
+ # words, this authenticator allows users to log in to CAS using their
8
+ # Gmail/Google accounts.
9
+ class CASServer::Authenticators::Google < CASServer::Authenticators::Base
10
+ def validate(credentials)
11
+ read_standard_credentials(credentials)
12
+
13
+ return false if @username.blank? || @password.blank?
14
+
15
+ if @options[:restricted_domain]
16
+ return false if @username.count('@') != 1
17
+ return false if @username.split('@').last != @options[:restricted_domain]
18
+ end
19
+
20
+ auth_data = {
21
+ 'Email' => @username,
22
+ 'Passwd' => @password,
23
+ 'service' => 'xapi',
24
+ 'source' => 'RubyCAS-Server',
25
+ 'accountType' => 'HOSTED_OR_GOOGLE'
26
+ }
27
+
28
+ url = URI.parse('https://www.google.com/accounts/ClientLogin')
29
+ if @options[:proxy]
30
+ http = Net::HTTP.Proxy(@options[:proxy][:host], @options[:proxy][:port], @options[:proxy][:username], @options[:proxy][:password]).new(url.host, url.port)
31
+ else
32
+ http = Net::HTTP.new(url.host, url.port)
33
+ end
34
+ http.use_ssl = true
35
+
36
+ # TODO: make the timeout configurable
37
+ wait_seconds = 10
38
+ begin
39
+ timeout(wait_seconds) do
40
+ res = http.start do |conn|
41
+ req = Net::HTTP::Post.new(url.path)
42
+ req.set_form_data(auth_data,'&')
43
+ conn.request(req)
44
+ end
45
+
46
+ case res
47
+ when Net::HTTPSuccess
48
+ true
49
+ when Net::HTTPForbidden
50
+ false
51
+ else
52
+ $LOG.error("Unexpected response from Google while validating credentials: #{res.inspect} ==> #{res.body}.")
53
+ raise CASServer::AuthenticatorError, "Unexpected response received from Google while validating credentials."
54
+ end
55
+ end
56
+ rescue Timeout::Error
57
+ $LOG.error("Google did not respond to the credential validation request. We waited for #{wait_seconds.inspect} seconds before giving up.")
58
+ raise CASServer::AuthenticatorError, "Timeout while waiting for Google to validate credentials."
59
+ end
60
+
61
+ end
62
+ end