cassy 1.1.4 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d02c0cb8713326b7d31872a36f9c654d0e6029f9
4
+ data.tar.gz: b31c080549d0595d9a442b73facffb3711e8143e
5
+ SHA512:
6
+ metadata.gz: 59d9cb6a3fcf836fa1dc313bfb1673b2f575767a07736cb03486b78e3e4f111610a8cc227b18d16965d0fc6ec0f06becf8db20974d2c79d437846d2ab8b7744c
7
+ data.tar.gz: 5f2720661a41e3f6d43bb30cca09988e9f839b0829a5fdecdadbc884e62c9fc31e6ba97068bd50d69934a8862245b49fb22627c4404b47bea2f43ccef987b05d
@@ -0,0 +1,156 @@
1
+ # Cassy
2
+
3
+ This project is designed to be a Rails engine that uses a large portion of the code from the [rubycas-server][https://github.com/gunark/rubycas-server] project. Certain portions of this code belong to the rubycas-server project owners.
4
+
5
+ ## Installation
6
+
7
+ This engine currently only works with Rails 3 and above. To have it work with the application you must do four things:
8
+
9
+ 1. Put this line in your project's `Gemfile`:
10
+
11
+ ```
12
+ gem 'cassy'
13
+ ```
14
+
15
+ 2. Create a configuration file at `config/cassy.yml` and fill it with these values:
16
+
17
+ ```
18
+ # Times are in seconds.
19
+ maximum_unused_login_ticket_lifetime: 300
20
+ maximum_unused_service_ticket_lifetime: 300
21
+
22
+ authenticator:
23
+ class: Cassy::Authenticators::Devise
24
+ ```
25
+
26
+ The first two keys are the time-to-expiry for the login and service tickets respectively. The class for the authentication can be any constant which responds to a `validates` method. Only Devise authentication is supported at the moment.
27
+
28
+ 3. Create a new initializer (probably called `config/initializers/cassy.rb`) and point cassy at the configuration file for your application:
29
+
30
+ ```
31
+ Cassy::Engine.config.config_file = Rails.root + "config/cassy.yml"
32
+ ```
33
+
34
+ 4. Tell Cassy to load its routes in your application by calling `cassy` in `config/routes.rb`:
35
+
36
+ ```
37
+ Rails.application.routes.draw do
38
+ cassy
39
+
40
+ # your routes go here
41
+ end
42
+ ```
43
+
44
+ Boom, done. Now this application will act as a CAS server.
45
+
46
+ For customization options please see the "Customization" section below.
47
+
48
+ ## Configuration
49
+
50
+ The configuration options for this gem goes into a file called `config/cassy.yml` at the root of the project if you've set it up as advised, and this allows the engine to be configured.
51
+
52
+ These configuration options are detailed here for your convenience. For specific term definitions, please consult the CAS spec.
53
+
54
+ * `authenticator`: Must specify at least one key, `class`, which is a string version of a constant that will be used for authentication in the system. This constant *must* respond to `validate`.
55
+ * `maximum_unused_login_ticket_lifetime`: The time before a login ticket would expire.
56
+ * `maximum_unused_service_ticket_lifetime`: The time before a service ticket would expire.
57
+ * `username_field`: Defines the field on the users table which is used for the lookup for the username. Defaults to " username".
58
+ * `username_label`: Allows for the "Username" label on the sign in page to be given a different value. Helpful if you want to call it "Email" or "User Name" instead.
59
+ * `client_app_user_field`: Defines the field name for the username on the *client* application side.
60
+ * `service_list`: List of services that use this server to authenticate, separated by environment.
61
+ * `default_redirect_url`: If the requested service isn't in the service_list (or is blank) then tickets will be generated for the valid services then the user will be redirected to here. Needs to be specified per environment as per the sample below. The default_redirect_url needs to be on the same domain as (at least) one of the urls on the service_list.
62
+ * `loosely_match_services`: If this is set to true, a request for the service http://www.something.com/something_else can be matched to the ticket for http://www.something.com.
63
+ * `enable_single_sign_out`: If this is set to true, calling send_logout_notification on a service ticket will send a request to the service telling it to clear the associated users session. Calling destroy_and_logout_all_service_tickets on a ticket granting ticket will send a session-terminating request to each service before destroying itself.
64
+ * `no_concurrent_sessions`: (requires enable_single_sign_out to be true) If this is true, when someone logs in, a session-terminating request is sent to each service for any old service tickets related to the current user.
65
+ * `concurrent_session_types`: If no_concurrent_sessions is true, concurrent_session_types can be specified so that a user can have concurrent sessions on different device types. If enabled, override `session_type` in `SessionsController` to return the session_type (any string).
66
+
67
+ A sample `cassy.yml` file:
68
+
69
+ ```
70
+ maximum_unused_login_ticket_lifetime: 7200
71
+ maximum_unused_service_ticket_lifetime: 7200
72
+ maximum_session_lifetime: 7200
73
+ username_field: username
74
+ client_app_user_field: id
75
+ service_list:
76
+ production:
77
+ - https://agencieshq.com/users/service
78
+ - https://administratorshq.agencieshq.com/users/service
79
+ development:
80
+ - http://localhost:3000/users/service
81
+ - http://localhost:3001/users/service
82
+ - http://localhost:3002/users/service
83
+ default_redirect_url:
84
+ development: http://localhost:3000
85
+ production: http://www.something.com
86
+ loosely_match_services: true
87
+ authenticator:
88
+ class: Cassy::Authenticators::Devise
89
+ no_concurrent_sessions: true
90
+ concurrent_session_types: [:mobile, :desktop]
91
+ extra_attributes:
92
+ - user_id
93
+ - user_username
94
+ ```
95
+
96
+ ## Customization
97
+
98
+ ### Sessions Controller
99
+
100
+ In Cassy, it is possible to override the controller which is used for authentication. To do this, the controller can be configured in `config/routes.rb`:
101
+
102
+ ```
103
+ cassy :controllers => "sessions"
104
+ ```
105
+
106
+ By doing this, it will point at the `SessionsController` rather than the default of `Cassy::SessionsController`. This controller then should inherit from `Cassy::SessionsController` to inherit the original behaviour and will need to point to the views of Cassy:
107
+
108
+ ```
109
+ class SessionsController < Cassy::SessionsController
110
+ def new
111
+ # custom behaviour goes here
112
+ super
113
+ end
114
+ end
115
+ ```
116
+
117
+ ## Contributing
118
+
119
+ ### Versioning
120
+
121
+ We use [Semantic Versioning](http://semver.org/) for our open source dependencies, and a modified version of Semantic Versioning in our internal dependencies.
122
+
123
+ #### Open Source
124
+
125
+ `MAJOR.MINOR.PATCH`
126
+
127
+ **1** MAJOR version when you make incompatible API changes
128
+
129
+ **2** MINOR version when you add functionality in a backwards-compatible manner, and
130
+
131
+ **3** PATCH version when you make backwards-compatible bug fixes.
132
+
133
+ #### Internal
134
+
135
+ **1** MAJOR version when major changes are made.
136
+
137
+ **2** MINOR version when you make incompatible API changes
138
+
139
+ **3** PATCH version when you make any backwards-compatible change.
140
+
141
+ ### Releasing
142
+
143
+ #### Prerequisites
144
+
145
+ - Gemfury CLI installed
146
+ - Gemfury Credentials from 1Password
147
+
148
+ #### Instructions
149
+
150
+ **1.** Update `lib/{gem_name}/version.rb` according to the versioning rules above.
151
+
152
+ **2.** Create a Pull Request with the version change to the repository.
153
+
154
+ **3.** Once the pull request is merged, run `gem build {gem_name}.gemspec`
155
+
156
+ **4.** Run `fury push {gem_name}_{version}.gem`.
data/Rakefile CHANGED
@@ -5,9 +5,25 @@ rescue LoadError
5
5
  puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
6
  end
7
7
 
8
+ # load rspec so we can set up the environment correctly using spec_helper
8
9
  require 'rspec/core/rake_task'
10
+ require_relative './spec/spec_helper.rb'
9
11
  RSpec::Core::RakeTask.new(:spec)
10
12
 
11
13
  Bundler::GemHelper.install_tasks
12
14
 
13
- task :default => :spec
15
+ # load our dummy app so we have access to rails rake tasks such as db:create and db:migrate
16
+ APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
17
+ begin
18
+ load 'rails/tasks/engine.rake'
19
+ rescue LoadError
20
+ # for Rails 3.0.x we need to load the Rakefile directly, as the engine rake task doesn't exist
21
+ load APP_RAKEFILE
22
+ end
23
+
24
+ task(:default).clear
25
+ task :default => [
26
+ 'db:create',
27
+ 'db:migrate',
28
+ 'spec'
29
+ ]
@@ -5,6 +5,7 @@ module Cassy
5
5
  attr_accessor :options
6
6
  attr_reader :username # make this accessible so that we can pick up any
7
7
  # transformations done within the authenticator
8
+ attr_accessor :client_app_user_field
8
9
  end
9
10
 
10
11
  # This is called at server startup.
@@ -18,6 +19,9 @@ module Cassy
18
19
  raise NotImplementedError
19
20
  end
20
21
 
22
+ def self.find_user_from_ticket(ticket)
23
+ raise NotImplementedError
24
+ end
21
25
  # This is called prior to #validate (i.e. each time the user tries to log in).
22
26
  # Any per-instance initialization for the authenticator should be done here.
23
27
  #
@@ -60,8 +64,8 @@ module Cassy
60
64
  def self.read_standard_credentials(credentials)
61
65
  @username = credentials[:username]
62
66
  @password = credentials[:password]
63
- @service = credentials[:service]
64
- @request = credentials[:request]
67
+ @service = credentials[:service]
68
+ @request = credentials[:request]
65
69
  end
66
70
 
67
71
  def extra_attributes_to_extract
@@ -1,18 +1,26 @@
1
1
  module Cassy
2
2
  module Authenticators
3
3
  class Devise < Base
4
-
4
+
5
5
  def self.find_user(credentials)
6
6
  # Find the user with the given email
7
7
  method = "find_by_#{Cassy.config[:username_field] || 'email'}"
8
8
  User.send(method, credentials[:username])
9
9
  end
10
-
10
+
11
+ def self.find_user_from_ticket(ticket)
12
+ return if ticket.nil?
13
+ key = Cassy.config[:client_app_user_field] || Cassy.config[:username_field] || "email"
14
+ method = "find_by_#{key}"
15
+ username = Cassy.config[:concurrent_session_types] ? Cassy.config[:concurrent_session_types].each{ |cst| break ticket.username.rpartition("-#{cst}").first if ticket.username.match("-#{cst}") } : ticket.username
16
+ User.send(method, username)
17
+ end
18
+
11
19
  def self.validate(credentials)
12
20
  user = find_user(credentials)
13
- # Did we find a user, and is their password valid?
14
- user && user.valid_password?(credentials[:password])
21
+ # Did we find a user, are they active? and is their password valid?
22
+ user && user.active_for_authentication? && user.valid_password?(credentials[:password])
15
23
  end
16
24
  end
17
25
  end
18
- end
26
+ end
@@ -15,13 +15,20 @@ class Cassy::Authenticators::Test < Cassy::Authenticators::Base
15
15
 
16
16
  return @password == valid_password
17
17
  end
18
-
18
+
19
19
  def self.find_user(*args)
20
20
  # To stop NotImplementedError raising
21
21
  @user = Object.new
22
22
  def @user.full_name
23
23
  "Example User"
24
24
  end
25
+ def @user.username
26
+ "Users Username"
27
+ end
25
28
  @user
26
29
  end
30
+
31
+ class << self
32
+ alias_method :find_user_from_ticket, :find_user
33
+ end
27
34
  end
@@ -9,19 +9,6 @@ require 'cassy/utils'
9
9
  module Cassy
10
10
  module CAS
11
11
 
12
- class Error
13
- attr_reader :code, :message
14
-
15
- def initialize(code, message)
16
- @code = code
17
- @message = message
18
- end
19
-
20
- def to_s
21
- message
22
- end
23
- end
24
-
25
12
  def settings
26
13
  Cassy.config
27
14
  end
@@ -37,35 +24,15 @@ module Cassy
37
24
  lt
38
25
  end
39
26
 
40
- # Creates a TicketGrantingTicket for the given username. This is done when the user logs in
41
- # for the first time to establish their SSO session (after their credentials have been validated).
42
- #
43
- # The optional 'extra_attributes' parameter takes a hash of additional attributes
44
- # that will be sent along with the username in the CAS response to subsequent
45
- # validation requests from clients.
46
- def generate_ticket_granting_ticket(username, extra_attributes={})
47
- # 3.6 (ticket granting cookie/ticket)
48
- tgt = Cassy::TicketGrantingTicket.new
49
- tgt.ticket = "TGC-" + Cassy::Utils.random_string
50
- tgt.username = username
51
- tgt.extra_attributes = extra_attributes
52
- tgt.client_hostname = env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_HOST'] || env['REMOTE_ADDR']
53
- tgt.save!
54
- tgt
27
+ def find_or_generate_service_tickets(username, tgt, hostname)
28
+ @service_tickets={}
29
+ valid_services.each do |service|
30
+ @service_tickets[service] = Cassy::ServiceTicket.find_or_generate(service, username, tgt, hostname)
31
+ end
55
32
  end
56
33
 
57
- def generate_service_ticket(service, username, tgt)
58
- # 3.1 (service ticket)
59
- st = ServiceTicket.new
60
- st.ticket = "ST-" + Cassy::Utils.random_string
61
- st.service = service
62
- st.username = username
63
- st.granted_by_tgt_id = tgt.id
64
- st.client_hostname = env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_HOST'] || env['REMOTE_ADDR']
65
- st.save!
66
- logger.debug("Generated service ticket '#{st.ticket}' for service '#{st.service}'" +
67
- " for user '#{st.username}' at '#{st.client_hostname}'")
68
- st
34
+ def valid_services
35
+ @valid_services || settings[:service_list][Rails.env]
69
36
  end
70
37
 
71
38
  def generate_proxy_ticket(target_service, pgt)
@@ -99,7 +66,7 @@ module Cassy
99
66
  https.start do |conn|
100
67
  path = uri.path.empty? ? '/' : uri.path
101
68
  path += '?' + uri.query unless (uri.query.nil? || uri.query.empty?)
102
-
69
+
103
70
  pgt = ProxyGrantingTicket.new
104
71
  pgt.ticket = "PGT-" + Cassy::Utils.random_string(60)
105
72
  pgt.iou = "PGTIOU-" + Cassy::Utils.random_string(57)
@@ -114,7 +81,7 @@ module Cassy
114
81
  response = conn.request_get(path)
115
82
  # TODO: follow redirects... 2.5.4 says that redirects MAY be followed
116
83
  # NOTE: The following response codes are valid according to the JA-SIG implementation even without following redirects
117
-
84
+
118
85
  if %w(200 202 301 302 304).include?(response.code)
119
86
  # 3.4 (proxy-granting ticket IOU)
120
87
  pgt.save!
@@ -127,162 +94,12 @@ module Cassy
127
94
  end
128
95
  end
129
96
 
130
- def validate_login_ticket(ticket)
131
- logger.debug("Validating login ticket '#{ticket}'")
132
-
133
- success = false
134
- if ticket.nil?
135
- error = "Your login request did not include a login ticket. There may be a problem with the authentication system."
136
- logger.warn "Missing login ticket."
137
- elsif lt = LoginTicket.find_by_ticket(ticket)
138
- if lt.consumed?
139
- error = "The login ticket you provided has already been used up. Please try logging in again."
140
- logger.warn "Login ticket '#{ticket}' previously used up"
141
- elsif Time.now - lt.created_on < settings[:maximum_unused_login_ticket_lifetime]
142
- logger.info "Login ticket '#{ticket}' successfully validated"
143
- else
144
- error = "You took too long to enter your credentials. Please try again."
145
- logger.warn "Expired login ticket '#{ticket}'"
146
- end
147
- else
148
- error = "The login ticket you provided is invalid. There may be a problem with the authentication system."
149
- logger.warn "Invalid login ticket '#{ticket}'"
150
- end
151
-
152
- lt.consume! if lt && !error.blank?
153
-
154
- error
155
- end
156
-
157
- def validate_ticket_granting_ticket(ticket)
158
- logger.debug("Validating ticket granting ticket '#{ticket}'")
159
-
160
- if ticket.nil?
161
- error = "No ticket granting ticket given."
162
- logger.debug error
163
- elsif tgt = TicketGrantingTicket.find_by_ticket(ticket)
164
- if settings[:maximum_session_lifetime] && Time.now - tgt.created_on > settings[:maximum_session_lifetime]
165
- tgt.destroy
166
- error = "Your session has expired. Please log in again."
167
- logger.info "Ticket granting ticket '#{ticket}' for user '#{tgt.username}' expired."
168
- else
169
- logger.info "Ticket granting ticket '#{ticket}' for user '#{tgt.username}' successfully validated."
170
- end
171
- else
172
- error = "Invalid ticket granting ticket '#{ticket}' (no matching ticket found in the database)."
173
- logger.warn(error)
174
- end
175
-
176
- [tgt, error]
177
- end
178
-
179
- def validate_service_ticket(service, ticket, allow_proxy_tickets = false)
180
- logger.debug "Validating service/proxy ticket '#{ticket}' for service '#{service}'"
181
-
182
- if service.nil? or ticket.nil?
183
- error = Error.new(:INVALID_REQUEST, "Ticket or service parameter was missing in the request.")
184
- logger.warn "#{error.code} - #{error.message}"
185
- elsif st = ServiceTicket.find_by_ticket(ticket)
186
- if st.consumed?
187
- error = Error.new(:INVALID_TICKET, "Ticket '#{ticket}' has already been used up.")
188
- logger.warn "#{error.code} - #{error.message}"
189
- elsif st.kind_of?(Cassy::ProxyTicket) && !allow_proxy_tickets
190
- error = Error.new(:INVALID_TICKET, "Ticket '#{ticket}' is a proxy ticket, but only service tickets are allowed here.")
191
- logger.warn "#{error.code} - #{error.message}"
192
- elsif Time.now - st.created_on > settings[:maximum_unused_service_ticket_lifetime]
193
- error = Error.new(:INVALID_TICKET, "Ticket '#{ticket}' has expired.")
194
- logger.warn "Ticket '#{ticket}' has expired."
195
- elsif !st.matches_service? service
196
- error = Error.new(:INVALID_SERVICE, "The ticket '#{ticket}' belonging to user '#{st.username}' is valid,"+
197
- " but the requested service '#{service}' does not match the service '#{st.service}' associated with this ticket.")
198
- logger.warn "#{error.code} - #{error.message}"
199
- else
200
- logger.info("Ticket '#{ticket}' for service '#{service}' for user '#{st.username}' successfully validated.")
201
- end
202
- else
203
- error = Error.new(:INVALID_TICKET, "Ticket '#{ticket}' not recognized.")
204
- logger.warn("#{error.code} - #{error.message}")
205
- end
206
-
207
- if st
208
- st.consume!
209
- end
210
-
211
-
212
- [st, error]
213
- end
214
-
215
- def validate_proxy_ticket(service, ticket)
216
- pt, error = validate_service_ticket(service, ticket, true)
217
-
218
- if pt.kind_of?(Cassy::ProxyTicket) && !error
219
- if not pt.granted_by_pgt
220
- error = Error.new(:INTERNAL_ERROR, "Proxy ticket '#{pt}' belonging to user '#{pt.username}' is not associated with a proxy granting ticket.")
221
- elsif not pt.granted_by_pgt.service_ticket
222
- error = Error.new(:INTERNAL_ERROR, "Proxy granting ticket '#{pt.granted_by_pgt}'"+
223
- " (associated with proxy ticket '#{pt}' and belonging to user '#{pt.username}' is not associated with a service ticket.")
224
- end
225
- end
226
-
227
- [pt, error]
228
- end
229
-
230
- def validate_proxy_granting_ticket(ticket)
231
- if ticket.nil?
232
- error = Error.new(:INVALID_REQUEST, "pgt parameter was missing in the request.")
233
- logger.warn("#{error.code} - #{error.message}")
234
- elsif pgt = ProxyGrantingTicket.find_by_ticket(ticket)
235
- if pgt.service_ticket
236
- logger.info("Proxy granting ticket '#{ticket}' belonging to user '#{pgt.service_ticket.username}' successfully validated.")
237
- else
238
- error = Error.new(:INTERNAL_ERROR, "Proxy granting ticket '#{ticket}' is not associated with a service ticket.")
239
- logger.error("#{error.code} - #{error.message}")
240
- end
241
- else
242
- error = Error.new(:BAD_PGT, "Invalid proxy granting ticket '#{ticket}' (no matching ticket found in the database).")
243
- logger.warn("#{error.code} - #{error.message}")
244
- end
245
-
246
- [pgt, error]
247
- end
248
-
249
- # Takes an existing ServiceTicket object (presumably pulled from the database)
250
- # and sends a POST with logout information to the service that the ticket
251
- # was generated for.
252
- #
253
- # This makes possible the "single sign-out" functionality added in CAS 3.1.
254
- # See http://www.ja-sig.org/wiki/display/CASUM/Single+Sign+Out
255
- def send_logout_notification_for_service_ticket(st)
256
- uri = URI.parse(st.service)
257
- uri.path = '/' if uri.path.empty?
258
- time = Time.now
259
- rand = Cassy::Utils.random_string
260
-
261
- begin
262
- response = Net::HTTP.post_form(uri, {'logoutRequest' => URI.escape(%{<samlp:LogoutRequest ID="#{rand}" Version="2.0" IssueInstant="#{time.rfc2822}">
263
- <saml:NameID></saml:NameID>
264
- <samlp:SessionIndex>#{st.ticket}</samlp:SessionIndex>
265
- </samlp:LogoutRequest>})})
266
- if response.kind_of? Net::HTTPSuccess
267
- logger.info "Logout notification successfully posted to #{st.service.inspect}."
268
- return true
269
- else
270
- logger.error "Service #{st.service.inspect} responed to logout notification with code '#{response.code}'!"
271
- return false
272
- end
273
- rescue Exception => e
274
- logger.error "Failed to send logout notification to service #{st.service.inspect} due to #{e}"
275
- return false
276
- end
277
- end
278
-
279
97
  def service_uri_with_ticket(service, st)
280
98
  raise ArgumentError, "Second argument must be a ServiceTicket!" unless st.kind_of? Cassy::ServiceTicket
281
99
 
282
100
  # This will choke with a URI::InvalidURIError if service URI is not properly URI-escaped...
283
101
  # This exception is handled further upstream (i.e. in the controller).
284
102
  service_uri = URI.parse(service)
285
-
286
103
  if service.include? "?"
287
104
  if service_uri.query.empty?
288
105
  query_separator = ""
@@ -325,5 +142,105 @@ module Cassy
325
142
  end
326
143
  module_function :clean_service_url
327
144
 
145
+ def base_service_url(full_service_url)
146
+ # strips a url back to the domain part only
147
+ # so that a service ticket can work for all urls on a given domain
148
+ # eg http://www.something.com/something_else
149
+ # is stripped back to
150
+ # http://www.something.com
151
+ # expects it to be in 'http://x' form
152
+ return unless full_service_url
153
+ match = full_service_url.match(/(http(s?):\/\/[a-z0-9\.:]*)/)
154
+ match && match[0]
155
+ end
156
+ module_function :base_service_url
157
+
158
+ def detect_ticketing_service(service)
159
+ # try to find the service in the valid_services list
160
+ # if loosely_matched_services is true, try to match the base url of the service to one in the valid_services list
161
+ # if still no luck, check if there is a default_redirect_url that we can use
162
+ @service||= service
163
+ @ticketing_service||= valid_services.detect{|s| s == @service } ||
164
+ (settings[:loosely_match_services] == true && valid_services.detect{|s| base_service_url(s) == base_service_url(@service)})
165
+ if !@ticketing_service && settings[:default_redirect_url]
166
+ @default_redirect_url||= settings[:default_redirect_url][Rails.env]
167
+ @ticketing_service = @default_redirect_url
168
+ end
169
+ @username||= params[:username].try(:strip)
170
+ @password||= params[:password]
171
+ @lt||= params['lt']
172
+ end
173
+ module_function :detect_ticketing_service
174
+
175
+ def cas_login(session_type = nil)
176
+ if valid_credentials?
177
+ username = ticket_username
178
+ username = "#{username}-#{session_type}" if Cassy.config[:concurrent_session_types]
179
+
180
+ @tgt||= Cassy::TicketGrantingTicket.generate(username, @extra_attributes, @hostname)
181
+ @existing_ticket_for_service = @tgt.granted_service_tickets.where(:service => @service).where("created_on > ?", Time.now - Cassy.config[:maximum_session_lifetime]).where("consumed IS NOT NULL").first
182
+ response.set_cookie('tgt', @tgt.to_s)
183
+ if @ticketing_service
184
+ find_or_generate_service_tickets(username, @tgt, @hostname)
185
+ @st = @service_tickets[@ticketing_service]
186
+ @service_with_ticket = @service && @st ? service_uri_with_ticket(@service, @st) : @default_redirect_url
187
+ end
188
+ # if there is an existing ticket for the service that redirected to cassy,
189
+ # then the session isn't valid on the serviced app and we don't want to redirect to it.
190
+ # Returning false here will fail the cas_login and the controller will see the instance variable and redirect to logout.
191
+ !@existing_ticket_for_service
192
+ else
193
+ false
194
+ end
195
+ end
196
+ module_function :cas_login
197
+
198
+ # Initializes authenticator, returns true / false depending on if user credentials are accurate
199
+ def valid_credentials?
200
+ detect_ticketing_service(params[:service])
201
+ @extra_attributes = {}
202
+ # Should probably be moved out of the request cycle and into an after init hook on the engine
203
+
204
+ credentials = { :username => @username,
205
+ :password => @password,
206
+ :service => @service,
207
+ :request => @env
208
+ }
209
+ @user = authenticator.find_user(credentials) || authenticator.find_user_from_ticket(@tgt)
210
+ valid = ((@user == @ticketed_user) || authenticator.validate(credentials)) && !!@user
211
+ if valid && @user
212
+ authenticator.extra_attributes_to_extract.each do |attr|
213
+ @extra_attributes[attr] = @user.send(attr)
214
+ end
215
+ end
216
+ return valid
217
+ end
218
+ module_function :valid_credentials?
219
+
220
+ protected
221
+
222
+ def ticket_username
223
+ # Store this into someticket.username
224
+ # It will get used to find users in client apps
225
+ user = @user || @ticketed_user
226
+ @cas_client_username = user.send(settings["client_app_user_field"]) if settings["client_app_user_field"].present? && !!user
227
+ @cas_client_username || @username
228
+ end
229
+
230
+ def ticketed_user(ticket)
231
+ # Find the SSO's instance of the user
232
+ @ticketed_user ||= authenticator.find_user_from_ticket(ticket) unless !ticket
233
+ end
234
+
235
+ def authenticator
236
+ unless @authenticator
237
+ auth_settings = Cassy.config["authenticator"]
238
+ @authenticator ||= auth_settings["class"].constantize
239
+ @authenticator.configure(auth_settings)
240
+ end
241
+ @authenticator
242
+ end
243
+
328
244
  end
245
+
329
246
  end
@@ -1,10 +1,8 @@
1
1
  module Cassy
2
2
  class Engine < Rails::Engine
3
-
4
- config.config_file = ENV["RUBYCAS_CONFIG_FILE"] || "/etc/rubycas-server/config.yml"
5
-
6
- config.after_initialize do
7
- config.configuration = HashWithIndifferentAccess.new(YAML.load_file(config.config_file))
3
+ config.config_file = ENV["RUBYCAS_CONFIG_FILE"]
4
+ config.after_initialize do
5
+ config.configuration = HashWithIndifferentAccess.new(YAML.load(ERB.new(File.read(config.config_file)).result))
8
6
  end
9
7
  end
10
8
  end
@@ -4,15 +4,15 @@ module ActionDispatch::Routing
4
4
  options[:controllers] ||= HashWithIndifferentAccess.new
5
5
  options[:controllers][:sessions] ||= "cassy/sessions"
6
6
  scope(:path => "cas") do
7
- root :to => "#{options[:controllers][:sessions]}#new"
7
+ mount Cassy::API => '/api'
8
8
  get 'login', :to => "#{options[:controllers][:sessions]}#new"
9
9
  post 'login', :to => "#{options[:controllers][:sessions]}#create"
10
-
10
+
11
11
  get 'logout', :to => "#{options[:controllers][:sessions]}#destroy"
12
-
12
+
13
13
  get 'serviceValidate', :to => "#{options[:controllers][:sessions]}#service_validate"
14
14
  get 'proxyValidate', :to => "#{options[:controllers][:sessions]}#proxy_validate"
15
15
  end
16
16
  end
17
17
  end
18
- end
18
+ end
@@ -1,5 +1,4 @@
1
- require 'crypt-isaac'
2
-
1
+ require 'crypt/isaac'
3
2
  # Misc utility function used throughout by the RubyCAS-Server.
4
3
  module Cassy
5
4
  module Utils
@@ -0,0 +1,3 @@
1
+ module Cassy
2
+ VERSION = "2.2.0"
3
+ end
metadata CHANGED
@@ -1,161 +1,214 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: cassy
3
- version: !ruby/object:Gem::Version
4
- prerelease:
5
- version: 1.1.4
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.2.0
6
5
  platform: ruby
7
- authors:
8
- - ryan@rubyx.com
6
+ authors:
7
+ - ryan bigg
8
+ - geoff@reinteractive.net
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
-
13
- date: 2011-08-24 00:00:00 Z
14
- dependencies:
15
- - !ruby/object:Gem::Dependency
12
+ date: 2017-06-30 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
16
15
  name: crypt-isaac
17
- requirement: &id001 !ruby/object:Gem::Requirement
18
- none: false
19
- requirements:
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
20
18
  - - ">="
21
- - !ruby/object:Gem::Version
22
- version: "0"
19
+ - !ruby/object:Gem::Version
20
+ version: 1.0.0
23
21
  type: :runtime
24
22
  prerelease: false
25
- version_requirements: *id001
26
- - !ruby/object:Gem::Dependency
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: 1.0.0
28
+ - !ruby/object:Gem::Dependency
27
29
  name: rails
28
- requirement: &id002 !ruby/object:Gem::Requirement
29
- none: false
30
- requirements:
31
- - - "="
32
- - !ruby/object:Gem::Version
33
- version: 3.0.7
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: 3.0.9
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: 3.0.9
42
+ - !ruby/object:Gem::Dependency
43
+ name: grape
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '0.14'
34
49
  type: :runtime
35
50
  prerelease: false
36
- version_requirements: *id002
37
- - !ruby/object:Gem::Dependency
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '0.14'
56
+ - !ruby/object:Gem::Dependency
57
+ name: grape-entity
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '0.5'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '0.5'
70
+ - !ruby/object:Gem::Dependency
38
71
  name: rspec-rails
39
- requirement: &id003 !ruby/object:Gem::Requirement
40
- none: false
41
- requirements:
42
- - - ~>
43
- - !ruby/object:Gem::Version
44
- version: 2.6.0
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: 2.7.0
45
77
  type: :development
46
78
  prerelease: false
47
- version_requirements: *id003
48
- - !ruby/object:Gem::Dependency
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: 2.7.0
84
+ - !ruby/object:Gem::Dependency
49
85
  name: capybara
50
- requirement: &id004 !ruby/object:Gem::Requirement
51
- none: false
52
- requirements:
53
- - - ~>
54
- - !ruby/object:Gem::Version
55
- version: "1.0"
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '1.0'
56
91
  type: :development
57
92
  prerelease: false
58
- version_requirements: *id004
59
- - !ruby/object:Gem::Dependency
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '1.0'
98
+ - !ruby/object:Gem::Dependency
60
99
  name: database_cleaner
61
- requirement: &id005 !ruby/object:Gem::Requirement
62
- none: false
63
- requirements:
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
64
102
  - - ">="
65
- - !ruby/object:Gem::Version
66
- version: "0"
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
67
105
  type: :development
68
106
  prerelease: false
69
- version_requirements: *id005
70
- - !ruby/object:Gem::Dependency
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ - !ruby/object:Gem::Dependency
71
113
  name: sqlite3
72
- requirement: &id006 !ruby/object:Gem::Requirement
73
- none: false
74
- requirements:
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
75
116
  - - ">="
76
- - !ruby/object:Gem::Version
77
- version: "0"
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
78
119
  type: :development
79
120
  prerelease: false
80
- version_requirements: *id006
81
- - !ruby/object:Gem::Dependency
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
82
127
  name: launchy
83
- requirement: &id007 !ruby/object:Gem::Requirement
84
- none: false
85
- requirements:
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
86
130
  - - ">="
87
- - !ruby/object:Gem::Version
88
- version: "0"
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
89
133
  type: :development
90
134
  prerelease: false
91
- version_requirements: *id007
92
- - !ruby/object:Gem::Dependency
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ - !ruby/object:Gem::Dependency
93
141
  name: devise
94
- requirement: &id008 !ruby/object:Gem::Requirement
95
- none: false
96
- requirements:
97
- - - ~>
98
- - !ruby/object:Gem::Version
99
- version: "1.3"
142
+ requirement: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - "~>"
145
+ - !ruby/object:Gem::Version
146
+ version: 3.2.4
147
+ type: :development
148
+ prerelease: false
149
+ version_requirements: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - "~>"
152
+ - !ruby/object:Gem::Version
153
+ version: 3.2.4
154
+ - !ruby/object:Gem::Dependency
155
+ name: webmock
156
+ requirement: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - ">="
159
+ - !ruby/object:Gem::Version
160
+ version: '0'
100
161
  type: :development
101
162
  prerelease: false
102
- version_requirements: *id008
103
- description: An engine that provides a CAS server to the application it's included within.
163
+ version_requirements: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: '0'
168
+ description: An engine that provides a CAS server to the application it's included
169
+ within.
104
170
  email:
105
171
  executables: []
106
-
107
172
  extensions: []
108
-
109
173
  extra_rdoc_files: []
110
-
111
- files:
174
+ files:
175
+ - MIT-LICENSE
176
+ - README.md
177
+ - Rakefile
178
+ - lib/cassy.rb
179
+ - lib/cassy/authenticators.rb
112
180
  - lib/cassy/authenticators/base.rb
113
181
  - lib/cassy/authenticators/devise.rb
114
182
  - lib/cassy/authenticators/test.rb
115
- - lib/cassy/authenticators.rb
116
183
  - lib/cassy/cas.rb
117
184
  - lib/cassy/engine.rb
118
185
  - lib/cassy/generators/views_generator.rb
119
186
  - lib/cassy/models.rb
120
187
  - lib/cassy/routes.rb
121
188
  - lib/cassy/utils.rb
122
- - lib/cassy.rb
189
+ - lib/cassy/version.rb
123
190
  - lib/tasks/cassy_tasks.rake
124
- - MIT-LICENSE
125
- - Rakefile
126
- - README.markdown
127
191
  homepage:
128
192
  licenses: []
129
-
193
+ metadata: {}
130
194
  post_install_message:
131
195
  rdoc_options: []
132
-
133
- require_paths:
196
+ require_paths:
134
197
  - lib
135
- required_ruby_version: !ruby/object:Gem::Requirement
136
- none: false
137
- requirements:
198
+ required_ruby_version: !ruby/object:Gem::Requirement
199
+ requirements:
138
200
  - - ">="
139
- - !ruby/object:Gem::Version
140
- hash: 68583265356027286
141
- segments:
142
- - 0
143
- version: "0"
144
- required_rubygems_version: !ruby/object:Gem::Requirement
145
- none: false
146
- requirements:
201
+ - !ruby/object:Gem::Version
202
+ version: '0'
203
+ required_rubygems_version: !ruby/object:Gem::Requirement
204
+ requirements:
147
205
  - - ">="
148
- - !ruby/object:Gem::Version
149
- hash: 68583265356027286
150
- segments:
151
- - 0
152
- version: "0"
206
+ - !ruby/object:Gem::Version
207
+ version: '0'
153
208
  requirements: []
154
-
155
209
  rubyforge_project:
156
- rubygems_version: 1.8.8
210
+ rubygems_version: 2.6.4
157
211
  signing_key:
158
- specification_version: 3
159
- summary: Insert Cassy summary.
212
+ specification_version: 4
213
+ summary: Cassy is a rails CAS engine
160
214
  test_files: []
161
-
@@ -1,70 +0,0 @@
1
- # Cassy
2
-
3
- This project is designed to be a Rails 3.0 engine that uses a large portion of the code from the [rubycas-server][https://github.com/gunark/rubycas-server] project. Certain portions of this code belong to the rubycas-server project owners.
4
-
5
- ## Installation
6
-
7
- This engine currently only works with Rails 3.0. To have it work with the application you must do three things:
8
-
9
- **Install as a gem**
10
-
11
- Put this line in your project's `Gemfile`:
12
-
13
- gem 'cassy'
14
-
15
- Create a new initializer (probably called `config/initializers/cassy.rb`) and point cassy at the correct configuration file of your application:
16
-
17
- Cassy::Engine.config.config_file = Rails.root + "config/cassy.yml"
18
-
19
- Create this configuration file at `config/cassy.yml`. Fill it with these values:
20
-
21
- # Times are in seconds.
22
- maximum_unused_login_ticket_lifetime: 300
23
- maximum_unused_service_ticket_lifetime: 300
24
-
25
- authenticator:
26
- class: Cassy::Authenticators::Devise
27
-
28
- The first two keys are the time-to-expiry for the login and service tickets respectively. The class for the authentication can be any constant which responds to a `validates` method. By default, only Devise authentication is supported at the moment.
29
-
30
- Next, you will need to tell Cassy to load its routes in your application which you can do by calling `cassy` in `config/routes.rb`:
31
-
32
- Rails.application.routes.draw do
33
- cassy
34
-
35
- # your routes go here
36
- end
37
-
38
- Boom, done. Now this application will act as a CAS server.
39
-
40
- For customization options please see the "Customization" section below.
41
-
42
- ## Configuration
43
-
44
- The configuration options for this gem goes into a file called `config/cassy.yml` at the root of the project if you've set it up as advised, and this allows the engine to be configured.
45
-
46
- These configuration options are detailed here for your convenience. For specific term definitions, please consult the CAS spec.
47
-
48
- `authenticator`: Must specify at least one key, `class`, which is a string version of a constant that will be used for authentication in the system. This constant *must* respond to `validate`.
49
- `maximum_unused_login_ticket_lifetime`: The time before a login ticket would expire.
50
- `maximum_unused_service_ticket_lifetime`: The time before a service ticket would expire.
51
- `username_field`: Defines the field on the users table which is used for the lookup for the username. Defaults to "username".
52
- `username_label`: Allows for the "Username" label on the sign in page to be given a different value. Helpful if you want to call it "Email" or "User Name" instead.
53
-
54
- ## Customization
55
-
56
- ### Sessions Controller
57
-
58
- In Cassy, it is possible to override the controller which is used for authentication. To do this, the controller can be configured in `config/routes.rb`:
59
-
60
- cassy :controllers => "sessions"
61
-
62
- By doing this, it will point at the `SessionsController` rather than the default of `Cassy::SessionsController`. This controller then should inherit from `Cassy::SessionsController` to inherit the original behaviour and will need to point to the views of Cassy:
63
-
64
- class SessionsController < Cassy::SessionsController
65
- def new
66
- # custom behaviour goes here
67
- super
68
- end
69
-
70
-