cas_client 0.1.0

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