rack-cas 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []