aker 3.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (118) hide show
  1. data/CHANGELOG.md +210 -0
  2. data/README.md +282 -0
  3. data/assets/aker/form/login.css +73 -0
  4. data/assets/aker/form/login.html.erb +44 -0
  5. data/lib/aker/authorities/automatic_access.rb +36 -0
  6. data/lib/aker/authorities/composite.rb +301 -0
  7. data/lib/aker/authorities/static.rb +283 -0
  8. data/lib/aker/authorities/support/find_sole_user.rb +24 -0
  9. data/lib/aker/authorities/support.rb +9 -0
  10. data/lib/aker/authorities.rb +46 -0
  11. data/lib/aker/cas/authority.rb +79 -0
  12. data/lib/aker/cas/configuration_helper.rb +85 -0
  13. data/lib/aker/cas/middleware/logout_responder.rb +49 -0
  14. data/lib/aker/cas/middleware/ticket_remover.rb +35 -0
  15. data/lib/aker/cas/middleware.rb +6 -0
  16. data/lib/aker/cas/proxy_mode.rb +108 -0
  17. data/lib/aker/cas/rack_proxy_callback.rb +188 -0
  18. data/lib/aker/cas/service_mode.rb +88 -0
  19. data/lib/aker/cas/service_url.rb +62 -0
  20. data/lib/aker/cas/user_ext.rb +64 -0
  21. data/lib/aker/cas.rb +31 -0
  22. data/lib/aker/central_parameters.rb +101 -0
  23. data/lib/aker/configuration.rb +534 -0
  24. data/lib/aker/deprecation.rb +105 -0
  25. data/lib/aker/form/custom_views_mode.rb +80 -0
  26. data/lib/aker/form/login_form_asset_provider.rb +56 -0
  27. data/lib/aker/form/middleware/custom_view_login_responder.rb +19 -0
  28. data/lib/aker/form/middleware/login_renderer.rb +72 -0
  29. data/lib/aker/form/middleware/login_responder.rb +71 -0
  30. data/lib/aker/form/middleware/logout_responder.rb +26 -0
  31. data/lib/aker/form/middleware.rb +10 -0
  32. data/lib/aker/form/mode.rb +118 -0
  33. data/lib/aker/form.rb +26 -0
  34. data/lib/aker/group.rb +67 -0
  35. data/lib/aker/group_membership.rb +162 -0
  36. data/lib/aker/ldap/authority.rb +392 -0
  37. data/lib/aker/ldap/user_ext.rb +19 -0
  38. data/lib/aker/ldap.rb +22 -0
  39. data/lib/aker/modes/base.rb +85 -0
  40. data/lib/aker/modes/http_basic.rb +100 -0
  41. data/lib/aker/modes/support/attempted_path.rb +22 -0
  42. data/lib/aker/modes/support/rfc_2617.rb +32 -0
  43. data/lib/aker/modes/support.rb +12 -0
  44. data/lib/aker/modes.rb +48 -0
  45. data/lib/aker/rack/authenticate.rb +37 -0
  46. data/lib/aker/rack/configuration_helper.rb +18 -0
  47. data/lib/aker/rack/default_logout_responder.rb +36 -0
  48. data/lib/aker/rack/environment_helper.rb +34 -0
  49. data/lib/aker/rack/facade.rb +102 -0
  50. data/lib/aker/rack/failure.rb +69 -0
  51. data/lib/aker/rack/logout.rb +63 -0
  52. data/lib/aker/rack/request_ext.rb +19 -0
  53. data/lib/aker/rack/session_timer.rb +95 -0
  54. data/lib/aker/rack/setup.rb +77 -0
  55. data/lib/aker/rack.rb +107 -0
  56. data/lib/aker/test/helpers.rb +22 -0
  57. data/lib/aker/test.rb +8 -0
  58. data/lib/aker/user.rb +231 -0
  59. data/lib/aker/version.rb +3 -0
  60. data/lib/aker.rb +51 -0
  61. data/spec/aker/aker-sample.yml +11 -0
  62. data/spec/aker/authorities/automatic_access_spec.rb +52 -0
  63. data/spec/aker/authorities/composite_spec.rb +488 -0
  64. data/spec/aker/authorities/nu-schema.jar +0 -0
  65. data/spec/aker/authorities/static_spec.rb +455 -0
  66. data/spec/aker/authorities/support/find_sole_user_spec.rb +33 -0
  67. data/spec/aker/authorities_spec.rb +16 -0
  68. data/spec/aker/cas/authority_spec.rb +106 -0
  69. data/spec/aker/cas/configuration_helper_spec.rb +92 -0
  70. data/spec/aker/cas/middleware/logout_responder_spec.rb +47 -0
  71. data/spec/aker/cas/middleware/ticket_remover_spec.rb +49 -0
  72. data/spec/aker/cas/proxy_mode_spec.rb +185 -0
  73. data/spec/aker/cas/rack_proxy_callback_spec.rb +190 -0
  74. data/spec/aker/cas/service_mode_spec.rb +122 -0
  75. data/spec/aker/cas/service_url_spec.rb +114 -0
  76. data/spec/aker/cas/user_ext_spec.rb +27 -0
  77. data/spec/aker/cas_spec.rb +19 -0
  78. data/spec/aker/central_parameters_spec.rb +44 -0
  79. data/spec/aker/configuration_spec.rb +465 -0
  80. data/spec/aker/deprecation_spec.rb +115 -0
  81. data/spec/aker/form/a_form_mode.rb +129 -0
  82. data/spec/aker/form/custom_views_mode_spec.rb +34 -0
  83. data/spec/aker/form/login_form_asset_provider_spec.rb +80 -0
  84. data/spec/aker/form/middleware/a_form_login_responder.rb +89 -0
  85. data/spec/aker/form/middleware/custom_view_login_responder_spec.rb +47 -0
  86. data/spec/aker/form/middleware/login_renderer_spec.rb +56 -0
  87. data/spec/aker/form/middleware/login_responder_spec.rb +34 -0
  88. data/spec/aker/form/middleware/logout_responder_spec.rb +55 -0
  89. data/spec/aker/form/mode_spec.rb +15 -0
  90. data/spec/aker/form_spec.rb +11 -0
  91. data/spec/aker/group_membership_spec.rb +208 -0
  92. data/spec/aker/group_spec.rb +66 -0
  93. data/spec/aker/ldap/authority_spec.rb +414 -0
  94. data/spec/aker/ldap/ldap-users.ldif +197 -0
  95. data/spec/aker/ldap_spec.rb +11 -0
  96. data/spec/aker/modes/a_aker_mode.rb +41 -0
  97. data/spec/aker/modes/http_basic_spec.rb +127 -0
  98. data/spec/aker/modes/support/attempted_path_spec.rb +32 -0
  99. data/spec/aker/modes_spec.rb +11 -0
  100. data/spec/aker/rack/authenticate_spec.rb +78 -0
  101. data/spec/aker/rack/default_logout_responder_spec.rb +67 -0
  102. data/spec/aker/rack/facade_spec.rb +154 -0
  103. data/spec/aker/rack/failure_spec.rb +151 -0
  104. data/spec/aker/rack/logout_spec.rb +63 -0
  105. data/spec/aker/rack/request_ext_spec.rb +29 -0
  106. data/spec/aker/rack/session_timer_spec.rb +134 -0
  107. data/spec/aker/rack/setup_spec.rb +87 -0
  108. data/spec/aker/rack_spec.rb +216 -0
  109. data/spec/aker/test/helpers_spec.rb +44 -0
  110. data/spec/aker/user_spec.rb +362 -0
  111. data/spec/aker_spec.rb +80 -0
  112. data/spec/deprecation_helper.rb +58 -0
  113. data/spec/java_helper.rb +5 -0
  114. data/spec/logger_helper.rb +17 -0
  115. data/spec/matchers.rb +31 -0
  116. data/spec/mock_builder.rb +25 -0
  117. data/spec/spec_helper.rb +52 -0
  118. metadata +265 -0
@@ -0,0 +1,79 @@
1
+ require 'aker/authorities'
2
+
3
+ require 'castanet'
4
+
5
+ module Aker::Cas
6
+ ##
7
+ # An authority which verifies CAS tickets with an actual CAS server.
8
+ #
9
+ # @see Aker::Cas::UserExt
10
+ class Authority
11
+ include ConfigurationHelper
12
+ include Castanet::Client
13
+
14
+ attr_reader :configuration
15
+
16
+ ##
17
+ # Creates a new instance of this authority. It reads parameters
18
+ # from the `:cas` parameters section of the given configuration.
19
+ # See {Aker::Cas::ConfigurationHelper} for information about the
20
+ # meanings of these parameters.
21
+ def initialize(configuration)
22
+ @configuration = configuration
23
+
24
+ unless cas_url
25
+ raise ":base_url parameter is required for CAS"
26
+ end
27
+ end
28
+
29
+ ##
30
+ # Verifies the given credentials with the CAS server. The `:cas`
31
+ # and `:cas_proxy` kinds are supported. Both kinds require two
32
+ # credentials in the following order:
33
+ #
34
+ # * The ticket (either a service ticket or proxy ticket)
35
+ # * The service URL associated with the ticket
36
+ #
37
+ # The returned user will be extended with {Aker::Cas::CasUser}.
38
+ #
39
+ # If CAS proxying is enabled, then this method also retrieves the
40
+ # proxy-granting ticket for the user.
41
+ #
42
+ # @see http://www.jasig.org/cas/protocol
43
+ # CAS 2 protocol specification, section 2.5.4
44
+ # @return [Aker::User,:unsupported,nil] a user if the credentials
45
+ # are valid, `:unsupported` if the kind is anything but `:cas`
46
+ # or `:cas_proxy`, and nil otherwise
47
+ def valid_credentials?(kind, *credentials)
48
+ return :unsupported unless [:cas, :cas_proxy].include?(kind)
49
+
50
+ ticket = ticket_for(kind, *credentials)
51
+ ticket.present!
52
+
53
+ return nil unless ticket.ok?
54
+
55
+ Aker::User.new(ticket.username).tap do |u|
56
+ u.extend Aker::Cas::UserExt
57
+
58
+ u.cas_url = cas_url
59
+ u.proxy_callback_url = proxy_callback_url
60
+ u.proxy_retrieval_url = proxy_retrieval_url
61
+
62
+ if ticket.pgt_iou
63
+ ticket.retrieve_pgt!
64
+
65
+ u.pgt = ticket.pgt
66
+ end
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def ticket_for(kind, ticket, service)
73
+ case kind
74
+ when :cas; service_ticket(ticket, service)
75
+ when :cas_proxy; proxy_ticket(ticket, service)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,85 @@
1
+ require 'aker/cas'
2
+ require 'uri'
3
+
4
+ module Aker::Cas
5
+ ##
6
+ # A helper for uniform creation of derived attributes for the CAS
7
+ # configuration. It expects to be mixed in to a context that
8
+ # provides a `configuration` method which returns a
9
+ # {Aker::Configuration}.
10
+ #
11
+ # @see Aker::Configuration
12
+ module ConfigurationHelper
13
+ ##
14
+ # The login URL on the CAS server. This may be set explicitly
15
+ # in the configuration as `parameters_for(:cas)[:login_url]`. If
16
+ # not set explicitly, it will be derived from the base URL.
17
+ #
18
+ # @return [String]
19
+ def cas_login_url
20
+ configuration.parameters_for(:cas)[:login_url] || URI.join(cas_url, 'login').to_s
21
+ end
22
+
23
+ ##
24
+ # The logout URL on the CAS server. This may be set explicitly
25
+ # in the configuration as `parameters_for(:cas)[:logout_url]`. If
26
+ # not set explicitly, it will be derived from the base URL.
27
+ #
28
+ # @return [String]
29
+ def cas_logout_url
30
+ configuration.parameters_for(:cas)[:logout_url] || URI.join(cas_url, 'logout').to_s
31
+ end
32
+
33
+ ##
34
+ # The base URL for all not-otherwise-explicitly-specified URLs on
35
+ # the CAS server. It may be set in the CAS parameters as either
36
+ # `:base_url` (preferred) or `:cas_base_url` (for backwards
37
+ # compatibility with aker 1.x).
38
+ #
39
+ # The base URL should end in a `/` (forward slash). If it does not, a
40
+ # trailing forward slash will be appended.
41
+ #
42
+ # @see http://www.ietf.org/rfc/rfc1808.txt
43
+ # RFC 1808, sections 4 and 5
44
+ # @return [String, nil]
45
+ def cas_url
46
+ appending_forward_slash do
47
+ configuration.parameters_for(:cas)[:base_url] ||
48
+ configuration.parameters_for(:cas)[:cas_base_url]
49
+ end
50
+ end
51
+
52
+ ##
53
+ # The URL that CAS will provide the PGT and PGTIOU to, per section
54
+ # 2.5.4 of the spec. Some CAS servers require that this be an
55
+ # SSL-protected resource. It is set in the CAS parameters as
56
+ # `:proxy_callback_url`.
57
+ #
58
+ # @return [String, nil]
59
+ def proxy_callback_url
60
+ configuration.parameters_for(:cas)[:proxy_callback_url]
61
+ end
62
+
63
+ ##
64
+ # The URL that the CAS client can retrieve the PGT from once it
65
+ # has been deposited at the {#proxy_callback_url} by the CAS
66
+ # server. It is set in the CAS parameters as
67
+ # `:proxy_retrieval_url`.
68
+ #
69
+ # (Note that this is not part of the CAS protocol — it is
70
+ # client-specific.)
71
+ #
72
+ # @return [String, nil]
73
+ def proxy_retrieval_url
74
+ configuration.parameters_for(:cas)[:proxy_retrieval_url]
75
+ end
76
+
77
+ private
78
+
79
+ def appending_forward_slash
80
+ url = yield
81
+
82
+ (url && url[-1].chr != '/') ? url + '/' : url
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,49 @@
1
+ require 'aker'
2
+
3
+ module Aker::Cas::Middleware
4
+ class LogoutResponder
5
+ include Aker::Rack::ConfigurationHelper
6
+
7
+ ##
8
+ # @param app a Rack app
9
+ # @param [String] cas_logout_url the CAS logout URL
10
+ def initialize(app)
11
+ @app = app
12
+ end
13
+
14
+ ##
15
+ # Rack entry point.
16
+ #
17
+ # Given a `GET` to the configured logout path, redirects to
18
+ # {#cas_logout_url}. All other requests are passed through.
19
+ #
20
+ # @see http://www.jasig.org/cas/protocol
21
+ # Section 2.3 of the CAS 2 protocol
22
+ def call(env)
23
+ if env['REQUEST_METHOD'] == 'GET' && env['PATH_INFO'] == logout_path(env)
24
+ ::Rack::Response.new { |r| r.redirect(cas_logout_url(env)) }.finish
25
+ else
26
+ @app.call(env)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def cas_logout_url(env)
33
+ configuration(env).parameters_for(:cas)[:logout_url] || URI.join(cas_url(env), 'logout').to_s
34
+ end
35
+
36
+ def cas_url(env)
37
+ appending_forward_slash do
38
+ configuration(env).parameters_for(:cas)[:base_url] ||
39
+ configuration(env).parameters_for(:cas)[:cas_base_url]
40
+ end
41
+ end
42
+
43
+ def appending_forward_slash
44
+ url = yield
45
+
46
+ (url && url[-1].chr != '/') ? url + '/' : url
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,35 @@
1
+ require 'aker'
2
+
3
+ module Aker::Cas::Middleware
4
+ ##
5
+ # Middleware which issues a redirect immediately after CAS
6
+ # authentication succeeds so that users never see a URL with the
7
+ # ticket in it. This prevents them from, e.g., bookmarking a URL
8
+ # with a ticket in it, keeping things cleaner and preventing
9
+ # requests to the CAS server for tickets which are definitely
10
+ # expired.
11
+ class TicketRemover
12
+ def initialize(app)
13
+ @app = app
14
+ end
15
+
16
+ def call(env)
17
+ if authenticated?(env) && ticket_present?(env)
18
+ url = Aker::Cas::ServiceUrl.service_url(Rack::Request.new(env))
19
+ [301, { 'Location' => url }, ["Removing authenticated CAS ticket"] ]
20
+ else
21
+ @app.call(env)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def authenticated?(env)
28
+ env['aker.check'] && env['aker.check'].user
29
+ end
30
+
31
+ def ticket_present?(env)
32
+ env['QUERY_STRING'] =~ /ticket=/
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,6 @@
1
+ require 'aker'
2
+
3
+ module Aker::Cas::Middleware
4
+ autoload :LogoutResponder, 'aker/cas/middleware/logout_responder'
5
+ autoload :TicketRemover, 'aker/cas/middleware/ticket_remover'
6
+ end
@@ -0,0 +1,108 @@
1
+ require 'aker'
2
+
3
+ module Aker
4
+ module Cas
5
+ ##
6
+ # A non-interactive mode that provides CAS proxy authentication conformant to
7
+ # CAS 2.
8
+ #
9
+ # This mode does _not_ handle interactive CAS authentication; see {Cas} for
10
+ # that.
11
+ #
12
+ # @see http://www.jasig.org/cas/protocol
13
+ # CAS 2 protocol specification
14
+ #
15
+ # @author David Yip
16
+ class ProxyMode < Aker::Modes::Base
17
+ include Aker::Modes::Support::Rfc2617
18
+
19
+ ##
20
+ # A key that refers to this mode; used for configuration convenience.
21
+ #
22
+ # @return [Symbol]
23
+ def self.key
24
+ :cas_proxy
25
+ end
26
+
27
+ ##
28
+ # The type of credentials supplied by this mode.
29
+ #
30
+ # @return [Symbol]
31
+ def kind
32
+ self.class.key
33
+ end
34
+
35
+ ##
36
+ # The supplied proxy ticket and the {#service_url service URL}.
37
+ #
38
+ # The proxy ticket is received in the HTTP `Authorization`
39
+ # header, per RFC2616. The scheme must be `CasProxy`. Example:
40
+ #
41
+ # > `Authorization: CasProxy PT-1272928074r13CBB9ACA794867F3E`
42
+ #
43
+ # @see http://www.jasig.org/cas/protocol CAS 2.0 protocol, section 3.7
44
+ # @return [Array<String>] the proxy ticket or an empty array
45
+ def credentials
46
+ key = 'HTTP_AUTHORIZATION'
47
+ matches = env[key].match(/CasProxy\s+([SP]T-[0-9A-Za-z\-]+)/) if env.has_key?(key)
48
+
49
+ if matches && matches[1]
50
+ [matches[1], service_url]
51
+ else
52
+ []
53
+ end
54
+ end
55
+
56
+ ##
57
+ # Returns true if a proxy ticket is present, false otherwise.
58
+ def valid?
59
+ !credentials.empty?
60
+ end
61
+
62
+ ##
63
+ # Used to build a WWW-Authenticate header that will be returned to a
64
+ # client failing non-interactive authentication.
65
+ #
66
+ # @return [String]
67
+ def scheme
68
+ "CasProxy"
69
+ end
70
+
71
+ ##
72
+ # Builds the service URL for this application.
73
+ #
74
+ # Colloquially, the service URL is the web server URL plus the
75
+ # application mount point. It does not include anything
76
+ # about the specific resource being requested. For instance, if
77
+ # you had the resource
78
+ #
79
+ # > https://notis.nubic.northwestern.edu/lsdb/patients/105661
80
+ #
81
+ # which was part of the `/lsdb` application, the service URL
82
+ # would be
83
+ #
84
+ # > https://notis.nubic.northwestern.edu/lsdb
85
+ #
86
+ # A little more formally, the URL is `url scheme +
87
+ # hostname + script name`. The port is also included if it is
88
+ # not the default for the URL scheme.
89
+ #
90
+ # The service URL never ends with a `/`, even if the application
91
+ # is mounted at the root.
92
+ #
93
+ # @return [String] the service URL derived from the request
94
+ # environment
95
+ def service_url
96
+ url = "#{env['rack.url_scheme']}://"
97
+ if env['HTTP_HOST']
98
+ url << env['HTTP_HOST'] # includes the port
99
+ else
100
+ url << env['SERVER_NAME']
101
+ default_port = { "http" => "80", "https" => "443" }[env['rack.url_scheme']]
102
+ url << ":#{env["SERVER_PORT"]}" unless env["SERVER_PORT"].to_s == default_port
103
+ end
104
+ url << env["SCRIPT_NAME"]
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,188 @@
1
+ require 'aker/cas'
2
+ require 'pstore'
3
+ require 'tmpdir'
4
+
5
+ module Aker::Cas
6
+ ##
7
+ # Rack code for handling the PGT callback part of the CAS proxy
8
+ # authentication protocol. The class itself is middleware; it can
9
+ # also generate an {.application endpoint}.
10
+ #
11
+ # ## Behavior
12
+ #
13
+ # As middleware, this class intercepts and handles two paths and
14
+ # passes all other requests down the chain. The paths are:
15
+ #
16
+ # * `/receive_pgt`: implements the PGT callback process per section
17
+ # 2.5.4 of the CAS protocol.
18
+ # * `/retrieve_pgt`: allows an application to retrieve the PGT for
19
+ # a PGTIOU. The PGTIOU is returned to the application as part of
20
+ # the CAS ticket validation process. It should be passed to
21
+ # `/receive_pgt` as the `pgtIou` query parameter. Note that a
22
+ # given PGT may only be retrieved once.
23
+ #
24
+ # As a full rack app, it handles the same two paths and returns `404
25
+ # Not Found` for all other requests.
26
+ #
27
+ # ## Middleware vs. Application
28
+ #
29
+ # It is **only** appropriate to use the class as middleware in a
30
+ # **multithreaded or multiprocessing deployment**. If your application
31
+ # only has one executor at a time, using this class as middleware
32
+ # **will cause a deadlock** during CAS authentication.
33
+ #
34
+ # ## Based on
35
+ #
36
+ # This class was heavily influenced by `CasProxyCallbackController`
37
+ # in rubycas-client. That class has approximately the same
38
+ # behavior, but is Rails-specific.
39
+ #
40
+ # @see http://www.jasig.org/cas/protocol
41
+ # CAS protocol, section 2.5.4
42
+ class RackProxyCallback
43
+ RETRIEVE_PATH = "/retrieve_pgt"
44
+ RECEIVE_PATH = "/receive_pgt"
45
+
46
+ ##
47
+ # Create a new instance of the middleware.
48
+ #
49
+ # @param [#call] app the next rack application in the chain.
50
+ # @param [Hash] options
51
+ # @option options [String] :store the file where the middleware
52
+ # will store the received PGTs until they are retrieved.
53
+ def initialize(app, options={})
54
+ @app = app
55
+ @store_filename = options.delete(:store) or
56
+ raise "Please specify a filename for the PGT store"
57
+ end
58
+
59
+ ##
60
+ # Handles a single request in the manner specified in the class
61
+ # overview.
62
+ #
63
+ # @param [Hash] env the rack environment for the request.
64
+ #
65
+ # @return [Array] an appropriate rack response.
66
+ def call(env)
67
+ return receive(env) if env["PATH_INFO"] == RECEIVE_PATH
68
+ return retrieve(env) if env["PATH_INFO"] == RETRIEVE_PATH
69
+ @app.call(env)
70
+ end
71
+
72
+ ##
73
+ # Creates a rack application which responds as described in the
74
+ # class overview.
75
+ #
76
+ # @param [Hash] options the same options that you can pass to
77
+ # {#initialize}.
78
+ #
79
+ # @return [#call] a full rack application
80
+ def self.application(options={})
81
+ app = lambda { |env|
82
+ [404, { "Content-Type" => "text/plain" }, ["Unknown resource #{env['PATH_INFO']}"]]
83
+ }
84
+ RackProxyCallback.new(app, options)
85
+ end
86
+
87
+ protected
88
+
89
+ ##
90
+ # Associates the given PGTIOU and PGT.
91
+ #
92
+ # @param [String] pgt_iou
93
+ # @param [String] pgt
94
+ #
95
+ # @return [void]
96
+ def store_iou(pgt_iou, pgt)
97
+ pstore = open_pstore
98
+
99
+ pstore.transaction do
100
+ pstore[pgt_iou] = pgt
101
+ end
102
+ end
103
+
104
+ ##
105
+ # Finds the PGT for the given PGTIOU. If there isn't one, it
106
+ # returns nil. If there is one, it deletes it from the store
107
+ # before returning it.
108
+ #
109
+ # @param [String] pgt_iou
110
+ # @return [String,nil]
111
+ def resolve_iou(pgt_iou)
112
+ pstore = open_pstore
113
+
114
+ pgt = nil
115
+ pstore.transaction do
116
+ pgt = pstore[pgt_iou]
117
+ pstore.delete(pgt_iou) if pgt
118
+ end
119
+
120
+ pgt
121
+ end
122
+
123
+ private
124
+
125
+ def receive(env)
126
+ req = Rack::Request.new(env)
127
+ resp = Rack::Response.new
128
+ resp.headers["Content-Type"] = "text/plain"
129
+
130
+ pgt = req.params["pgtId"]
131
+ pgt_iou = req.params["pgtIou"]
132
+
133
+ unless pgt && pgt_iou
134
+ missing = [("pgtId" unless pgt), ("pgtIou" unless pgt_iou)].compact
135
+ missing_msg =
136
+ if missing.size == 1
137
+ "#{missing.first} is a required query parameter."
138
+ else
139
+ "Both #{missing.join(' and ')} are required query parameters."
140
+ end
141
+ resp.status =
142
+ if missing.size == 2
143
+ #
144
+ # This oddity is required by the JA-SIG CAS Server.
145
+ #
146
+ 200
147
+ else
148
+ 400
149
+ end
150
+
151
+ resp.body = ["#{missing_msg}\nSee section 2.5.4 of the CAS protocol specification."]
152
+ else
153
+ store_iou(pgt_iou, pgt)
154
+
155
+ resp.body = ["PGT and PGTIOU received. Thanks, my robotic friend."]
156
+ end
157
+
158
+ resp.finish
159
+ end
160
+
161
+ def retrieve(env)
162
+ req = Rack::Request.new(env)
163
+ resp = Rack::Response.new
164
+ resp.headers["Content-Type"] = "text/plain"
165
+
166
+ pgt_iou = req.params["pgtIou"]
167
+
168
+ if pgt_iou
169
+ pgt = resolve_iou(pgt_iou)
170
+ if pgt
171
+ resp.body = [pgt]
172
+ else
173
+ resp.status = 404
174
+ resp.body = ["pgtIou=#{pgt_iou} does not exist. Perhaps it has already been retrieved."]
175
+ end
176
+ else
177
+ resp.status = 400
178
+ resp.body = ["pgtIou is a required query parameter."]
179
+ end
180
+
181
+ resp.finish
182
+ end
183
+
184
+ def open_pstore
185
+ PStore.new(@store_filename)
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,88 @@
1
+ require 'aker'
2
+ require 'rack'
3
+
4
+ module Aker
5
+ module Cas
6
+ ##
7
+ # An interactive mode that provides CAS authentication conformant to CAS 2.
8
+ #
9
+ # This mode does _not_ handle non-interactive CAS proxying. See
10
+ # {ProxyMode} for that.
11
+ #
12
+ # @see http://www.jasig.org/cas/protocol
13
+ # CAS 2 protocol specification
14
+ #
15
+ # @author David Yip
16
+ class ServiceMode < Aker::Modes::Base
17
+ include ConfigurationHelper
18
+ include ::Rack::Utils
19
+ include Aker::Modes::Support::AttemptedPath
20
+ include ServiceUrl
21
+
22
+ ##
23
+ # A key that refers to this mode; used for configuration convenience.
24
+ #
25
+ # @return [Symbol]
26
+ def self.key
27
+ :cas
28
+ end
29
+
30
+ ##
31
+ # Appends the {Middleware::LogoutResponder logout responder} and
32
+ # the {Middleware::TicketRemover ticket remover} to the Rack
33
+ # middleware stack.
34
+ def self.append_middleware(builder)
35
+ builder.use(Middleware::LogoutResponder)
36
+ builder.use(Middleware::TicketRemover)
37
+ end
38
+
39
+ ##
40
+ # The type of credentials supplied by this mode.
41
+ #
42
+ # @return [Symbol]
43
+ def kind
44
+ self.class.key
45
+ end
46
+
47
+ ##
48
+ # Extracts the service ticket from the request parameters.
49
+ #
50
+ # The service ticket is assumed to be a parameter named ST in either GET
51
+ # or POST data.
52
+ #
53
+ # @return [Array<String>,nil] a two-item array containing the
54
+ # service ticket and the service URL to which the ticket
55
+ # (it is asserted) applies
56
+ def credentials
57
+ if request['ticket']
58
+ [request['ticket'], service_url]
59
+ end
60
+ end
61
+
62
+ ##
63
+ # Returns true if a service ticket is present in the query string, false
64
+ # otherwise.
65
+ def valid?
66
+ credentials
67
+ end
68
+
69
+ ##
70
+ # Builds a Rack response that redirects to a CAS server's login page.
71
+ #
72
+ # The constructed response uses the URL of the resource for which
73
+ # authentication failed as the CAS service URL.
74
+ #
75
+ # @see http://www.jasig.org/cas/protocol
76
+ # Section 2.2.1 of the CAS 2 protocol
77
+ #
78
+ # @return [Rack::Response]
79
+ def on_ui_failure
80
+ ::Rack::Response.new do |resp|
81
+ login_uri = URI.parse(cas_login_url)
82
+ login_uri.query = "service=#{escape(service_url)}"
83
+ resp.redirect(login_uri.to_s)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,62 @@
1
+ require 'aker/cas'
2
+
3
+ module Aker::Cas
4
+ ##
5
+ # Provides logic for reconstructing the full requested URL from a
6
+ # rack request.
7
+ #
8
+ # If used as a mixin, the host class must have a `#request`
9
+ # accessor. It may optionally also have a `#attempted_path`
10
+ # accessor.
11
+ #
12
+ # @see ServiceMode
13
+ # @see Aker::Modes::Support::AttemptedPath
14
+ module ServiceUrl
15
+ ##
16
+ # The service URL supplied to the CAS login page. This is the
17
+ # requested URL, sans any service ticket.
18
+ #
19
+ # @return [String]
20
+ def service_url
21
+ ServiceUrl.service_url(
22
+ request,
23
+ (attempted_path if self.respond_to?(:attempted_path))
24
+ )
25
+ end
26
+
27
+ ##
28
+ # Builds the service URL that should be used for the given
29
+ # request. This is the requested URL (or the attempted_path, if
30
+ # given), sans any service ticket.
31
+ #
32
+ # @param [Rack::Request] request
33
+ # @param [String] attempted_path
34
+ # @return [String]
35
+ def self.service_url(request, attempted_path=nil)
36
+ requested = URI.parse(
37
+ if attempted_path
38
+ url = "#{request.scheme}://#{request.host}"
39
+
40
+ unless [ ["https", 443], ["http", 80] ].include?([request.scheme, request.port])
41
+ url << ":#{request.port}"
42
+ end
43
+
44
+ url << attempted_path
45
+ else
46
+ request.url
47
+ end
48
+ )
49
+ if requested.query
50
+ requested.query.gsub!(/(&?)ticket=ST-[^&]+(&?)/) do
51
+ if [$1, $2].uniq == ['&'] # in the middle
52
+ '&'
53
+ else
54
+ nil
55
+ end
56
+ end
57
+ requested.query = nil if requested.query.empty?
58
+ end
59
+ requested.to_s
60
+ end
61
+ end
62
+ end