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.
- data/.gitignore +4 -0
- data/.rvmrc +1 -0
- data/Gemfile +5 -0
- data/README +89 -0
- data/Rakefile +4 -0
- data/TODO +1 -0
- data/app/controllers/cas_client/sessions_controller.rb +20 -0
- data/cas_client.gemspec +24 -0
- data/config/routes.rb +8 -0
- data/lib/cas_client/engine.rb +28 -0
- data/lib/cas_client/errors.rb +10 -0
- data/lib/cas_client/user_api.rb +172 -0
- data/lib/cas_client/version.rb +3 -0
- data/lib/cas_client.rb +12 -0
- data/lib/generators/install_generator.rb +19 -0
- data/lib/generators/templates/cas_server.yml +14 -0
- data/lib/generators/templates/sessions_controller.rb +18 -0
- data/lib/generators/templates/user.rb +16 -0
- data/lib/omniauth/strategies/facebook_signup/configuration.rb +91 -0
- data/lib/omniauth/strategies/facebook_signup/service_ticket_validator.rb +80 -0
- data/lib/omniauth/strategies/facebook_signup/strategy.rb +45 -0
- data/spec/models/user_spec.rb +133 -0
- data/spec/spec_app/.gitignore +4 -0
- data/spec/spec_app/.rspec +1 -0
- data/spec/spec_app/.rvmrc +1 -0
- data/spec/spec_app/Gemfile +11 -0
- data/spec/spec_app/README +256 -0
- data/spec/spec_app/Rakefile +7 -0
- data/spec/spec_app/app/controllers/application_controller.rb +3 -0
- data/spec/spec_app/app/helpers/application_helper.rb +2 -0
- data/spec/spec_app/app/models/user.rb +14 -0
- data/spec/spec_app/app/views/layouts/application.html.erb +14 -0
- data/spec/spec_app/config/application.rb +42 -0
- data/spec/spec_app/config/boot.rb +6 -0
- data/spec/spec_app/config/cas_server.yml +14 -0
- data/spec/spec_app/config/database.yml +22 -0
- data/spec/spec_app/config/environment.rb +5 -0
- data/spec/spec_app/config/environments/development.rb +26 -0
- data/spec/spec_app/config/environments/production.rb +49 -0
- data/spec/spec_app/config/environments/test.rb +35 -0
- data/spec/spec_app/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/spec_app/config/initializers/inflections.rb +10 -0
- data/spec/spec_app/config/initializers/mime_types.rb +5 -0
- data/spec/spec_app/config/initializers/secret_token.rb +7 -0
- data/spec/spec_app/config/initializers/session_store.rb +8 -0
- data/spec/spec_app/config/locales/en.yml +5 -0
- data/spec/spec_app/config/routes.rb +58 -0
- data/spec/spec_app/config.ru +4 -0
- data/spec/spec_app/db/migrate/20110224230909_create_users.rb +17 -0
- data/spec/spec_app/db/schema.rb +25 -0
- data/spec/spec_app/db/seeds.rb +7 -0
- data/spec/spec_app/doc/README_FOR_APP +2 -0
- data/spec/spec_app/lib/tasks/.gitkeep +0 -0
- data/spec/spec_app/public/404.html +26 -0
- data/spec/spec_app/public/422.html +26 -0
- data/spec/spec_app/public/500.html +26 -0
- data/spec/spec_app/public/favicon.ico +0 -0
- data/spec/spec_app/public/images/rails.png +0 -0
- data/spec/spec_app/public/index.html +239 -0
- data/spec/spec_app/public/javascripts/.gitkeep +0 -0
- data/spec/spec_app/public/javascripts/application.js +0 -0
- data/spec/spec_app/public/robots.txt +5 -0
- data/spec/spec_app/public/stylesheets/.gitkeep +0 -0
- data/spec/spec_app/script/rails +6 -0
- data/spec/spec_app/vendor/plugins/.gitkeep +0 -0
- data/spec/spec_helper.rb +57 -0
- data/spec/tasks/spec.rake +19 -0
- metadata +208 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use ree-1.8.7-2010.02@spec_app
|
data/Gemfile
ADDED
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,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
|
data/cas_client.gemspec
ADDED
@@ -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,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
|
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
|