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