aker 3.0.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +210 -0
- data/README.md +282 -0
- data/assets/aker/form/login.css +73 -0
- data/assets/aker/form/login.html.erb +44 -0
- data/lib/aker/authorities/automatic_access.rb +36 -0
- data/lib/aker/authorities/composite.rb +301 -0
- data/lib/aker/authorities/static.rb +283 -0
- data/lib/aker/authorities/support/find_sole_user.rb +24 -0
- data/lib/aker/authorities/support.rb +9 -0
- data/lib/aker/authorities.rb +46 -0
- data/lib/aker/cas/authority.rb +79 -0
- data/lib/aker/cas/configuration_helper.rb +85 -0
- data/lib/aker/cas/middleware/logout_responder.rb +49 -0
- data/lib/aker/cas/middleware/ticket_remover.rb +35 -0
- data/lib/aker/cas/middleware.rb +6 -0
- data/lib/aker/cas/proxy_mode.rb +108 -0
- data/lib/aker/cas/rack_proxy_callback.rb +188 -0
- data/lib/aker/cas/service_mode.rb +88 -0
- data/lib/aker/cas/service_url.rb +62 -0
- data/lib/aker/cas/user_ext.rb +64 -0
- data/lib/aker/cas.rb +31 -0
- data/lib/aker/central_parameters.rb +101 -0
- data/lib/aker/configuration.rb +534 -0
- data/lib/aker/deprecation.rb +105 -0
- data/lib/aker/form/custom_views_mode.rb +80 -0
- data/lib/aker/form/login_form_asset_provider.rb +56 -0
- data/lib/aker/form/middleware/custom_view_login_responder.rb +19 -0
- data/lib/aker/form/middleware/login_renderer.rb +72 -0
- data/lib/aker/form/middleware/login_responder.rb +71 -0
- data/lib/aker/form/middleware/logout_responder.rb +26 -0
- data/lib/aker/form/middleware.rb +10 -0
- data/lib/aker/form/mode.rb +118 -0
- data/lib/aker/form.rb +26 -0
- data/lib/aker/group.rb +67 -0
- data/lib/aker/group_membership.rb +162 -0
- data/lib/aker/ldap/authority.rb +392 -0
- data/lib/aker/ldap/user_ext.rb +19 -0
- data/lib/aker/ldap.rb +22 -0
- data/lib/aker/modes/base.rb +85 -0
- data/lib/aker/modes/http_basic.rb +100 -0
- data/lib/aker/modes/support/attempted_path.rb +22 -0
- data/lib/aker/modes/support/rfc_2617.rb +32 -0
- data/lib/aker/modes/support.rb +12 -0
- data/lib/aker/modes.rb +48 -0
- data/lib/aker/rack/authenticate.rb +37 -0
- data/lib/aker/rack/configuration_helper.rb +18 -0
- data/lib/aker/rack/default_logout_responder.rb +36 -0
- data/lib/aker/rack/environment_helper.rb +34 -0
- data/lib/aker/rack/facade.rb +102 -0
- data/lib/aker/rack/failure.rb +69 -0
- data/lib/aker/rack/logout.rb +63 -0
- data/lib/aker/rack/request_ext.rb +19 -0
- data/lib/aker/rack/session_timer.rb +95 -0
- data/lib/aker/rack/setup.rb +77 -0
- data/lib/aker/rack.rb +107 -0
- data/lib/aker/test/helpers.rb +22 -0
- data/lib/aker/test.rb +8 -0
- data/lib/aker/user.rb +231 -0
- data/lib/aker/version.rb +3 -0
- data/lib/aker.rb +51 -0
- data/spec/aker/aker-sample.yml +11 -0
- data/spec/aker/authorities/automatic_access_spec.rb +52 -0
- data/spec/aker/authorities/composite_spec.rb +488 -0
- data/spec/aker/authorities/nu-schema.jar +0 -0
- data/spec/aker/authorities/static_spec.rb +455 -0
- data/spec/aker/authorities/support/find_sole_user_spec.rb +33 -0
- data/spec/aker/authorities_spec.rb +16 -0
- data/spec/aker/cas/authority_spec.rb +106 -0
- data/spec/aker/cas/configuration_helper_spec.rb +92 -0
- data/spec/aker/cas/middleware/logout_responder_spec.rb +47 -0
- data/spec/aker/cas/middleware/ticket_remover_spec.rb +49 -0
- data/spec/aker/cas/proxy_mode_spec.rb +185 -0
- data/spec/aker/cas/rack_proxy_callback_spec.rb +190 -0
- data/spec/aker/cas/service_mode_spec.rb +122 -0
- data/spec/aker/cas/service_url_spec.rb +114 -0
- data/spec/aker/cas/user_ext_spec.rb +27 -0
- data/spec/aker/cas_spec.rb +19 -0
- data/spec/aker/central_parameters_spec.rb +44 -0
- data/spec/aker/configuration_spec.rb +465 -0
- data/spec/aker/deprecation_spec.rb +115 -0
- data/spec/aker/form/a_form_mode.rb +129 -0
- data/spec/aker/form/custom_views_mode_spec.rb +34 -0
- data/spec/aker/form/login_form_asset_provider_spec.rb +80 -0
- data/spec/aker/form/middleware/a_form_login_responder.rb +89 -0
- data/spec/aker/form/middleware/custom_view_login_responder_spec.rb +47 -0
- data/spec/aker/form/middleware/login_renderer_spec.rb +56 -0
- data/spec/aker/form/middleware/login_responder_spec.rb +34 -0
- data/spec/aker/form/middleware/logout_responder_spec.rb +55 -0
- data/spec/aker/form/mode_spec.rb +15 -0
- data/spec/aker/form_spec.rb +11 -0
- data/spec/aker/group_membership_spec.rb +208 -0
- data/spec/aker/group_spec.rb +66 -0
- data/spec/aker/ldap/authority_spec.rb +414 -0
- data/spec/aker/ldap/ldap-users.ldif +197 -0
- data/spec/aker/ldap_spec.rb +11 -0
- data/spec/aker/modes/a_aker_mode.rb +41 -0
- data/spec/aker/modes/http_basic_spec.rb +127 -0
- data/spec/aker/modes/support/attempted_path_spec.rb +32 -0
- data/spec/aker/modes_spec.rb +11 -0
- data/spec/aker/rack/authenticate_spec.rb +78 -0
- data/spec/aker/rack/default_logout_responder_spec.rb +67 -0
- data/spec/aker/rack/facade_spec.rb +154 -0
- data/spec/aker/rack/failure_spec.rb +151 -0
- data/spec/aker/rack/logout_spec.rb +63 -0
- data/spec/aker/rack/request_ext_spec.rb +29 -0
- data/spec/aker/rack/session_timer_spec.rb +134 -0
- data/spec/aker/rack/setup_spec.rb +87 -0
- data/spec/aker/rack_spec.rb +216 -0
- data/spec/aker/test/helpers_spec.rb +44 -0
- data/spec/aker/user_spec.rb +362 -0
- data/spec/aker_spec.rb +80 -0
- data/spec/deprecation_helper.rb +58 -0
- data/spec/java_helper.rb +5 -0
- data/spec/logger_helper.rb +17 -0
- data/spec/matchers.rb +31 -0
- data/spec/mock_builder.rb +25 -0
- data/spec/spec_helper.rb +52 -0
- 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,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
|