aker 3.0.0.pre
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|