rack-cas 0.1.0

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.
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2012 by Biola University
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
@@ -0,0 +1,47 @@
1
+ Rack-CAS
2
+ ========
3
+ Rack-CAS is simple [Rack](http://rack.github.com/) middleware to perform [CAS](http://jasig.org/cas) client authentication.
4
+
5
+ Features
6
+ ========
7
+ * __Rack based__
8
+ * __Framework independent__
9
+ Works with but doesn't depend on Rails, Sinatra, etc.
10
+ * __Minimal dependencies__
11
+ Current gem dependencies are [rack](http://rubygems.org/gems/rack), [addressable](http://rubygems.org/gems/addressable) and [nokogiri](http://rubygems.org/gems/nokogiri).
12
+ * __Supports CAS extra attributes__
13
+ Extra attributes are a mess though. So let me know if your brand of CAS server isn't supported.
14
+
15
+ Coming Soon
16
+ ===========
17
+ * __Single sign out__
18
+
19
+ Requirements
20
+ ============
21
+ * Ruby >= 1.9.2
22
+ * A working [CAS server](http://code.google.com/p/rubycas-server)
23
+
24
+ Installation
25
+ ============
26
+
27
+ gem install rack-cas
28
+
29
+ Or for [Bundler](http://gembundler.com):
30
+
31
+ gem 'rack-cas'
32
+
33
+ Then in your `config.ru` file add
34
+
35
+ require 'rack/cas'
36
+ use Rack::CAS, server_url: 'https://login.example.com/cas'
37
+
38
+ Integration
39
+ ===========
40
+ Your app should __return a [401 status](http://httpstatus.es/401)__ whenever a request is made that requires authentication. Rack-CAS will catch these responses and attempt to authenticate via your CAS server.
41
+
42
+ Once authentication with the CAS server has completed, Rack-CAS will set the following session variables:
43
+
44
+ request.session['cas']['user'] #=> johndoe
45
+ request.session['cas']['extra_attributes'] #=> { 'first_name' => 'John', 'last_name' => ... }
46
+
47
+ __NOTE:__ `extra_attributes` will be an empty hash unless they've been [configured on your CAS server](http://code.google.com/p/rubycas-server/wiki/HowToSendExtraUserAttributes).
@@ -0,0 +1,2 @@
1
+ module RackCAS
2
+ end
@@ -0,0 +1,30 @@
1
+ require 'rack-cas/url'
2
+ require 'rack-cas/service_validation_response'
3
+
4
+ module RackCAS
5
+ class Server
6
+ def initialize(url)
7
+ @url = RackCAS::URL.parse(url)
8
+ end
9
+
10
+ def login_url(service_url)
11
+ @url.dup.append_path('login').add_params(service: service_url)
12
+ end
13
+
14
+ def logout_url
15
+ @url.dup.append_path('logout')
16
+ end
17
+
18
+ def validate_service(service_url, ticket)
19
+ response = ServiceValidationResponse.new validate_service_url(service_url, ticket)
20
+ [response.user, response.extra_attributes]
21
+ end
22
+
23
+ protected
24
+
25
+ def validate_service_url(service_url, ticket)
26
+ service_url = URL.parse(service_url).remove_param('ticket').to_s
27
+ @url.dup.append_path('serviceValidate').add_params(service: service_url, ticket: ticket)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,60 @@
1
+ module RackCAS
2
+ class ServiceValidationResponse
3
+ REQUEST_HEADERS = { 'Accept' => '*/*' }
4
+
5
+ def initialize(url)
6
+ @url = URL.parse(url)
7
+ end
8
+
9
+ def user
10
+ xml.xpath('/cas:serviceResponse/cas:authenticationSuccess/cas:user').text
11
+ end
12
+
13
+ def extra_attributes
14
+ attrs = {}
15
+
16
+ # Jasig style
17
+ if attr_node = xml.at('/cas:serviceResponse/cas:authenticationSuccess/cas:attributes')
18
+ attr_node.children.each do |node|
19
+ if node.is_a? Nokogiri::XML::Element
20
+ attrs[node.name] = node.text
21
+ end
22
+ end
23
+
24
+ # RubyCas-Server style
25
+ else
26
+ xml.at('/cas:serviceResponse/cas:authenticationSuccess').children.each do |node|
27
+ if node.is_a? Nokogiri::XML::Element
28
+ if !node.namespace || !node.namespace.prefix == 'cas'
29
+ # TODO: support JSON encoding
30
+ attrs[node.name] = YAML.load node.text.strip
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ attrs
37
+ end
38
+
39
+ protected
40
+
41
+ def response
42
+ return @response unless @response.nil?
43
+
44
+ http = Net::HTTP.new(@url.host, @url.inferred_port)
45
+ http.use_ssl = true if @url.scheme == 'https'
46
+
47
+ http.start do |conn|
48
+ @response = conn.get(@url.request_uri, REQUEST_HEADERS)
49
+ end
50
+
51
+ @response
52
+ end
53
+
54
+ def xml
55
+ return @xml unless @xml.nil?
56
+
57
+ @xml = Nokogiri::XML(response.body)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,50 @@
1
+ require 'addressable/uri'
2
+
3
+ module RackCAS
4
+ class URL < Addressable::URI
5
+ def append_path(path)
6
+ self.tap do |u|
7
+ u.path = (u.path.split('/') + [path]).join('/')
8
+ end
9
+ end
10
+
11
+ def add_params(params)
12
+ self.tap do |u|
13
+ u.query_values = (u.query_values || {}).tap do |qv|
14
+ params.each do |key, value|
15
+ qv[key] = value
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ def remove_param(param)
22
+ remove_params(Array(param))
23
+ end
24
+
25
+ # params can be an array or a hash
26
+ def remove_params(params)
27
+ self.tap do |u|
28
+ u.query_values = (u.query_values || {}).tap do |qv|
29
+ params.each do |key, value|
30
+ qv.delete key
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ def dup
37
+ duplicated_uri = RackCAS::URL.new(
38
+ :scheme => self.scheme ? self.scheme.dup : nil,
39
+ :user => self.user ? self.user.dup : nil,
40
+ :password => self.password ? self.password.dup : nil,
41
+ :host => self.host ? self.host.dup : nil,
42
+ :port => self.port,
43
+ :path => self.path ? self.path.dup : nil,
44
+ :query => self.query ? self.query.dup : nil,
45
+ :fragment => self.fragment ? self.fragment.dup : nil
46
+ )
47
+ return duplicated_uri
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,3 @@
1
+ module RackCAS
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,79 @@
1
+ require 'rack'
2
+ require 'addressable/uri'
3
+ require 'rack-cas/server'
4
+
5
+ class Rack::CAS
6
+ attr_accessor :server_url
7
+
8
+ def initialize(app, config={})
9
+ @app = app
10
+ @server_url = config.delete(:server_url)
11
+ @config = config
12
+
13
+ raise ArgumentError, 'server_url is required' if @server_url.nil?
14
+ end
15
+
16
+ def call(env)
17
+ request = Rack::Request.new(env)
18
+
19
+ if ticket_validation_request?(request)
20
+ user, extra_attrs = get_user(request)
21
+
22
+ store_session request, user, extra_attrs
23
+ return redirect_to ticketless_url(request)
24
+ end
25
+
26
+ if logout_request?(request)
27
+ request.session.clear
28
+ return redirect_to server.logout_url.to_s
29
+ end
30
+
31
+ response = @app.call(env)
32
+
33
+ if access_denied_response?(response)
34
+ redirect_to server.login_url(request.url).to_s
35
+ else
36
+ response
37
+ end
38
+ end
39
+
40
+ protected
41
+
42
+ def server
43
+ @server ||= RackCAS::Server.new(@server_url)
44
+ end
45
+
46
+ def logout_request?(request)
47
+ request.path_info == '/logout'
48
+ end
49
+
50
+ def ticket_validation_request?(request)
51
+ !get_ticket(request).nil?
52
+ end
53
+
54
+ def access_denied_response?(response)
55
+ response[0] == 401
56
+ end
57
+
58
+ def ticketless_url(request)
59
+ RackCAS::URL.parse(request.url).remove_param('ticket').to_s
60
+ end
61
+
62
+ def get_ticket(request)
63
+ request.params['ticket']
64
+ end
65
+
66
+ def get_user(request)
67
+ server.validate_service(request.url, get_ticket(request))
68
+ end
69
+
70
+ def store_session(request, user, extra_attrs = {})
71
+ request.session['cas'] = {}
72
+ request.session['cas']['user'] = user
73
+ request.session['cas']['extra_attributes'] = extra_attrs
74
+ end
75
+
76
+ def redirect_to(url, status=302)
77
+ [ status, { 'Location' => url, 'Content-Type' => 'text/plain' }, ["Redirecting you to #{url}"] ]
78
+ end
79
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-cas
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Adam Crownoble
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-11 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rack
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: addressable
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '2.3'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '2.3'
46
+ - !ruby/object:Gem::Dependency
47
+ name: nokogiri
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ description: Simple CAS authentication for Rails, Sinatra or any Rack-based site
63
+ email: adam.crownoble@biola.edu
64
+ executables: []
65
+ extensions: []
66
+ extra_rdoc_files: []
67
+ files:
68
+ - README.markdown
69
+ - MIT-LICENSE
70
+ - lib/rack-cas.rb
71
+ - lib/rack-cas/url.rb
72
+ - lib/rack-cas/version.rb
73
+ - lib/rack-cas/service_validation_response.rb
74
+ - lib/rack-cas/server.rb
75
+ - lib/rack/cas.rb
76
+ homepage: https://github.com/biola/rack-cas
77
+ licenses: []
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubyforge_project:
96
+ rubygems_version: 1.8.24
97
+ signing_key:
98
+ specification_version: 3
99
+ summary: Rack-based CAS client
100
+ test_files: []