diff --git a/.gitignore b/.gitignore
index 2465b90..05de119 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,5 @@
Gemfile.lock
/gemfiles/*.lock
pkg/*
+.rvmrc
+.idea
\ No newline at end of file
diff --git a/lib/generators/proxy_granting_ticket_ious_migration_generator.rb b/lib/generators/proxy_granting_ticket_ious_migration_generator.rb
new file mode 100644
index 0000000..a789e26
--- /dev/null
+++ b/lib/generators/proxy_granting_ticket_ious_migration_generator.rb
@@ -0,0 +1,26 @@
+require 'rails/generators'
+require 'rails/generators/migration'
+
+class ProxyGrantingTicketIousMigrationGenerator < Rails::Generators::Base
+ include Rails::Generators::Migration
+
+ desc 'Creates a new Proxy Granting Ticket IOUs migration file'
+
+ def self.source_root
+ File.expand_path('../templates', __FILE__)
+ end
+
+ def self.next_migration_number(dirname)
+ if ActiveRecord::Base.timestamped_migrations
+ migration_number = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
+ migration_number += 1
+ migration_number.to_s
+ else
+ "%.3d" % (current_migration_number(dirname) + 1)
+ end
+ end
+
+ def create_migration_file
+ migration_template 'create_rack_cas_proxy_granting_ticket_ious_migration.rb', 'db/migrate/create_rack_cas_proxy_granting_ticket_ious.rb'
+ end
+end
diff --git a/lib/generators/templates/create_rack_cas_proxy_granting_ticket_ious_migration.rb b/lib/generators/templates/create_rack_cas_proxy_granting_ticket_ious_migration.rb
new file mode 100644
index 0000000..97275d2
--- /dev/null
+++ b/lib/generators/templates/create_rack_cas_proxy_granting_ticket_ious_migration.rb
@@ -0,0 +1,17 @@
+class CreateRackCasProxyGrantingTicketIous < ActiveRecord::Migration
+ def self.up
+ create_table :proxy_granting_ticket_ious do |t|
+ t.string :proxy_granting_ticket_iou, :null => false
+ t.string :proxy_granting_ticket, :null => false
+ t.timestamps
+ end
+
+ add_index :proxy_granting_ticket_ious, :proxy_granting_ticket_iou
+ add_index :proxy_granting_ticket_ious, :proxy_granting_ticket
+ add_index :proxy_granting_ticket_ious, :updated_at
+ end
+
+ def self.down
+ drop_table :proxy_granting_ticket_ious
+ end
+end
\ No newline at end of file
diff --git a/lib/rack-cas/cas_request.rb b/lib/rack-cas/cas_request.rb
index 0db1acd..8d10634 100644
--- a/lib/rack-cas/cas_request.rb
+++ b/lib/rack-cas/cas_request.rb
@@ -1,3 +1,4 @@
+require 'addressable/uri'
require 'nokogiri'
class CASRequest
@@ -29,10 +30,15 @@ def single_sign_out?
def ticket_validation?
# The CAS protocol specifies that services must support tickets of
- # *up to* 32 characters in length (including ST-), and recommendes
+ # *up to* 32 characters in length (including ST-), and recommends
# that services accept tickets up to 256 characters long.
- # http://www.jasig.org/cas/protocol
- !!(@request.get? && ticket_param && ticket_param.to_s =~ /\AST\-[^\s]{1,253}\Z/)
+ #
+ # It also specifies that although the service ticket MUST start with "ST-",
+ # the proxy ticket SHOULD start with "PT-". The "ST-" validation has
+ # been moved to the validate_service_url method in server.rb.
+ #
+ # http://jasig.github.io/cas/development/protocol/CAS-Protocol-Specification.html
+ !!(@request.get? && ticket_param && ticket_param.to_s =~ /\A[^\s]{1,256}\Z/)
end
def path_matches?(strings_or_regexps)
@@ -45,12 +51,35 @@ def path_matches?(strings_or_regexps)
end
end
+ def pgt_callback?
+ !!(@request.get? && RackCAS.config.pgt_callback_url? && \
+ Addressable::URI.parse(RackCAS.config.pgt_callback_url).path == Addressable::URI.parse(@request.url).path && \
+ pgt_iou_param && pgt_iou_param.to_s =~ /\A[^\s]{1,256}\Z/ && \
+ pgt_param && pgt_param.to_s =~ /\A[^\s]{1,256}\Z/)
+ end
+
+ def pgt
+ pgt_param if pgt_callback?
+ end
+
+ def pgt_iou
+ pgt_iou_param if pgt_callback?
+ end
+
private
def ticket_param
@request.params['ticket']
end
+ def pgt_iou_param
+ @request.params['pgtIou']
+ end
+
+ def pgt_param
+ @request.params['pgtId']
+ end
+
def sso_ticket
xml = Nokogiri::XML(@request.params['logoutRequest'])
node = xml.root.children.find { |c| c.name =~ /SessionIndex/i }
diff --git a/lib/rack-cas/configuration.rb b/lib/rack-cas/configuration.rb
index 1c7f7bf..ab22a57 100644
--- a/lib/rack-cas/configuration.rb
+++ b/lib/rack-cas/configuration.rb
@@ -1,8 +1,8 @@
module RackCAS
class Configuration
SETTINGS = [:fake, :server_url, :session_store, :exclude_path, :exclude_paths, :extra_attributes_filter,
- :verify_ssl_cert, :renew, :use_saml_validation, :ignore_intercept_validator, :exclude_request_validator, :protocol]
-
+ :verify_ssl_cert, :renew, :use_saml_validation, :ignore_intercept_validator, :exclude_request_validator,
+ :protocol, :pgt_callback_url]
SETTINGS.each do |setting|
attr_accessor setting
diff --git a/lib/rack-cas/proxy_response.rb b/lib/rack-cas/proxy_response.rb
new file mode 100644
index 0000000..4001a16
--- /dev/null
+++ b/lib/rack-cas/proxy_response.rb
@@ -0,0 +1,79 @@
+require 'nokogiri'
+
+module RackCAS
+ class ProxyResponse
+ class ProxyFailure < StandardError; end
+ class RequestInvalidError < ProxyFailure; end
+ class UnauthorizedServiceError < ProxyFailure; end
+ class InternalError < ProxyFailure; end
+
+ REQUEST_HEADERS = { 'Accept' => '*/*' }
+
+ def initialize(url)
+ @url = URL.parse(url)
+ end
+
+ def proxy_ticket
+ if success?
+ xml.xpath('/cas:serviceResponse/cas:proxySuccess/cas:proxyTicket').text
+ else
+ case failure_code
+ when 'INVALID_REQUEST'
+ raise RequestInvalidError, failure_message
+ when 'UNAUTHORIZED_SERVICE'
+ raise UnauthorizedServiceError, failure_message
+ when 'INTERNAL_ERROR'
+ raise InternalError, failure_message
+ else
+ raise ProxyFailure, failure_message
+ end
+ end
+ end
+
+ protected
+
+ def success?
+ @success ||= !!xml.at('/cas:serviceResponse/cas:proxySuccess')
+ end
+
+ def proxy_failure
+ @proxy_failure ||= xml.at('/cas:serviceResponse/cas:proxyFailure')
+ end
+
+ def failure_message
+ if proxy_failure
+ proxy_failure.text.strip
+ end
+ end
+
+ def failure_code
+ if proxy_failure
+ proxy_failure['code']
+ end
+ end
+
+ def response
+ require 'net/http'
+ return @response unless @response.nil?
+
+ http = Net::HTTP.new(@url.host, @url.inferred_port)
+ if @url.scheme == 'https'
+ http.use_ssl = true
+ http.verify_mode = RackCAS.config.verify_ssl_cert? ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
+ end
+
+ http.start do |conn|
+ @response = conn.get(@url.request_uri, REQUEST_HEADERS)
+ end
+
+ @response
+ end
+
+ def xml
+ return @xml unless @xml.nil?
+
+ @xml = Nokogiri::XML(response.body)
+ end
+
+ end
+end
\ No newline at end of file
diff --git a/lib/rack-cas/proxy_ticket_generator.rb b/lib/rack-cas/proxy_ticket_generator.rb
new file mode 100644
index 0000000..d7c03c6
--- /dev/null
+++ b/lib/rack-cas/proxy_ticket_generator.rb
@@ -0,0 +1,24 @@
+require 'rack-cas/url'
+require 'rack-cas/proxy_response'
+
+module RackCAS
+ class ProxyTicketGenerator
+
+ def self.generate(service_url, pgt)
+ response = ProxyResponse.new proxy_url(service_url, pgt)
+ response.proxy_ticket
+ end
+
+ private
+
+ def self.proxy_url(service_url, pgt)
+ service_url = URL.parse(service_url).remove_param('ticket').to_s
+ server_url = RackCAS::URL.parse(RackCAS.config.server_url)
+ server_url.dup.tap do |url|
+ url.append_path('proxy')
+ url.add_params(targetService: service_url, pgt: pgt)
+ end
+ end
+
+ end
+end
\ No newline at end of file
diff --git a/lib/rack-cas/server.rb b/lib/rack-cas/server.rb
index 2679e39..b43cb93 100644
--- a/lib/rack-cas/server.rb
+++ b/lib/rack-cas/server.rb
@@ -23,13 +23,17 @@ def logout_url(params = {})
end
end
- def validate_service(service_url, ticket)
+ def validate_service(service_url, ticket, pgt_url = RackCAS.config.pgt_callback_url)
+ pgt_iou = nil
unless RackCAS.config.use_saml_validation?
- response = ServiceValidationResponse.new validate_service_url(service_url, ticket)
+ response = ServiceValidationResponse.new validate_service_url(service_url, ticket, pgt_url)
+ if !!pgt_url
+ pgt_iou = response.proxy_granting_ticket_iou
+ end
else
response = SAMLValidationResponse.new saml_validate_url(service_url), ticket
end
- [response.user, response.extra_attributes]
+ [response.user, response.extra_attributes, pgt_iou]
end
protected
@@ -39,9 +43,17 @@ def saml_validate_url(service_url)
@url.dup.append_path(path_for_protocol('samlValidate')).add_params(TARGET: service_url)
end
- def validate_service_url(service_url, ticket)
+ def validate_service_url(service_url, ticket, pgt_url = RackCAS.config.pgt_callback_url)
service_url = URL.parse(service_url).remove_param('ticket').to_s
- @url.dup.append_path(path_for_protocol('serviceValidate')).add_params(service: service_url, ticket: ticket)
+ @url.dup.tap do |url|
+ if ticket =~ /\AST\-[^\s]{1,253}\Z/
+ url.append_path(path_for_protocol('serviceValidate'))
+ else
+ url.append_path(path_for_protocol('proxyValidate'))
+ end
+ url.add_params(service: service_url, ticket: ticket)
+ url.add_params(pgtUrl: pgt_url) if pgt_url
+ end
end
def path_for_protocol(path)
@@ -51,5 +63,6 @@ def path_for_protocol(path)
path
end
end
+
end
end
diff --git a/lib/rack-cas/service_validation_response.rb b/lib/rack-cas/service_validation_response.rb
index e5e2d9f..3aba8f1 100644
--- a/lib/rack-cas/service_validation_response.rb
+++ b/lib/rack-cas/service_validation_response.rb
@@ -52,6 +52,12 @@ def extra_attributes
attrs
end
+ def proxy_granting_ticket_iou
+ if success?
+ @proxy_granting_ticket_iou ||= xml.at('//serviceResponse/authenticationSuccess/proxyGrantingTicket').text
+ end
+ end
+
protected
def success?
diff --git a/lib/rack-cas/session_store/active_record.rb b/lib/rack-cas/session_store/active_record.rb
index d80bc04..af8a031 100644
--- a/lib/rack-cas/session_store/active_record.rb
+++ b/lib/rack-cas/session_store/active_record.rb
@@ -1,8 +1,20 @@
module RackCAS
module ActiveRecordStore
+ class ProxyGrantingTicketIou < ActiveRecord::Base
+ end
+
class Session < ActiveRecord::Base
end
+ def self.create_proxy_granting_ticket(pgt_iou, pgt)
+ ProxyGrantingTicketIou.create!(proxy_granting_ticket_iou: pgt_iou, proxy_granting_ticket: pgt)
+ end
+
+ def self.proxy_granting_ticket_for(pgt_iou)
+ proxy_granting_ticket_iou = ProxyGrantingTicketIou.where(proxy_granting_ticket_iou: pgt_iou).first || nil
+ proxy_granting_ticket_iou.proxy_granting_ticket if proxy_granting_ticket_iou
+ end
+
def self.destroy_session_by_cas_ticket(cas_ticket)
affected = Session.delete_all(cas_ticket: cas_ticket)
affected == 1
@@ -11,6 +23,7 @@ def self.destroy_session_by_cas_ticket(cas_ticket)
def self.prune(after = nil)
after ||= Time.now - 2592000 # 30 days ago
Session.where('updated_at < ?', after).delete_all
+ ProxyGrantingTicketIou.where('updated_at < ?', after).delete_all
end
private
diff --git a/lib/rack-cas/session_store/mongoid.rb b/lib/rack-cas/session_store/mongoid.rb
index 3cd1cff..747c0d7 100644
--- a/lib/rack-cas/session_store/mongoid.rb
+++ b/lib/rack-cas/session_store/mongoid.rb
@@ -1,5 +1,15 @@
module RackCAS
module MongoidStore
+
+ class ProxyGrantingTicketIou
+ include Mongoid::Document
+ include Mongoid::Timestamps
+
+ field :_id, type: String
+ field :proxy_granting_ticket_iou, type: String
+ field :proxy_granting_ticket, type: String
+ end
+
class Session
include Mongoid::Document
include Mongoid::Timestamps
@@ -15,6 +25,17 @@ class Session
field :cas_ticket, type: String
end
+ def self.create_proxy_granting_ticket(pgt_iou, pgt)
+ ProxyGrantingTicketIou.create!(proxy_granting_ticket_iou: pgt_iou, proxy_granting_ticket: pgt)
+ end
+
+ def self.proxy_granting_ticket_for(pgt_iou = nil)
+ if pgt_iou
+ proxy_granting_ticket_iou = ProxyGrantingTicketIou.where(proxy_granting_ticket_iou: pgt_iou).first || nil
+ proxy_granting_ticket_iou.proxy_granting_ticket if proxy_granting_ticket_iou
+ end
+ end
+
def self.destroy_session_by_cas_ticket(cas_ticket)
affected = Session.where(cas_ticket: cas_ticket).delete
affected == 1
@@ -23,6 +44,7 @@ def self.destroy_session_by_cas_ticket(cas_ticket)
def self.prune(after = nil)
after ||= Time.now - 2592000 # 30 days ago
Session.where(:updated_at.lte => after).delete
+ ProxyGrantingTicketIou.where(:updated_at.lte => after).delete
end
private
diff --git a/lib/rack-cas/url.rb b/lib/rack-cas/url.rb
index 01bd455..b8ff898 100644
--- a/lib/rack-cas/url.rb
+++ b/lib/rack-cas/url.rb
@@ -7,7 +7,7 @@ def self.parse(uri)
# Addressable to replace + spaces with %20 spaces. Standardizing on %20
# should prevent service lookup issues due to encoding differences.
super.tap do |u|
- u.query_values = u.query_values
+ u.query_values = u.query_values(Array)
end
end
@@ -19,9 +19,9 @@ def append_path(path)
def add_params(params)
self.tap do |u|
- u.query_values = (u.query_values || {}).tap do |qv|
+ u.query_values = (u.query_values(Array) || []).tap do |qv|
params.each do |key, value|
- qv[key] = value
+ qv << [key, value]
end
end
end
@@ -34,9 +34,9 @@ def remove_param(param)
# params can be an array or a hash
def remove_params(params)
self.tap do |u|
- u.query_values = (u.query_values || {}).tap do |qv|
+ u.query_values = (u.query_values(Array) || []).tap do |qv|
params.each do |key, value|
- qv.delete key
+ qv.reject! { |param| param.first == key }
end
end
if u.query_values.empty?
diff --git a/lib/rack/cas.rb b/lib/rack/cas.rb
index 232d125..125e36b 100644
--- a/lib/rack/cas.rb
+++ b/lib/rack/cas.rb
@@ -22,17 +22,23 @@ def call(env)
log env, 'rack-cas: Intercepting ticket validation request.'
begin
- user, extra_attrs = get_user(request.url, cas_request.ticket)
+ user, extra_attrs, pgt_iou = get_user(request.url, cas_request.ticket)
rescue RackCAS::ServiceValidationResponse::TicketInvalidError, RackCAS::SAMLValidationResponse::TicketInvalidError
log env, 'rack-cas: Invalid ticket. Redirecting to CAS login.'
return redirect_to server.login_url(cas_request.service_url).to_s
end
- store_session request, user, cas_request.ticket, extra_attrs
+ store_session request, user, cas_request.ticket, extra_attrs, pgt_iou
return redirect_to cas_request.service_url
end
+ if cas_request.pgt_callback?
+ log env, 'rack-cas: Intercepting proxy granting ticket callback request.'
+ RackCAS.config.session_store.create_proxy_granting_ticket(cas_request.pgt_iou, cas_request.pgt)
+ return [200, {'Content-Type' => 'text/plain'}, ['CAS proxy granting ticket created successfully.']]
+ end
+
if cas_request.logout?
log env, 'rack-cas: Intercepting logout request.'
@@ -78,15 +84,21 @@ def exclude_request?(cas_request)
end
def get_user(service_url, ticket)
- server.validate_service(service_url, ticket)
+ server.validate_service(service_url, ticket, RackCAS.config.pgt_callback_url)
end
- def store_session(request, user, ticket, extra_attrs = {})
+ def store_session(request, user, ticket, extra_attrs = {}, pgt_iou = nil)
if RackCAS.config.extra_attributes_filter?
extra_attrs.select! { |key, val| RackCAS.config.extra_attributes_filter.map(&:to_s).include? key.to_s }
end
request.session['cas'] = { 'user' => user, 'ticket' => ticket, 'extra_attributes' => extra_attrs }
+
+ if pgt_iou
+ pgt = RackCAS.config.session_store.proxy_granting_ticket_for(pgt_iou)
+ request.session['cas']['pgt'] = pgt if pgt
+ end
+
end
def redirect_to(url, status=302)
diff --git a/rack-cas.gemspec b/rack-cas.gemspec
index ddf629a..c77bce8 100644
--- a/rack-cas.gemspec
+++ b/rack-cas.gemspec
@@ -15,7 +15,7 @@ spec = Gem::Specification.new do |s|
s.homepage = 'https://github.com/biola/rack-cas'
s.license = 'MIT'
s.add_dependency 'rack', '>= 1.3'
- s.add_dependency 'addressable', '~> 2.3'
+ s.add_dependency 'addressable', '~> 2'
s.add_dependency 'nokogiri', '~> 1.5'
s.add_development_dependency 'rspec', '~> 3.2'
s.add_development_dependency 'rspec-its', '~> 1.0'
diff --git a/spec/fixtures/jasig_service_response.xml b/spec/fixtures/jasig_service_response.xml
index 215b0eb..a6b917b 100644
--- a/spec/fixtures/jasig_service_response.xml
+++ b/spec/fixtures/jasig_service_response.xml
@@ -16,5 +16,6 @@
CN=Support,OU=Departments,DC=example,DC=com
CN=Testing,OU=Departments,DC=example,DC=com
+ PGTIOU-1234567890
diff --git a/spec/fixtures/rubycas_service_response.xml b/spec/fixtures/rubycas_service_response.xml
index 6d4abda..12f1181 100644
--- a/spec/fixtures/rubycas_service_response.xml
+++ b/spec/fixtures/rubycas_service_response.xml
@@ -58,5 +58,6 @@
- CN=Employees,OU=Security Groups,DC=example,DC=com
]]>
+ PGTIOU-1234567890
\ No newline at end of file
diff --git a/spec/rack-cas/cas_request_spec.rb b/spec/rack-cas/cas_request_spec.rb
index 63e2aa3..f8709e8 100644
--- a/spec/rack-cas/cas_request_spec.rb
+++ b/spec/rack-cas/cas_request_spec.rb
@@ -24,13 +24,13 @@ def app
end
context 'invalid ticket' do
- before { get '/private/something?ticket=BLARG' }
+ before { get '/private/something?ticket=BL+ARG' }
its(:ticket_validation?) { should be false }
its(:ticket) { should be nil }
end
context 'short ticket' do
- before { get '/private/something?ticket=ST-' }
+ before { get '/private/something?ticket=' }
its(:ticket_validation?) { should be false }
its(:ticket) { should be nil }
end
diff --git a/spec/rack-cas/configuration_spec.rb b/spec/rack-cas/configuration_spec.rb
index f4c7725..67463e0 100644
--- a/spec/rack-cas/configuration_spec.rb
+++ b/spec/rack-cas/configuration_spec.rb
@@ -2,6 +2,7 @@
require 'rack-cas/configuration'
describe RackCAS::Configuration do
+
subject { RackCAS::Configuration.new }
context 'when the attribute is neither nil, an empty array, nor false' do
diff --git a/spec/rack-cas/server_spec.rb b/spec/rack-cas/server_spec.rb
index bf9bf11..5670b4d 100644
--- a/spec/rack-cas/server_spec.rb
+++ b/spec/rack-cas/server_spec.rb
@@ -15,13 +15,13 @@
context 'with params' do
subject { server.login_url(service_url, gateway: 'true') }
- its(:to_s) { should eql 'http://example.com/cas/login?gateway=true&service=http%3A%2F%2Fexample.org%2Fwhatever' }
+ its(:to_s) { should eql 'http://example.com/cas/login?service=http%3A%2F%2Fexample.org%2Fwhatever&gateway=true' }
end
context 'with renew = true' do
before { RackCAS.config.stub(renew: true) }
subject { server.login_url(service_url) }
- its(:to_s) { should eql 'http://example.com/cas/login?renew=true&service=http%3A%2F%2Fexample.org%2Fwhatever' }
+ its(:to_s) { should eql 'http://example.com/cas/login?service=http%3A%2F%2Fexample.org%2Fwhatever&renew=true' }
end
end
@@ -42,11 +42,19 @@
end
end
- describe :validate_service do
+ describe :validate_service_without_pgt_url do
subject { server.validate_service(service_url, ticket) }
- its(:length) { should eql 2 }
+ its(:length) { should eql 3 }
its(:first) { should eql 'johnd0' }
- its(:last) { should be_kind_of Hash }
+ its(:last) { should be nil }
+ end
+
+ describe :validate_service_with_pgt_url do
+
+ subject { server.validate_service(service_url, ticket, 'http://example.com/callback') }
+ its(:length) { should eql 3 }
+ its(:first) { should eql 'johnd0' }
+ its(:last) { should eql 'PGTIOU-1234567890' }
end
describe :validate_service_url do
diff --git a/spec/rack-cas/url_spec.rb b/spec/rack-cas/url_spec.rb
index 2054417..6a6775f 100644
--- a/spec/rack-cas/url_spec.rb
+++ b/spec/rack-cas/url_spec.rb
@@ -19,7 +19,7 @@
describe :add_params do
subject { url.add_params(appended: 'param') }
- its(:query) { should eql 'appended=param¶m1=value1¶m2=value2' }
+ its(:query) { should eql 'param1=value1¶m2=value2&appended=param' }
end
describe :remove_param do