cas_client 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.
Files changed (68) hide show
  1. data/.gitignore +4 -0
  2. data/.rvmrc +1 -0
  3. data/Gemfile +5 -0
  4. data/README +89 -0
  5. data/Rakefile +4 -0
  6. data/TODO +1 -0
  7. data/app/controllers/cas_client/sessions_controller.rb +20 -0
  8. data/cas_client.gemspec +24 -0
  9. data/config/routes.rb +8 -0
  10. data/lib/cas_client/engine.rb +28 -0
  11. data/lib/cas_client/errors.rb +10 -0
  12. data/lib/cas_client/user_api.rb +172 -0
  13. data/lib/cas_client/version.rb +3 -0
  14. data/lib/cas_client.rb +12 -0
  15. data/lib/generators/install_generator.rb +19 -0
  16. data/lib/generators/templates/cas_server.yml +14 -0
  17. data/lib/generators/templates/sessions_controller.rb +18 -0
  18. data/lib/generators/templates/user.rb +16 -0
  19. data/lib/omniauth/strategies/facebook_signup/configuration.rb +91 -0
  20. data/lib/omniauth/strategies/facebook_signup/service_ticket_validator.rb +80 -0
  21. data/lib/omniauth/strategies/facebook_signup/strategy.rb +45 -0
  22. data/spec/models/user_spec.rb +133 -0
  23. data/spec/spec_app/.gitignore +4 -0
  24. data/spec/spec_app/.rspec +1 -0
  25. data/spec/spec_app/.rvmrc +1 -0
  26. data/spec/spec_app/Gemfile +11 -0
  27. data/spec/spec_app/README +256 -0
  28. data/spec/spec_app/Rakefile +7 -0
  29. data/spec/spec_app/app/controllers/application_controller.rb +3 -0
  30. data/spec/spec_app/app/helpers/application_helper.rb +2 -0
  31. data/spec/spec_app/app/models/user.rb +14 -0
  32. data/spec/spec_app/app/views/layouts/application.html.erb +14 -0
  33. data/spec/spec_app/config/application.rb +42 -0
  34. data/spec/spec_app/config/boot.rb +6 -0
  35. data/spec/spec_app/config/cas_server.yml +14 -0
  36. data/spec/spec_app/config/database.yml +22 -0
  37. data/spec/spec_app/config/environment.rb +5 -0
  38. data/spec/spec_app/config/environments/development.rb +26 -0
  39. data/spec/spec_app/config/environments/production.rb +49 -0
  40. data/spec/spec_app/config/environments/test.rb +35 -0
  41. data/spec/spec_app/config/initializers/backtrace_silencers.rb +7 -0
  42. data/spec/spec_app/config/initializers/inflections.rb +10 -0
  43. data/spec/spec_app/config/initializers/mime_types.rb +5 -0
  44. data/spec/spec_app/config/initializers/secret_token.rb +7 -0
  45. data/spec/spec_app/config/initializers/session_store.rb +8 -0
  46. data/spec/spec_app/config/locales/en.yml +5 -0
  47. data/spec/spec_app/config/routes.rb +58 -0
  48. data/spec/spec_app/config.ru +4 -0
  49. data/spec/spec_app/db/migrate/20110224230909_create_users.rb +17 -0
  50. data/spec/spec_app/db/schema.rb +25 -0
  51. data/spec/spec_app/db/seeds.rb +7 -0
  52. data/spec/spec_app/doc/README_FOR_APP +2 -0
  53. data/spec/spec_app/lib/tasks/.gitkeep +0 -0
  54. data/spec/spec_app/public/404.html +26 -0
  55. data/spec/spec_app/public/422.html +26 -0
  56. data/spec/spec_app/public/500.html +26 -0
  57. data/spec/spec_app/public/favicon.ico +0 -0
  58. data/spec/spec_app/public/images/rails.png +0 -0
  59. data/spec/spec_app/public/index.html +239 -0
  60. data/spec/spec_app/public/javascripts/.gitkeep +0 -0
  61. data/spec/spec_app/public/javascripts/application.js +0 -0
  62. data/spec/spec_app/public/robots.txt +5 -0
  63. data/spec/spec_app/public/stylesheets/.gitkeep +0 -0
  64. data/spec/spec_app/script/rails +6 -0
  65. data/spec/spec_app/vendor/plugins/.gitkeep +0 -0
  66. data/spec/spec_helper.rb +57 -0
  67. data/spec/tasks/spec.rake +19 -0
  68. metadata +208 -0
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use ree-1.8.7-2010.02@spec_app
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in cas_client.gemspec
4
+ gemspec
5
+
data/README ADDED
@@ -0,0 +1,89 @@
1
+ CAS Client Gem
2
+ ==============
3
+
4
+ Introduction
5
+ ------------
6
+
7
+ This gem is meant to be used as a wrapper over the User API and CAS authentication functionality. It consists of these two parts, as a controller that can be inherited from, and a module to be included into an ActiveRecord model.
8
+
9
+ SessionsController
10
+ ------------------
11
+
12
+ Provides a template for an app's SessionsController.
13
+
14
+ `
15
+ class SessionsController < CASClient::SessionsController
16
+
17
+ skip_before_filter :authorize, :only => [:new, :create] # disable whatever authorization mechanism you have for these actions so that the server can redirect users who are not logged in
18
+
19
+ def create
20
+ session[:uuid] = request.env['rack.auth']['uid'] # do whatever you need here to persist the users session within your app
21
+ redirect_to '/'
22
+ end
23
+
24
+ def cas_logout
25
+ session[:uuid] = nil # do whatever you need here to kill a user's session
26
+ super
27
+ end
28
+
29
+ end
30
+ `
31
+
32
+ Scenarios:
33
+ * User login
34
+ 1. User goes to '/login' which hits Sessions#new
35
+ 2. User is redirected to CAS to login
36
+ 3. CAS will authenticate and redirect back to Sessions#create which will receive all of the users credential info in request.env['rack.auth']
37
+
38
+ * User logout
39
+ 1. User goes to '/logout'
40
+ 2. User is redirected to CAS where their session on CAS is expired
41
+ 3. CAS opens up connections to '/cas_logout' to expire sessions on all client apps covered by the CAS
42
+
43
+ User API
44
+ --------
45
+
46
+ A module to be included into your User model:
47
+
48
+ `
49
+ class User < ActiveRecord::Base
50
+ include CASClient::UserAPI
51
+
52
+ after_create :cas_create
53
+ after_save :cas_update_attributes
54
+
55
+ def self.cas_map
56
+ {
57
+ :uuid => :username,
58
+ :first_name => :firstname,
59
+ :middle_name => :middlename,
60
+ :last_name => :lastname,
61
+ :email => :email_address
62
+ }
63
+ end
64
+ end
65
+ `
66
+
67
+ **The callbacks in the example model above are optional.**
68
+
69
+ Class Methods:
70
+ * User.cas_all: returns an array with hashes of attributes for all users
71
+ * User.cas_fetch_user(uuid): returns a hash of the attributes for this user or nil if there is no user with this uuid
72
+ * User.cas_uuid_available?(uuid): returns true or false depending on the availability of the uuid on CAS
73
+
74
+ Instance Methods:
75
+ * user.cas_create: creates a new user on CAS with this user's attributes
76
+ - **NOTE: if the unique user ID that is submitted has already been taken, CASClient will raise UserAlreadyExists
77
+ * user.cas_update_attributes: updates the attributes for this user on CAS
78
+ * user.cas_retrieve_attributes: retrieves the attributes for this user on CAS
79
+ - **NOTE: if the unique user ID that is submitted has already been taken, CASClient will raise UserAlreadyExists
80
+ * user.cas_reset_password: will flag the user as needing password reset, and send them and email to do so
81
+ - **NOTE: if this user does not have an email address on CAS before calling this method, CASClient will raise MissingEmail
82
+
83
+ Creating a User account through Facebook
84
+ ----------------------------------------
85
+
86
+ 1. Add a link to the facebook_signup_path where you would have your icon for Facebook Connect.
87
+ 2. In Sessions#create you can use User.find_or_create_facebook_user_by_* to wrap the lower-level User.find_or_create_by_* call, but populated with the Facebook parameters.
88
+ - Pass the request.env['rack.auth'] hash into this method call
89
+ - This will return a user
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ load File.dirname(__FILE__) + "/spec/tasks/spec.rake"
data/TODO ADDED
@@ -0,0 +1 @@
1
+ 1. test SessionsController
@@ -0,0 +1,20 @@
1
+ class CASClient::SessionsController < ApplicationController
2
+ unloadable # ensures this controller doesn't get reloaded between requests in development
3
+
4
+ def new
5
+ redirect_to '/auth/cas'
6
+ end
7
+
8
+ def create
9
+ raise "Not Implemented"
10
+ end
11
+
12
+ def destroy
13
+ redirect_to ::CAS_SERVER["domain"] + '/logout'
14
+ end
15
+
16
+ def cas_logout
17
+ render :nothing => true
18
+ end
19
+
20
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "cas_client/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "cas_client"
7
+ s.version = CASClient::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Ryan Moran"]
10
+ s.email = ["ryan.moran@revolutionprep.com"]
11
+ s.homepage = "http://www.revolutionprep.com"
12
+ s.summary = %q{CAS client implementation}
13
+ s.description = %q{Helpers, controllers and middleware to implement a CAS client app}
14
+
15
+ s.rubyforge_project = "cas_client"
16
+
17
+ s.add_dependency('oa-enterprise', '>= 0.1.6')
18
+ s.add_dependency('yajl-ruby')
19
+
20
+ s.files = `git ls-files`.split("\n")
21
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
22
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
23
+ s.require_paths = ["lib"]
24
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,8 @@
1
+ Rails.application.routes.draw do
2
+ get "/login", :to => "sessions#new", :as => :login
3
+ get "/logout", :to => "sessions#destroy", :as => :logout
4
+ get "/cas_logout", :to => "sessions#cas_logout"
5
+ get "auth/cas/callback", :to => "sessions#create"
6
+ get "auth/facebook_signup/callback", :to => "sessions#create"
7
+ get "auth/facebook_signup", :as => :facebook_signup
8
+ end
@@ -0,0 +1,28 @@
1
+ require 'cas_client'
2
+ require 'rails'
3
+ require 'omniauth/enterprise'
4
+ require 'net/https'
5
+ require File.dirname(__FILE__) + '/user_api.rb'
6
+
7
+ module CASClient
8
+
9
+ class Engine < Rails::Engine
10
+ engine_name :cas_client
11
+
12
+ initializer "cas_client.configure_omniauth" do |app|
13
+ if File.exists?(Rails.root.to_s + '/config/cas_server.yml')
14
+ ::CAS_SERVER = YAML::load(File.open(Rails.root.to_s + '/config/cas_server.yml'))[Rails.env]
15
+ OmniAuth::Strategies.autoload :FacebookSignup, 'facebook_signup'
16
+ app.config.middleware.use OmniAuth::Builder do
17
+ provider :c_a_s, :cas_server => CAS_SERVER["domain"]
18
+ provider :facebook_signup, :cas_server => CAS_SERVER["domain"]
19
+ end
20
+ end
21
+ end
22
+
23
+ generators do
24
+ require File.dirname(__FILE__) + '/../generators/install_generator.rb'
25
+ end
26
+ end
27
+
28
+ end
@@ -0,0 +1,10 @@
1
+ module CASClient
2
+ class UUIDNotFound < StandardError
3
+ end
4
+
5
+ class MissingEmail < StandardError
6
+ end
7
+
8
+ class UserAlreadyExists < StandardError
9
+ end
10
+ end
@@ -0,0 +1,172 @@
1
+ require 'net/http'
2
+ require 'yajl'
3
+
4
+ module CASClient
5
+ module UserAPI
6
+
7
+ def self.included(base)
8
+ base.extend ClassMethods
9
+ end
10
+
11
+ module ClassMethods
12
+
13
+ def find_or_create_facebook_user(attribute, auth)
14
+ if auth['provider'] == 'facebook_signup'
15
+ attributes = {}
16
+ cas_map.invert.each_pair do |k,v|
17
+ attributes.store(k.to_sym, auth['extra'][v.to_s]) unless attribute.to_sym == k.to_sym
18
+ end
19
+ send(("find_or_create_by_" + attribute), auth['extra'][cas_map.invert[attribute.to_sym].to_s], attributes)
20
+ end
21
+ end
22
+
23
+ def cas_all
24
+ res = fetch("#{CAS_SERVER["domain"]}/api/users")
25
+ Yajl::Parser.new(:symbolize_keys => true).parse(res.body)
26
+ end
27
+
28
+ def cas_fetch_user(uuid)
29
+ res = fetch("#{CAS_SERVER["domain"]}/api/users/#{uuid}")
30
+ Yajl::Parser.new(:symbolize_keys => true).parse(res.body)
31
+ rescue CASClient::UUIDNotFound
32
+ end
33
+
34
+ def cas_uuid_available?(uuid)
35
+ !cas_fetch_user(uuid)
36
+ end
37
+
38
+ def fetch(uri_string, limit = 10)
39
+ raise StandardError, 'HTTP redirect too deep' if limit == 0
40
+ url = URI.parse(uri_string)
41
+ handle_response(make_request(url, Net::HTTP::Get.new(url.path)), limit)
42
+ end
43
+ private :fetch
44
+
45
+ def make_request(url, req)
46
+ req.basic_auth CAS_SERVER["username"], CAS_SERVER["password"]
47
+ res = Net::HTTP.start(url.host, url.port) { |http| http.request(req) }
48
+ end
49
+ private :make_request
50
+
51
+ def handle_response(res, limit = 10)
52
+ case res
53
+ when Net::HTTPSuccess
54
+ res
55
+ when Net::HTTPRedirection
56
+ fetch(res['location'], limit - 1)
57
+ when Net::HTTPNotFound
58
+ raise CASClient::UUIDNotFound
59
+ else
60
+ res.error!
61
+ end
62
+ end
63
+ private :handle_response
64
+
65
+ def method_missing(method_sym, *arguments, &block)
66
+ if method_sym.to_s =~ /^find_or_create_facebook_user_by_(.*)$/
67
+ find_or_create_facebook_user($1, arguments.first)
68
+ else
69
+ super
70
+ end
71
+ end
72
+
73
+ end
74
+
75
+ def cas_password=(_password)
76
+ @cas_password = _password
77
+ end
78
+
79
+ def cas_password
80
+ @cas_password
81
+ end
82
+
83
+ def cas_password_confirmation=(_password_confirmation)
84
+ @cas_password_confirmation = _password_confirmation
85
+ end
86
+
87
+ def cas_password_confirmation
88
+ @cas_password_confirmation
89
+ end
90
+
91
+ def cas_create
92
+ url = URI.parse("#{CAS_SERVER["domain"]}/api/users")
93
+ res = handle_response(make_request(url, Net::HTTP::Post.new(url.path)))
94
+ Yajl::Parser.new(:symbolize_keys => true).parse(res.body)
95
+ end
96
+
97
+ def cas_update_attributes
98
+ if self.changes.keys.include?(self.class.cas_map[:uuid].to_s)
99
+ _user_identifier = self.changes[self.class.cas_map[:uuid].to_s].first
100
+ else
101
+ _user_identifier = self.send(self.class.cas_map[:uuid])
102
+ end
103
+ url = URI.parse("#{CAS_SERVER["domain"]}/api/users/#{_user_identifier}")
104
+ res = handle_response(make_request(url, Net::HTTP::Put.new(url.path)))
105
+ Yajl::Parser.new(:symbolize_keys => true).parse(res.body)
106
+ end
107
+
108
+ def cas_retrieve_attributes
109
+ res = fetch("#{CAS_SERVER["domain"]}/api/users/#{self.send(self.class.cas_map[:uuid])}")
110
+ Yajl::Parser.new(:symbolize_keys => true).parse(res.body)
111
+ end
112
+
113
+ def cas_reset_password
114
+ res = fetch("#{CAS_SERVER["domain"]}/api/users/#{self.send(self.class.cas_map[:uuid])}/reset_password")
115
+ res.body
116
+ end
117
+
118
+ def fetch(uri_string, limit = 10)
119
+ raise StandardError, 'HTTP redirect too deep' if limit == 0
120
+ url = URI.parse(uri_string)
121
+ handle_response(make_request(url, Net::HTTP::Get.new(url.path)), limit)
122
+ end
123
+ private :fetch
124
+
125
+ def make_request(url, req)
126
+ req.basic_auth CAS_SERVER["username"], CAS_SERVER["password"]
127
+ req.set_form_data(build_user_attributes_hash, ';') if [Net::HTTP::Post, Net::HTTP::Put].include?(req.class)
128
+ Net::HTTP.new(url.host, url.port).start { |http| http.request(req) }
129
+ end
130
+ private :make_request
131
+
132
+ def handle_response(res, limit = 10)
133
+ case res
134
+ when Net::HTTPSuccess
135
+ res
136
+ when Net::HTTPRedirection
137
+ fetch(res['location'], limit - 1)
138
+ when Net::HTTPNotFound
139
+ raise CASClient::UUIDNotFound
140
+ when Net::HTTPForbidden
141
+ case Yajl::Parser.new(:symbolize_keys => true).parse(res.body)[:errors].first
142
+ when "Email is required to reset password"
143
+ raise CASClient::MissingEmail, "Email is required to reset password"
144
+ when "Uuid has already been taken"
145
+ raise CASClient::UserAlreadyExists, "This unique user ID has already been taken"
146
+ else
147
+ puts res.body
148
+ res.error!
149
+ end
150
+ else
151
+ res.error!
152
+ end
153
+ end
154
+ private :handle_response
155
+
156
+ def build_user_attributes_hash
157
+ hash = {}
158
+ self.class.cas_map.each_pair do |k,v|
159
+ if value = self.send(v.to_sym)
160
+ hash.store(k.to_sym, value)
161
+ end
162
+ end
163
+ if cas_password && cas_password_confirmation
164
+ hash.store(:password, cas_password)
165
+ hash.store(:password_confirmation, cas_password_confirmation)
166
+ end
167
+ hash
168
+ end
169
+ private :build_user_attributes_hash
170
+
171
+ end
172
+ end
@@ -0,0 +1,3 @@
1
+ module CASClient
2
+ VERSION = "0.1.0"
3
+ end
data/lib/cas_client.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.require
4
+
5
+ if defined?(Rails) && Rails::VERSION::MAJOR == 3
6
+ require 'cas_client/engine'
7
+ require 'omniauth/strategies/facebook_signup/strategy'
8
+ end
9
+ require 'cas_client/errors'
10
+
11
+ module CASClient
12
+ end
@@ -0,0 +1,19 @@
1
+ require 'rails/generators'
2
+
3
+ module CASClient
4
+ class InstallGenerator < Rails::Generators::Base
5
+
6
+ source_root File.join(File.dirname(__FILE__), 'templates')
7
+
8
+ def manifest
9
+ copy_file "sessions_controller.rb", "app/controllers/sessions_controller.rb.example"
10
+ copy_file "cas_server.yml", "config/cas_server.yml.example"
11
+ copy_file "user.rb", "app/models/user.rb.example"
12
+ end
13
+
14
+ def self.namespace(name = nil)
15
+ super.gsub("c_a_s_client", "cas_client")
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ development:
2
+ domain: 'http://localhost:4000'
3
+ username: 'test'
4
+ password: 'password'
5
+
6
+ test:
7
+ domain: 'http://localhost:4000'
8
+ username: 'test'
9
+ password: 'password'
10
+
11
+ production:
12
+ domain: 'http://localhost:4000'
13
+ username: 'test'
14
+ password: 'password'
@@ -0,0 +1,18 @@
1
+ class SessionsController < CASClient::SessionsController
2
+
3
+ # disable whatever authorization mechanism you have for these actions so that the server can redirect users who are not logged in
4
+ # skip_before_filter :authorize, :only => [:new, :create]
5
+
6
+ def create
7
+ # do whatever you need here to persist the users session within your app
8
+ # session[:uuid] = request.env['rack.auth']['uid']
9
+ # redirect_to '/'
10
+ end
11
+
12
+ def cas_logout
13
+ # do whatever you need here to kill a user's session
14
+ # session[:uuid] = nil
15
+ super # DO NOT REMOVE THIS
16
+ end
17
+
18
+ end
@@ -0,0 +1,16 @@
1
+ class User < ActiveRecord::Base
2
+ include CASClient::UserAPI
3
+
4
+ # after_create :cas_create
5
+ # after_save :cas_update_attributes
6
+
7
+ def self.cas_map
8
+ {
9
+ :uuid => :username,
10
+ :first_name => :firstname,
11
+ :middle_name => :middlename,
12
+ :last_name => :lastname,
13
+ :email => :email_address
14
+ }
15
+ end
16
+ end
@@ -0,0 +1,91 @@
1
+ require 'rack'
2
+
3
+ module OmniAuth
4
+ module Strategies
5
+ class FacebookSignup
6
+ class Configuration
7
+
8
+ DEFAULT_LOGIN_URL = "%s/auth/facebook/prerequest"
9
+
10
+ DEFAULT_SERVICE_VALIDATE_URL = "%s/serviceValidate"
11
+
12
+ # @param [Hash] params configuration options
13
+ # @option params [String, nil] :cas_server the CAS server root URL; probably something like
14
+ # `http://cas.mycompany.com` or `http://cas.mycompany.com/cas`; optional.
15
+ # @option params [String, nil] :cas_login_url (:cas_server + '/login') the URL to which to
16
+ # redirect for logins; options if `:cas_server` is specified,
17
+ # required otherwise.
18
+ # @option params [String, nil] :cas_service_validate_url (:cas_server + '/serviceValidate') the
19
+ # URL to use for validating service tickets; optional if `:cas_server` is
20
+ # specified, requred otherwise.
21
+ def initialize(params)
22
+ parse_params params
23
+ end
24
+
25
+ # Build a CAS login URL from +service+.
26
+ #
27
+ # @param [String] service the service (a.k.a. return-to) URL
28
+ #
29
+ # @return [String] a URL like `http://cas.mycompany.com/login?service=...`
30
+ def login_url(service)
31
+ append_service @login_url, service
32
+ end
33
+
34
+ # Build a service-validation URL from +service+ and +ticket+.
35
+ # If +service+ has a ticket param, first remove it. URL-encode
36
+ # +service+ and add it and the +ticket+ as paraemters to the
37
+ # CAS serviceValidate URL.
38
+ #
39
+ # @param [String] service the service (a.k.a. return-to) URL
40
+ # @param [String] ticket the ticket to validate
41
+ #
42
+ # @return [String] a URL like `http://cas.mycompany.com/serviceValidate?service=...&ticket=...`
43
+ def service_validate_url(service, ticket)
44
+ service = service.sub(/[?&]ticket=[^?&]+/, '')
45
+ url = append_service(@service_validate_url, service)
46
+ url << '&ticket=' << Rack::Utils.escape(ticket)
47
+ end
48
+
49
+ private
50
+
51
+ def parse_params(params)
52
+ if params[:cas_server].nil? && params[:cas_login_url].nil?
53
+ raise ArgumentError.new(":cas_server or :cas_login_url MUST be provided")
54
+ end
55
+ @login_url = params[:cas_login_url]
56
+ @login_url ||= DEFAULT_LOGIN_URL % params[:cas_server]
57
+ validate_is_url 'login URL', @login_url
58
+
59
+ if params[:cas_server].nil? && params[:cas_service_validate_url].nil?
60
+ raise ArgumentError.new(":cas_server or :cas_service_validate_url MUST be provided")
61
+ end
62
+ @service_validate_url = params[:cas_service_validate_url]
63
+ @service_validate_url ||= DEFAULT_SERVICE_VALIDATE_URL % params[:cas_server]
64
+ validate_is_url 'service-validate URL', @service_validate_url
65
+ end
66
+
67
+ IS_NOT_URL_ERROR_MESSAGE = "%s is not a valid URL"
68
+
69
+ def validate_is_url(name, possibly_a_url)
70
+ url = URI.parse(possibly_a_url) rescue nil
71
+ raise ArgumentError.new(IS_NOT_URL_ERROR_MESSAGE % name) unless url.kind_of?(URI::HTTP)
72
+ end
73
+
74
+ # Adds +service+ as an URL-escaped parameter to +base+.
75
+ #
76
+ # @param [String] base the base URL
77
+ # @param [String] service the service (a.k.a. return-to) URL.
78
+ #
79
+ # @return [String] the new joined URL.
80
+ def append_service(base, service)
81
+ result = base.dup
82
+ result << '?force_create=1'
83
+ result << (result.include?('?') ? '&' : '?')
84
+ result << 'service='
85
+ result << Rack::Utils.escape(service)
86
+ end
87
+
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,80 @@
1
+ require 'nokogiri'
2
+
3
+ module OmniAuth
4
+ module Strategies
5
+ class FacebookSignup
6
+ class ServiceTicketValidator
7
+
8
+ VALIDATION_REQUEST_HEADERS = { 'Accept' => '*/*' }
9
+
10
+ # Build a validator from a +configuration+, a
11
+ # +return_to+ URL, and a +ticket+.
12
+ #
13
+ # @param [OmniAuth::Strategies::CAS::Configuration] configuration the CAS configuration
14
+ # @param [String] return_to_url the URL of this CAS client service
15
+ # @param [String] ticket the service ticket to validate
16
+ def initialize(configuration, return_to_url, ticket)
17
+ @uri = URI.parse(configuration.service_validate_url(return_to_url, ticket))
18
+ end
19
+
20
+ # Request validation of the ticket from the CAS server's
21
+ # serviceValidate (CAS 2.0) function.
22
+ #
23
+ # Swallows all XML parsing errors (and returns +nil+ in those cases).
24
+ #
25
+ # @return [Hash, nil] a user information hash if the response is valid; +nil+ otherwise.
26
+ #
27
+ # @raise any connection errors encountered.
28
+ def user_info
29
+ parse_user_info(find_authentication_success(get_service_response_body))
30
+ end
31
+
32
+ private
33
+
34
+ # turns an `<cas:authenticationSuccess>` node into a Hash;
35
+ # returns nil if given nil
36
+ def parse_user_info(node)
37
+ return nil if node.nil?
38
+ node.children.inject({}) do |hash, child|
39
+ unless child.kind_of?(Nokogiri::XML::Text) ||
40
+ child.name == 'cas:proxies' ||
41
+ child.name == 'proxies'
42
+ hash[child.name.sub(/^cas:/, '')] = child.content
43
+ end
44
+ hash
45
+ end
46
+ end
47
+
48
+ # finds an `<cas:authenticationSuccess>` node in
49
+ # a `<cas:serviceResponse>` body if present; returns nil
50
+ # if the passed body is nil or if there is no such node.
51
+ def find_authentication_success(body)
52
+ return nil if body.nil? || body == ''
53
+ begin
54
+ doc = Nokogiri::XML(body)
55
+ begin
56
+ doc.xpath('/cas:serviceResponse/cas:authenticationSuccess')
57
+ rescue Nokogiri::XML::XPath::SyntaxError
58
+ doc.xpath('/serviceResponse/authenticationSuccess')
59
+ end
60
+ rescue Nokogiri::XML::XPath::SyntaxError
61
+ nil
62
+ end
63
+ end
64
+
65
+ # retrieves the `<cas:serviceResponse>` XML from the CAS server
66
+ def get_service_response_body
67
+ result = ''
68
+ http = Net::HTTP.new(@uri.host, @uri.port)
69
+ http.use_ssl = @uri.port == 443 || @uri.instance_of?(URI::HTTPS)
70
+ http.start do |c|
71
+ response = c.get "#{@uri.path}?#{@uri.query}", VALIDATION_REQUEST_HEADERS
72
+ result = response.body
73
+ end
74
+ result
75
+ end
76
+
77
+ end
78
+ end
79
+ end
80
+ end