factorylabs-casrack_the_authenticator 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ doc/rdoc
2
+ .yardoc
3
+ pkg
data/README.rdoc ADDED
@@ -0,0 +1,87 @@
1
+ Casrack the Authenticator is a
2
+ Rack[http://github.com/chneukirchen/rack] middleware
3
+ that provides CAS[http://www.jasig.org/cas] support.
4
+
5
+ As of the current version, Casrack the Authenticator only supports the most basic
6
+ of CAS scenarios: it requires CAS authentication if it receives a 401 Unauthorized
7
+ response from lower-down in the Rack stack, and it stores the authentication token
8
+ in the session (so logout happens when users close their browers). Casrack the Authenticator
9
+ is a very open-minded beast, though, so please contribute (well-tested) additions to do
10
+ proxy-authentication and single-sign-out, or for anything else you desire.
11
+
12
+ === How-To
13
+
14
+ ==== 1: install
15
+
16
+ [sudo] gem install casrack_the_authenticator
17
+
18
+ ==== 2: set up the middleware:
19
+
20
+ # in your rackup:
21
+ use CasrackTheAuthenticator::Simple, :cas_server => "http://cas.mycompany.com/cas"
22
+ # or "config.middleware.use" if you're on Rails
23
+
24
+ See CasrackTheAuthenticator::Configuration for specifics on that Hash argument.
25
+
26
+ ==== 3: optionally install CasrackTheAuthenticator::RequireCAS if you want _every_ request to require CAS authentication:
27
+
28
+ # in your rackup:
29
+ use CasrackTheAuthenticator::Simple, :cas_server => ...
30
+ use CasrackTheAuthenticator::RequireCAS
31
+ # or "config.middleware.use" if you're on Rails
32
+
33
+ ==== 4: pull the authenticated CAS username out of the Rack session:
34
+
35
+ # in a Rack app:
36
+ def call(env)
37
+ user = cas_user(env)
38
+ ...
39
+ end
40
+
41
+ def cas_user(env)
42
+ username = Rack::Request.new(env).session[CasrackTheAuthenticator::USERNAME_PARAM]
43
+ User.find_by_username(username)
44
+ end
45
+
46
+ # or, in a Rails controller:
47
+
48
+ def cas_user
49
+ username = Rack::Request.new(request.env).session[CasrackTheAuthenticator::USERNAME_PARAM]
50
+ User.find_by_username(username)
51
+ end
52
+
53
+ === Disconnected (Fake) Mode
54
+
55
+ I've often found myself working on a CAS-ified project while away from the office
56
+ and unable to access the CAS server. To support this type of disconnected development,
57
+ just substitute in the CasrackTheAuthenticator::Fake middleware. It acts like
58
+ CasrackTheAuthenticator::Simple, but it uses HTTP Basic authentication against a
59
+ preset list of usernames.
60
+
61
+ A common pattern for Rails apps is to create a disconnected environment:
62
+
63
+ ==== 1: set up <tt>[rails_root]/config/database.yml</tt>
64
+
65
+ development: &DEV
66
+ adapter: sqlite3
67
+ database: db/development.sqlite3
68
+ pool: 5
69
+ timeout: 5000
70
+
71
+ # development mode when disconnected from MITRE
72
+ disconnected:
73
+ <<: *DEV
74
+
75
+ ==== 2: set up <tt>[rails_root]/config/disconnected.rb</tt>:
76
+
77
+ load './development.rb'
78
+ config.middleware.swap 'CasrackTheAuthenticator::Simple', CasrackTheAuthenticator::Fake, 'jimbob', 'sueann'
79
+
80
+ ==== 3: run in disconnected mode:
81
+
82
+ script/server -e disconnected
83
+
84
+ ==== 4: login as 'jimbob' or 'sueann'
85
+
86
+ Passwords are ignored.
87
+
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'rake'
2
+
3
+ CASRACK_PROJECT_ROOT = File.expand_path(File.dirname(__FILE__))
4
+ Dir['developer_tasks/*.rake'].each { |f| load(f) }
5
+
6
+ desc "Default: run all tests, including features"
7
+ task :default => ['test']
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.6.0
@@ -0,0 +1,83 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{casrack_the_authenticator}
8
+ s.version = "1.6.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["James Rosen"]
12
+ s.date = %q{2009-09-23}
13
+ s.description = %q{CAS Authentication via Rack Middleware}
14
+ s.email = %q{james.a.rosen@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "README.rdoc"
17
+ ]
18
+ s.files = [
19
+ ".gitignore",
20
+ "README.rdoc",
21
+ "Rakefile",
22
+ "VERSION",
23
+ "casrack_the_authenticator.gemspec",
24
+ "developer_tasks/doc.rake",
25
+ "developer_tasks/gem.rake",
26
+ "developer_tasks/test.rake",
27
+ "features/fake.feature",
28
+ "features/require_cas.feature",
29
+ "features/simple.feature",
30
+ "features/step_definitions/fake_cas_steps.rb",
31
+ "features/step_definitions/rack_steps.rb",
32
+ "features/support/assertions.rb",
33
+ "features/support/rack_support.rb",
34
+ "lib/casrack_the_authenticator.rb",
35
+ "lib/casrack_the_authenticator/configuration.rb",
36
+ "lib/casrack_the_authenticator/fake.rb",
37
+ "lib/casrack_the_authenticator/require_cas.rb",
38
+ "lib/casrack_the_authenticator/service_ticket_validator.rb",
39
+ "lib/casrack_the_authenticator/simple.rb",
40
+ "test/configuration_test.rb",
41
+ "test/fake_test.rb",
42
+ "test/service_ticket_validator_test.rb",
43
+ "test/simple_test.rb",
44
+ "test/test_helper.rb"
45
+ ]
46
+ s.homepage = %q{http://github.com/gcnovus/casrack_the_authenticator}
47
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Casrack the Authenticator: RDoc", "--charset", "utf-8"]
48
+ s.require_paths = ["lib"]
49
+ s.rubygems_version = %q{1.3.3}
50
+ s.summary = %q{CAS Authentication via Rack Middleware}
51
+ s.test_files = [
52
+ "test/configuration_test.rb",
53
+ "test/fake_test.rb",
54
+ "test/service_ticket_validator_test.rb",
55
+ "test/simple_test.rb",
56
+ "test/test_helper.rb"
57
+ ]
58
+
59
+ if s.respond_to? :specification_version then
60
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
61
+ s.specification_version = 3
62
+
63
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
64
+ s.add_runtime_dependency(%q<nokogiri>, ["~> 1.3.2"])
65
+ s.add_development_dependency(%q<thoughtbot-shoulda>, ["~> 2.10.2"])
66
+ s.add_development_dependency(%q<jferris-mocha>, ["~> 0.9.7"])
67
+ s.add_development_dependency(%q<redgreen>, ["~> 1.2.2"])
68
+ s.add_development_dependency(%q<rack>, ["~> 1.0.0"])
69
+ else
70
+ s.add_dependency(%q<nokogiri>, ["~> 1.3.2"])
71
+ s.add_dependency(%q<thoughtbot-shoulda>, ["~> 2.10.2"])
72
+ s.add_dependency(%q<jferris-mocha>, ["~> 0.9.7"])
73
+ s.add_dependency(%q<redgreen>, ["~> 1.2.2"])
74
+ s.add_dependency(%q<rack>, ["~> 1.0.0"])
75
+ end
76
+ else
77
+ s.add_dependency(%q<nokogiri>, ["~> 1.3.2"])
78
+ s.add_dependency(%q<thoughtbot-shoulda>, ["~> 2.10.2"])
79
+ s.add_dependency(%q<jferris-mocha>, ["~> 0.9.7"])
80
+ s.add_dependency(%q<redgreen>, ["~> 1.2.2"])
81
+ s.add_dependency(%q<rack>, ["~> 1.0.0"])
82
+ end
83
+ end
@@ -0,0 +1,22 @@
1
+ require 'yard'
2
+ require 'yard/rake/yardoc_task'
3
+
4
+ desc "Generate RDoc"
5
+ task :doc => ['doc:generate']
6
+
7
+ namespace :doc do
8
+
9
+ doc_dir = File.join(CASRACK_PROJECT_ROOT, 'doc', 'rdoc')
10
+
11
+ YARD::Rake::YardocTask.new(:generate) do |yt|
12
+ yt.files = Dir.glob(File.join(CASRACK_PROJECT_ROOT, 'lib', '**', '*.rb')) +
13
+ [ File.join(CASRACK_PROJECT_ROOT, 'README.rdoc') ]
14
+ yt.options = ['--output-dir', doc_dir, '--readme', 'README.rdoc']
15
+ end
16
+
17
+ desc "Remove generated documenation"
18
+ task :clean do
19
+ rm_r doc_dir if File.exists?(doc_dir)
20
+ end
21
+
22
+ end
@@ -0,0 +1,20 @@
1
+ begin
2
+ require 'jeweler'
3
+ Jeweler::Tasks.new do |gemspec|
4
+ gemspec.name = "factorylabs-casrack_the_authenticator"
5
+ gemspec.summary = "CAS Authentication via Rack Middleware"
6
+ gemspec.description = gemspec.summary
7
+ gemspec.email = "james.a.rosen@gmail.com"
8
+ gemspec.homepage = "http://github.com/gcnovus/casrack_the_authenticator"
9
+ gemspec.authors = ["James Rosen"]
10
+ gemspec.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Casrack the Authenticator: RDoc", "--charset", "utf-8"]
11
+ gemspec.platform = Gem::Platform::RUBY
12
+ gemspec.add_dependency 'nokogiri', '~> 1.4.1'
13
+ gemspec.add_development_dependency 'thoughtbot-shoulda', '~> 2.10.2'
14
+ gemspec.add_development_dependency 'jferris-mocha', '~> 0.9.7'
15
+ gemspec.add_development_dependency 'redgreen', '~> 1.2.2'
16
+ gemspec.add_development_dependency 'rack', '~> 1.0.0'
17
+ end
18
+ rescue LoadError
19
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
20
+ end
@@ -0,0 +1,24 @@
1
+ require 'cucumber'
2
+ require 'cucumber/rake/task'
3
+ require 'rake/testtask'
4
+
5
+ Cucumber::Rake::Task.new(:features) do |t|
6
+ t.cucumber_opts = "features --format pretty"
7
+ end
8
+
9
+ LIB_DIRECTORIES = FileList.new do |fl|
10
+ fl.include "#{CASRACK_PROJECT_ROOT}/lib"
11
+ fl.include "#{CASRACK_PROJECT_ROOT}/test/lib"
12
+ end
13
+
14
+ TEST_FILES = FileList.new do |fl|
15
+ fl.include "#{CASRACK_PROJECT_ROOT}/test/**/*_test.rb"
16
+ fl.exclude "#{CASRACK_PROJECT_ROOT}/test/test_helper.rb"
17
+ fl.exclude "#{CASRACK_PROJECT_ROOT}/test/lib/**/*.rb"
18
+ end
19
+
20
+ Rake::TestTask.new(:test) do |t|
21
+ t.libs = LIB_DIRECTORIES
22
+ t.test_files = TEST_FILES
23
+ t.verbose = true
24
+ end
@@ -0,0 +1,34 @@
1
+ Feature: Fake CAS Authentication
2
+ In order to support developers who work away from the office
3
+ Casrack the Authenticator provides a Fake middleware.
4
+
5
+ Background:
6
+ Given a Rack application exists
7
+ And the fake Casrack middleware is installed with user "missy"
8
+
9
+ Scenario: not-signed-in user makes a request to a public area
10
+ Given the underlying Rack application returns [200, {}, "Public Information"]
11
+ When I make a request
12
+ Then the response should be successful
13
+ And the response body should include "Public Information"
14
+ And the CAS user should be nil
15
+
16
+ Scenario: not-signed-in user makes a request to a private area
17
+ Given the underlying Rack application returns [401, {}, 'Restricted. Go away.']
18
+ When I make a request
19
+ Then I should be presented with a HTTP Basic authentication request
20
+ And the CAS user should be nil
21
+
22
+ Scenario: user presenting invalid credentials makes a request to a private area
23
+ Given the underlying Rack application returns [401, {}, 'Restricted. Go away.']
24
+ When I make a request as "thomas"
25
+ Then I should be presented with a HTTP Basic authentication request
26
+ And the CAS user should be nil
27
+
28
+ Scenario: user presenting credentials makes a request to a private area
29
+ Given the underlying Rack application returns [200, {}, 'Restricted. Shh.']
30
+ When I make a request as "missy"
31
+ Then the response should be successful
32
+ And the response body should include "Restricted. Shh."
33
+ And the CAS user should be "missy"
34
+
@@ -0,0 +1,19 @@
1
+ Feature: Required CAS Authentication
2
+ In order make it dead-simple for users to implement a CAS-login requirement
3
+ Casrack the Authenticator provides a RequireCAS middleware.
4
+
5
+ Background:
6
+ Given a Rack application exists
7
+ And the RequireCAS middleware is installed
8
+ And the simple version of Casrack the Authenticator is installed
9
+ And the underlying Rack application returns [200, {}, "Public Information"]
10
+
11
+ Scenario: not-signed-in user makes a request
12
+ When I make a request
13
+ Then I should be redirected to CAS
14
+ And the "Content-Type" header should be "text/plain"
15
+
16
+ Scenario: signed-in-user makes a request
17
+ When I return to "http://myapp.org/bar" with a valid CAS ticket for "tperon"
18
+ Then the response should be successful
19
+ And the response body should include "Public Information"
@@ -0,0 +1,32 @@
1
+ Feature: Simple CAS Authentication
2
+ In order to maintain privacy and accountability while keeping IT costs low
3
+ "Upper Management" wants to use CAS authentication
4
+
5
+ Background:
6
+ Given a Rack application exists
7
+ And the simple version of Casrack the Authenticator is installed
8
+
9
+ Scenario: not-signed-in user accesses public material
10
+ Given the underlying Rack application returns [200, {}, "Public Information"]
11
+ When I make a request
12
+ Then the response should be successful
13
+ And the response body should include "Public Information"
14
+
15
+ Scenario: not-signed-in user accesses restricted material
16
+ Given the underlying Rack application returns [401, {}, "Restricted!"]
17
+ When I make a request to "http://myapp.com/foo?bar=baz"
18
+ Then I should be redirected to CAS
19
+ And the "Content-Type" header should be "text/plain"
20
+ And CAS should return me to "http://myapp.com/foo?bar=baz"
21
+
22
+ Scenario: returning from a successful CAS sign-in
23
+ Given the underlying Rack application returns [200, {}, "Information for jswanson"]
24
+ When I return to "http://myapp.org/bar" with a valid CAS ticket for "jswanson"
25
+ Then the CAS user should be "jswanson"
26
+ And the response should be successful
27
+ And the response body should include "Information for jswanson"
28
+
29
+ Scenario: returning from an unsuccessful CAS sign-in
30
+ Given the underlying Rack application returns [401, {}, "Restricted!"]
31
+ When I return to "http://myapp.org/bar" with an invalid CAS ticket
32
+ Then the CAS user should be nil
@@ -0,0 +1,14 @@
1
+ Given /^the fake Casrack middleware is installed with user "([^\"]*)"$/ do |user|
2
+ self.app = CasrackTheAuthenticator::Fake.new(app, user)
3
+ end
4
+
5
+ Then /^I should be presented with a HTTP Basic authentication request$/ do
6
+ assert_equal 401, response.status
7
+ assert_equal 'text/plain', response.headers['Content-Type']
8
+ assert_equal '0', response.headers['Content-Length']
9
+ assert_equal 'Basic realm="CasrackTheAuthenticator::Fake"', response.headers['WWW-Authenticate']
10
+ end
11
+
12
+ When /^I make a request as "([^\"]*)"$/ do |username|
13
+ get '/', { 'HTTP_AUTHORIZATION' => 'Basic ' + ["#{username}:sekret"].pack("m*") }
14
+ end
@@ -0,0 +1,69 @@
1
+ require File.join(File.dirname(__FILE__), '..', '..', 'test', 'test_helper.rb')
2
+ require 'rack/mock'
3
+
4
+ Given /^a Rack application exists$/ do
5
+ self.underlying_app = lambda { |env| nil }
6
+ self.app = underlying_app
7
+ end
8
+
9
+ Given /^the simple version of Casrack the Authenticator is installed$/ do
10
+ self.app = CasrackTheAuthenticator::Simple.new(app, :cas_server => 'http://cas.test/cas')
11
+ end
12
+
13
+ Given /^the RequireCAS middleware is installed$/ do
14
+ self.app = CasrackTheAuthenticator::RequireCAS.new(app)
15
+ end
16
+
17
+ Given /^the underlying Rack application returns (.+)$/ do |response|
18
+ underlying_app.stubs(:call).returns(eval(response))
19
+ end
20
+
21
+ When /^I make a request$/ do
22
+ get '/'
23
+ end
24
+
25
+ When /^I make a request to "([^\"]*)"$/ do |url|
26
+ get url
27
+ end
28
+
29
+ When /^I return to "([^\"]*)" with a valid CAS ticket for "([^\"]*)"$/ do |url, user|
30
+ http_request_returns_valid_cas_user user
31
+ url << (url.include?('?') ? '&' : '?') << 'ticket=ST-123455'
32
+ When "I make a request to \"#{url}\""
33
+ end
34
+
35
+ When /^I return to "([^\"]*)" with an invalid CAS ticket$/ do |url|
36
+ http_request_returns_error
37
+ url << (url.include?('?') ? '&' : '?') << 'ticket=ST-not-a-valid-ticket'
38
+ When "I make a request to \"#{url}\""
39
+ end
40
+
41
+ Then /^the CAS user should be nil$/ do
42
+ assert_equal nil, session[:cas_user]
43
+ end
44
+
45
+ Then /^the CAS user should be "([^\"]*)"$/ do |username|
46
+ assert_equal username, session[:cas_user]
47
+ end
48
+
49
+ Then /^the response should be successful$/ do
50
+ assert((200..299).include?(response.status), "Expected success, but was #{response.status}")
51
+ end
52
+
53
+ Then /^the response body should include "([^\"]*)"$/ do |text|
54
+ assert response.body.include?(text)
55
+ end
56
+
57
+ Then /^I should be redirected to CAS$/ do
58
+ assert((300..399).include?(response.status), "Expected redirect, but was #{response.status}")
59
+ assert !redirected_to.nil?
60
+ assert redirected_to.to_s =~ /cas/i
61
+ end
62
+
63
+ Then /^CAS should return me to "([^\"]*)"$/ do |return_to|
64
+ assert_equal return_to, service_url
65
+ end
66
+
67
+ Then /^the "([^\"]*)" header should be "([^\"]*)"$/ do |header, value|
68
+ assert_equal value, response.headers[header]
69
+ end
@@ -0,0 +1,3 @@
1
+ require 'test/unit/assertions'
2
+
3
+ World(Test::Unit::Assertions)
@@ -0,0 +1,89 @@
1
+ require 'rack'
2
+ require 'rack/mock'
3
+ Rack::MockRequest.class_eval do
4
+
5
+ class <<self
6
+ def env_for_with_hook(*args)
7
+ return ::RackSupport.current_env unless ::RackSupport.current_env.nil?
8
+ env = env_for_without_hook(*args)
9
+ ::RackSupport.current_env = env
10
+ env
11
+ end
12
+ alias_method :env_for_without_hook, :env_for
13
+ alias_method :env_for, :env_for_with_hook
14
+ end
15
+
16
+ end
17
+
18
+ module RackSupport
19
+
20
+ VALID_CAS_USER_XML = <<-EOX
21
+ <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
22
+ <cas:authenticationSuccess>
23
+ <cas:user>%s</cas:user>
24
+ </cas:authenticationSuccess>
25
+ </cas:serviceResponse>
26
+ EOX
27
+
28
+ @@current_env = nil
29
+
30
+ def self.current_env
31
+ @@current_env
32
+ end
33
+
34
+ def self.current_env=(env)
35
+ @@current_env = env
36
+ end
37
+
38
+ attr_accessor :underlying_app, :app, :response, :session
39
+
40
+ def cleanup_rack_variables
41
+ ::RackSupport.current_env = nil
42
+ self.underlying_app = self.app = self.session = self.response = nil
43
+ end
44
+
45
+ def get(url, headers = {})
46
+ env = RackSupport.current_env = Rack::MockRequest.env_for(url, headers)
47
+ if session
48
+ env['rack.session'] = session
49
+ else
50
+ self.session = Rack::Request.new(env).session
51
+ end
52
+ self.response = Rack::MockRequest.new(app).get url, headers
53
+ end
54
+
55
+ def redirected_to
56
+ return nil if response.headers['Location'].nil?
57
+ URI::parse(response.headers['Location'])
58
+ end
59
+
60
+ def service_url
61
+ return nil if redirected_to.nil?
62
+ Rack::Utils.parse_nested_query(redirected_to.query)['service']
63
+ end
64
+
65
+ def http_request_returns_valid_cas_user(username)
66
+ http_request_returns VALID_CAS_USER_XML % username
67
+ end
68
+
69
+ def http_request_returns_error
70
+ http_request_returns "this is not a valid CAS service-ticket-validation response!"
71
+ end
72
+
73
+ def http_request_returns(content)
74
+ server = Object.new
75
+ connection = Object.new
76
+ response = Object.new
77
+ Net::HTTP.stubs(:new).returns(server)
78
+ server.stubs(:start).yields(connection)
79
+ connection.stubs(:get).returns(response)
80
+ response.stubs(:body).returns(content)
81
+ end
82
+
83
+ end
84
+
85
+ After do
86
+ cleanup_rack_variables
87
+ end
88
+
89
+ World(RackSupport)