hancock 0.0.2
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/LICENSE +20 -0
- data/README.md +134 -0
- data/Rakefile +72 -0
- data/lib/hancock.rb +47 -0
- data/lib/models/consumer.rb +24 -0
- data/lib/models/user.rb +63 -0
- data/lib/sinatra/hancock/defaults.rb +25 -0
- data/lib/sinatra/hancock/openid_server.rb +96 -0
- data/lib/sinatra/hancock/sessions.rb +53 -0
- data/lib/sinatra/hancock/users.rb +79 -0
- data/lib/sinatra/hancock/views/openid_server/yadis.erb +13 -0
- data/lib/sinatra/hancock/views/sessions/unauthenticated.haml +14 -0
- data/lib/sinatra/hancock/views/users/register_form.haml +12 -0
- data/lib/sinatra/hancock/views/users/signup_confirmation.haml +13 -0
- data/lib/sinatra/hancock/views/users/signup_form.haml +18 -0
- data/spec/acceptance/signing_up_spec.rb +90 -0
- data/spec/app.rb +29 -0
- data/spec/features/sessions.feature +24 -0
- data/spec/features/signup.feature +26 -0
- data/spec/features/sso.feature +35 -0
- data/spec/features/step_definitions/sessions_steps.rb +31 -0
- data/spec/features/step_definitions/signup_steps.rb +56 -0
- data/spec/features/step_definitions/sso_steps.rb +74 -0
- data/spec/features/support/env.rb +16 -0
- data/spec/fixtures.rb +39 -0
- data/spec/matchers.rb +170 -0
- data/spec/spec_helper.rb +61 -0
- data/spec/units/app_spec.rb +15 -0
- data/spec/units/consumer_spec.rb +43 -0
- data/spec/units/identity_provider_spec.rb +27 -0
- data/spec/units/landing_page_spec.rb +42 -0
- data/spec/units/login_spec.rb +61 -0
- data/spec/units/logout_spec.rb +22 -0
- data/spec/units/openid_spec.rb +154 -0
- data/spec/units/signup_spec.rb +34 -0
- data/spec/units/user_spec.rb +10 -0
- metadata +143 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
%fieldset
|
|
2
|
+
%legend Signup for an account
|
|
3
|
+
%form{:action => '/sso/signup', :method => 'POST'}
|
|
4
|
+
%label{:for => 'first_name'}
|
|
5
|
+
First Name:
|
|
6
|
+
%input{:type => 'text', :name => 'first_name'}
|
|
7
|
+
%br
|
|
8
|
+
%label{:for => 'last_name'}
|
|
9
|
+
Last Name:
|
|
10
|
+
%input{:type => 'text', :name => 'last_name'}
|
|
11
|
+
%br
|
|
12
|
+
%label{:for => 'email'}
|
|
13
|
+
Email:
|
|
14
|
+
%input{:type => 'text', :name => 'email'}
|
|
15
|
+
%br
|
|
16
|
+
%input{:type => 'submit', :value => 'Signup'}
|
|
17
|
+
or
|
|
18
|
+
%a{:href => '/'} Cancel
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__)+'/../spec_helper')
|
|
2
|
+
|
|
3
|
+
describe "visiting /sso/signup" do
|
|
4
|
+
def app
|
|
5
|
+
Hancock::App
|
|
6
|
+
end
|
|
7
|
+
before(:each) do
|
|
8
|
+
@user = Hancock::User.new(:email => /\w+@\w+\.\w{2,3}/.gen.downcase,
|
|
9
|
+
:first_name => /\w+/.gen.capitalize,
|
|
10
|
+
:last_name => /\w+/.gen.capitalize)
|
|
11
|
+
end
|
|
12
|
+
describe "when signing up" do
|
|
13
|
+
it "should sign the user up" do
|
|
14
|
+
get '/sso/signup'
|
|
15
|
+
|
|
16
|
+
post '/sso/signup', :email => @user.email,
|
|
17
|
+
:first_name => @user.first_name,
|
|
18
|
+
:last_name => @user.last_name
|
|
19
|
+
|
|
20
|
+
confirmation_url = last_response.body.to_s.match(%r!/sso/register/\w{40}!)
|
|
21
|
+
confirmation_url.should_not be_nil
|
|
22
|
+
|
|
23
|
+
get "#{confirmation_url}"
|
|
24
|
+
password = /\w+{9,32}/.gen
|
|
25
|
+
|
|
26
|
+
last_response.body.to_s.should have_selector("form[action='#{confirmation_url}']")
|
|
27
|
+
|
|
28
|
+
post "#{confirmation_url}", :password => password, :password_confirmation => password
|
|
29
|
+
follow_redirect!
|
|
30
|
+
|
|
31
|
+
last_response.body.to_s.should have_selector("h3:contains('Hello #{@user.first_name} #{@user.last_name}')")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe "and form hacking" do
|
|
35
|
+
it "should be unauthorized" do
|
|
36
|
+
get '/sso/signup'
|
|
37
|
+
|
|
38
|
+
post '/sso/signup', :email => @user.email,
|
|
39
|
+
:first_name => @user.first_name,
|
|
40
|
+
:last_name => @user.last_name
|
|
41
|
+
|
|
42
|
+
fake_url = /\w+{9,40}/.gen
|
|
43
|
+
get "/sso/register/#{fake_url}"
|
|
44
|
+
last_response.body.to_s.should match(/BadRequest/)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
if ENV['WATIR']
|
|
50
|
+
begin
|
|
51
|
+
require 'safariwatir'
|
|
52
|
+
describe "with no valid browser sessions" do
|
|
53
|
+
before(:each) do
|
|
54
|
+
@sso_server = 'http://hancock.atmos.org/sso'
|
|
55
|
+
@browser = Watir::Safari.new
|
|
56
|
+
@browser.goto("http://localhost:5000/sso/logout")
|
|
57
|
+
end
|
|
58
|
+
it "should browse properly in safari" do
|
|
59
|
+
# session cookie fails on localhost :\
|
|
60
|
+
# sso_server = 'http://localhost:20000/sso'
|
|
61
|
+
|
|
62
|
+
# make a request and signup to access the site
|
|
63
|
+
@browser.goto('http://localhost:5000/')
|
|
64
|
+
@browser.link(:url, "#{@sso_server}/signup").click
|
|
65
|
+
@browser.text_field(:name, :first_name).set(@user.first_name)
|
|
66
|
+
@browser.text_field(:name, :last_name).set(@user.last_name)
|
|
67
|
+
@browser.text_field(:name, :email).set(@user.email)
|
|
68
|
+
@browser.button(:value, 'Signup').click
|
|
69
|
+
|
|
70
|
+
# hacky way to strip this outta the markup in development mode
|
|
71
|
+
register_url = @browser.html.match(%r!#{@sso_server}/register/\w{40}!).to_s
|
|
72
|
+
register_url.should_not be_nil
|
|
73
|
+
password = /\w+{9,32}/.gen
|
|
74
|
+
|
|
75
|
+
# register from the url from their email
|
|
76
|
+
@browser.goto(register_url)
|
|
77
|
+
@browser.text_field(:name, :password).set(password)
|
|
78
|
+
@browser.text_field(:name, :password_confirmation).set(password)
|
|
79
|
+
@browser.button(:value, 'Am I Done Yet?').click
|
|
80
|
+
|
|
81
|
+
sleep 2
|
|
82
|
+
|
|
83
|
+
# sent back to be greeted on the consumer
|
|
84
|
+
@browser.html.should match(%r!Hancock Client: Sinatra!)
|
|
85
|
+
@browser.html.should have_selector("h2 a[href='mailto:#{@user.email}']:contains('#{@user.first_name} #{@user.last_name}')")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
rescue; end
|
|
89
|
+
end
|
|
90
|
+
end
|
data/spec/app.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Sinatra
|
|
2
|
+
module Hancock
|
|
3
|
+
module TestApp
|
|
4
|
+
module Helpers
|
|
5
|
+
def landing_page
|
|
6
|
+
<<-HAML
|
|
7
|
+
%h3 Hello #{session_user.first_name} #{session_user.last_name}!
|
|
8
|
+
- unless @consumers.empty?
|
|
9
|
+
%ul#consumers
|
|
10
|
+
- @consumers.each do |consumer|
|
|
11
|
+
%li
|
|
12
|
+
%a{:href => consumer.url}= consumer.label
|
|
13
|
+
HAML
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
def self.registered(app)
|
|
17
|
+
app.helpers Helpers
|
|
18
|
+
app.set :sessions, true
|
|
19
|
+
app.get '/' do
|
|
20
|
+
ensure_authenticated
|
|
21
|
+
@consumers = ::Hancock::Consumer.visible
|
|
22
|
+
@consumers += ::Hancock::Consumer.internal if session_user.internal?
|
|
23
|
+
haml landing_page
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
::Hancock::App.register(Hancock::TestApp)
|
|
29
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Feature: Logging In to an SSO Account
|
|
2
|
+
In order to authenticate existing users
|
|
3
|
+
As an existing user
|
|
4
|
+
Scenario: logging in
|
|
5
|
+
Given I am not logged in on the sso provider
|
|
6
|
+
And a valid consumer and user exists
|
|
7
|
+
When I request the landing page
|
|
8
|
+
Then I should see the login form
|
|
9
|
+
When I login
|
|
10
|
+
Then I should see a list of consumers
|
|
11
|
+
Scenario: logging in as a redirect from a consumer
|
|
12
|
+
Given I am not logged in on the sso provider
|
|
13
|
+
And a valid consumer and user exists
|
|
14
|
+
When I request authentication returning to the consumer app
|
|
15
|
+
Then I should see the login form
|
|
16
|
+
When I login
|
|
17
|
+
Then I should be redirected to the consumer app to start the handshake
|
|
18
|
+
Scenario: logging in
|
|
19
|
+
Given I am not logged in on the sso provider
|
|
20
|
+
And a valid consumer and user exists
|
|
21
|
+
When I request authentication
|
|
22
|
+
Then I should see the login form
|
|
23
|
+
When I login
|
|
24
|
+
Then I should be redirected to the sso provider root on login
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Feature: Signing Up for an SSO Account
|
|
2
|
+
In order to get users involved
|
|
3
|
+
As a new user
|
|
4
|
+
Scenario: signing up as a redirect from a consumer
|
|
5
|
+
Given I am not logged in on the sso provider
|
|
6
|
+
And a valid consumer exists
|
|
7
|
+
When I request authentication returning to the consumer app
|
|
8
|
+
Then I should see the login form
|
|
9
|
+
When I click signup
|
|
10
|
+
Then I should see the signup form
|
|
11
|
+
When I signup with valid info
|
|
12
|
+
Then I should receive a registration url via email
|
|
13
|
+
When I hit the registration url and provide a password
|
|
14
|
+
Then I should be redirected to the consumer app
|
|
15
|
+
|
|
16
|
+
Scenario: signing up
|
|
17
|
+
Given I am not logged in on the sso provider
|
|
18
|
+
And a valid consumer exists
|
|
19
|
+
Given I request authentication
|
|
20
|
+
Then I should see the login form
|
|
21
|
+
When I click signup
|
|
22
|
+
Then I should see the signup form
|
|
23
|
+
When I signup with valid info
|
|
24
|
+
Then I should receive a registration url via email
|
|
25
|
+
When I hit the registration url and provide a password
|
|
26
|
+
Then I should be redirected to the sso provider root
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Feature: Authenticating Against the SSO Provider
|
|
2
|
+
In order to authenticate existing users on a consumer via openid
|
|
3
|
+
Scenario: SSO Server should be an OpenID Identity Provider
|
|
4
|
+
When I request the OpenID Identity Provider Page
|
|
5
|
+
Then I should receive a yadis document from the sso server
|
|
6
|
+
|
|
7
|
+
Scenario: OpenID Mode CheckIDSetup
|
|
8
|
+
Given a valid consumer and user exists
|
|
9
|
+
And I am logged in on the sso provider
|
|
10
|
+
When I request the sso page with a checkid mode of checkIDSetup
|
|
11
|
+
Then I should be redirected to the consumer app with openid params
|
|
12
|
+
Scenario: OpenID Mode CheckIDSetup
|
|
13
|
+
Given a valid consumer and user exists
|
|
14
|
+
And I am logged in on the sso provider
|
|
15
|
+
When I request the sso page with a checkid mode of checkIDSetup for another users's page
|
|
16
|
+
Then I should not be redirected to the consumer app with openid params
|
|
17
|
+
Scenario: OpenID Mode CheckIDSetup unauthenticated
|
|
18
|
+
Given a valid consumer and user exists
|
|
19
|
+
When I request the sso page with a checkid mode of checkIDSetup
|
|
20
|
+
Then I should see the login form
|
|
21
|
+
|
|
22
|
+
Scenario: OpenID Mode Immediate
|
|
23
|
+
Given a valid consumer and user exists
|
|
24
|
+
And I am logged in on the sso provider
|
|
25
|
+
When I request the sso page with a checkid mode of immediate
|
|
26
|
+
Then I should be redirected to the consumer app with openid params
|
|
27
|
+
Scenario: OpenID Mode Immediate unauthenticated
|
|
28
|
+
Given a valid consumer and user exists
|
|
29
|
+
When I request the sso page with a checkid mode of immediate
|
|
30
|
+
Then I should see the login form
|
|
31
|
+
|
|
32
|
+
Scenario: OpenID Mode Associate
|
|
33
|
+
Given a valid consumer and user exists
|
|
34
|
+
When I request the sso page with a checkid mode of associate
|
|
35
|
+
Then I should receive an associate response from the sso server
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Given /^a valid consumer and user exists$/ do
|
|
2
|
+
@consumer = ::Hancock::Consumer.gen(:internal)
|
|
3
|
+
@user = ::Hancock::User.gen
|
|
4
|
+
get '/sso/logout' # log us out if we're logged in
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
Then /^I login$/ do
|
|
8
|
+
post "/sso/login", :email => @user.email,
|
|
9
|
+
:password => @user.password
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
Then /^I should be redirected to the consumer app to start the handshake$/ do
|
|
13
|
+
redirection = Addressable::URI.parse(last_response.headers['Location'])
|
|
14
|
+
|
|
15
|
+
"#{redirection.scheme}://#{redirection.host}#{redirection.path}".should eql(@consumer.url)
|
|
16
|
+
redirection.query_values['id'].to_i.should eql(@user.id)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Then /^I should be redirected to the sso provider root on login$/ do
|
|
20
|
+
last_response.headers['Location'].should eql('/')
|
|
21
|
+
follow_redirect!
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
When /^I request the landing page$/ do
|
|
25
|
+
get '/'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
Then /^I should see a list of consumers$/ do
|
|
29
|
+
last_response.headers['Location'].should eql('/')
|
|
30
|
+
follow_redirect!
|
|
31
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Given /^I am not logged in on the sso provider$/ do
|
|
2
|
+
@user = Hancock::User.new(:email => /\w+@\w+\.\w{2,3}/.gen.downcase,
|
|
3
|
+
:first_name => /\w+/.gen.capitalize,
|
|
4
|
+
:last_name => /\w+/.gen.capitalize)
|
|
5
|
+
get "/sso/logout"
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
Given /^a valid consumer exists$/ do
|
|
9
|
+
@consumer = ::Hancock::Consumer.gen(:internal)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
Given /^I request authentication$/ do
|
|
13
|
+
get "/sso/login"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
Given /^I request authentication returning to the consumer app$/ do
|
|
17
|
+
get "/sso/login?return_to=#{@consumer.url}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
Then /^I should see the login form$/ do
|
|
21
|
+
last_response.should be_a_login_form
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
Given /^I click signup$/ do
|
|
25
|
+
get "/sso/signup"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
Then /^I should see the signup form$/ do
|
|
29
|
+
last_response.should be_a_signup_form
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
Given /^I signup with valid info$/ do
|
|
33
|
+
post "/sso/signup", 'email' => @user.email,
|
|
34
|
+
'first_name' => @user.first_name,
|
|
35
|
+
'last_name' => @user.last_name
|
|
36
|
+
last_response.status.should eql(200)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
Then /^I should receive a registration url via email$/ do
|
|
40
|
+
@confirmation_url = last_response.body.to_s.match(%r!/sso/register/\w{40}!).to_s
|
|
41
|
+
@confirmation_url.should_not match(/^\s*$/)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
Given /^I hit the registration url and provide a password$/ do
|
|
45
|
+
get @confirmation_url
|
|
46
|
+
post @confirmation_url, 'user[password]' => @user.password,
|
|
47
|
+
'used[password_confirmation]' => @user.password
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
Then /^I should be redirected to the sso provider root$/ do
|
|
51
|
+
last_response.headers['Location'].should eql('/')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
Then /^I should be redirected to the consumer app$/ do
|
|
55
|
+
last_response.headers['Location'].should eql(@consumer.url)
|
|
56
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
When /^I am logged in on the sso provider$/ do
|
|
2
|
+
@identity_url = "http://example.org/sso/users/#{@user.id}"
|
|
3
|
+
post "/sso/login", :email => @user.email,
|
|
4
|
+
:password => @user.password
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
When /^I request the sso page with a checkid mode of checkIDSetup$/ do
|
|
8
|
+
params = {
|
|
9
|
+
"openid.ns" => "http://specs.openid.net/auth/2.0",
|
|
10
|
+
"openid.mode" => "checkid_setup",
|
|
11
|
+
"openid.return_to" => @consumer.url,
|
|
12
|
+
"openid.identity" => @identity_url,
|
|
13
|
+
"openid.claimed_id" => @identity_url
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get "/sso", params
|
|
17
|
+
end
|
|
18
|
+
When %r!^I request the sso page with a checkid mode of checkIDSetup for another users's page$! do
|
|
19
|
+
params = {
|
|
20
|
+
"openid.ns" => "http://specs.openid.net/auth/2.0",
|
|
21
|
+
"openid.mode" => "checkid_setup",
|
|
22
|
+
"openid.return_to" => @consumer.url,
|
|
23
|
+
"openid.identity" => 'http://example.org/sso/users/42',
|
|
24
|
+
"openid.claimed_id" => 'http://example.org/sso/users/42'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get "/sso", params
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
When /^I request the sso page with a checkid mode of associate$/ do
|
|
31
|
+
@openid_session = OpenID::Consumer::AssociationManager.create_session("DH-SHA1")
|
|
32
|
+
params = {
|
|
33
|
+
"openid.ns" => 'http://specs.openid.net/auth/2.0',
|
|
34
|
+
"openid.mode" => "associate",
|
|
35
|
+
"openid.session_type" => 'DH-SHA1',
|
|
36
|
+
"openid.assoc_type" => 'HMAC-SHA1',
|
|
37
|
+
"openid.dh_consumer_public"=> @openid_session.get_request['dh_consumer_public']
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get "/sso", params
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
When /^I request the sso page with a checkid mode of immediate$/ do
|
|
44
|
+
params = {
|
|
45
|
+
"openid.ns" => "http://specs.openid.net/auth/2.0",
|
|
46
|
+
"openid.mode" => "checkid_immediate",
|
|
47
|
+
"openid.return_to" => @consumer.url,
|
|
48
|
+
"openid.identity" => @identity_url,
|
|
49
|
+
"openid.claimed_id" => @identity_url
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get "/sso", params
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
Then /^I should be redirected to the consumer app with openid params$/ do
|
|
56
|
+
last_response.should be_a_redirect_to_the_consumer(@consumer, @user)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
Then /^I should receive an associate response from the sso server$/ do
|
|
60
|
+
last_response.should be_an_openid_associate_response(@openid_session)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
Then /^I should not be redirected to the consumer app with openid params$/ do
|
|
64
|
+
last_response.status.should eql(403)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
When /^I request the OpenID Identity Provider Page$/ do
|
|
68
|
+
get '/sso/xrds'
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
Then /^I should receive a yadis document from the sso server$/ do
|
|
72
|
+
last_response.should be_an_identity_provider
|
|
73
|
+
end
|
|
74
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__)+'/../../spec_helper')
|
|
2
|
+
require 'haml'
|
|
3
|
+
|
|
4
|
+
Hancock::App.set :environment, :development
|
|
5
|
+
|
|
6
|
+
World do
|
|
7
|
+
def app
|
|
8
|
+
@app = Rack::Builder.new do
|
|
9
|
+
run Hancock::App
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
include Rack::Test::Methods
|
|
13
|
+
include Webrat::Methods
|
|
14
|
+
include Webrat::Matchers
|
|
15
|
+
include Hancock::Matchers
|
|
16
|
+
end
|
data/spec/fixtures.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Hancock::User.fix {{
|
|
2
|
+
:enabled => true,
|
|
3
|
+
:email => /\w+@\w+.\w{2,3}/.gen.downcase,
|
|
4
|
+
:first_name => /\w+/.gen.capitalize,
|
|
5
|
+
:last_name => /\w+/.gen.capitalize,
|
|
6
|
+
:password => (pass = /\w+/.gen.downcase),
|
|
7
|
+
:password_confirmation => pass,
|
|
8
|
+
:salt => (salt = Digest::SHA1.hexdigest("--#{Time.now.to_s}--email--")),
|
|
9
|
+
:crypted_password => Hancock::User.encrypt(pass, salt)
|
|
10
|
+
}}
|
|
11
|
+
|
|
12
|
+
Hancock::User.fix(:internal) {{
|
|
13
|
+
:enabled => true,
|
|
14
|
+
:email => /\w+@\w+.\w{2,3}/.gen.downcase,
|
|
15
|
+
:first_name => /\w+/.gen.capitalize,
|
|
16
|
+
:last_name => /\w+/.gen.capitalize,
|
|
17
|
+
:password => (pass = /\w+/.gen.downcase),
|
|
18
|
+
:password_confirmation => pass,
|
|
19
|
+
:salt => (salt = Digest::SHA1.hexdigest("--#{Time.now.to_s}--email--")),
|
|
20
|
+
:crypted_password => Hancock::User.encrypt(pass, salt),
|
|
21
|
+
:internal => true
|
|
22
|
+
}}
|
|
23
|
+
|
|
24
|
+
Hancock::Consumer.fix(:internal) {{
|
|
25
|
+
:url => %r!http://(\w+).example.org/login!.gen.downcase,
|
|
26
|
+
:label => /(\w+) (\w+)/.gen,
|
|
27
|
+
:internal => true
|
|
28
|
+
}}
|
|
29
|
+
|
|
30
|
+
Hancock::Consumer.fix(:visible_to_all) {{
|
|
31
|
+
:url => %r!http://(\w+).consumerapp.com/login!.gen.downcase,
|
|
32
|
+
:label => /(\w+) (\w+)/.gen,
|
|
33
|
+
:internal => false
|
|
34
|
+
}}
|
|
35
|
+
|
|
36
|
+
Hancock::Consumer.fix(:hidden) {{
|
|
37
|
+
:url => 'http://localhost:9292/login',
|
|
38
|
+
:internal => false
|
|
39
|
+
}}
|
data/spec/matchers.rb
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
module Hancock
|
|
2
|
+
module Matchers
|
|
3
|
+
class LoginForm
|
|
4
|
+
include Webrat::Methods
|
|
5
|
+
include Webrat::Matchers
|
|
6
|
+
def matches?(target)
|
|
7
|
+
target.should have_selector("form[action='/sso/login'][method='POST']")
|
|
8
|
+
target.should have_selector("form[action='/sso/login'][method='POST'] input[type='text'][name='email']")
|
|
9
|
+
target.should have_selector("form[action='/sso/login'][method='POST'] input[type='password'][name='password']")
|
|
10
|
+
target.should have_selector("form[action='/sso/login'][method='POST'] input[type='submit'][value='Login']")
|
|
11
|
+
true
|
|
12
|
+
end
|
|
13
|
+
def failure_message
|
|
14
|
+
puts "Expected a login form to be displayed, it wasn't"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def be_a_login_form
|
|
19
|
+
LoginForm.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class SignupForm
|
|
23
|
+
include Webrat::Methods
|
|
24
|
+
include Webrat::Matchers
|
|
25
|
+
def matches?(target)
|
|
26
|
+
target.should have_selector("form[action='/sso/signup'][method='POST']")
|
|
27
|
+
target.should have_selector("form[action='/sso/signup'][method='POST'] input[type='text'][name='email']")
|
|
28
|
+
target.should have_selector("form[action='/sso/signup'][method='POST'] input[type='text'][name='first_name']")
|
|
29
|
+
target.should have_selector("form[action='/sso/signup'][method='POST'] input[type='text'][name='last_name']")
|
|
30
|
+
target.should have_selector("form[action='/sso/signup'][method='POST'] input[type='submit'][value='Signup']")
|
|
31
|
+
true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def failure_message
|
|
35
|
+
puts "Expected a signup form to be displayed, it wasn't"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
def be_a_signup_form
|
|
39
|
+
SignupForm.new
|
|
40
|
+
end
|
|
41
|
+
class IdentityProviderDocument
|
|
42
|
+
include Webrat::Methods
|
|
43
|
+
include Webrat::Matchers
|
|
44
|
+
include Spec::Matchers
|
|
45
|
+
def matches?(target)
|
|
46
|
+
target.header['Content-Type'].should eql('application/xrds+xml')
|
|
47
|
+
target.body.should have_xpath("//xrd/service[uri='http://example.org/sso']")
|
|
48
|
+
target.body.should have_xpath("//xrd/service[type='http://specs.openid.net/auth/2.0/server']")
|
|
49
|
+
true
|
|
50
|
+
end
|
|
51
|
+
def failure_message
|
|
52
|
+
puts "Expected a identity provider yadis document"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def be_an_identity_provider
|
|
57
|
+
IdentityProviderDocument.new
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class RedirectToConsumer
|
|
61
|
+
include Spec::Matchers
|
|
62
|
+
include Webrat::Methods
|
|
63
|
+
include Webrat::Matchers
|
|
64
|
+
def initialize(consumer, user)
|
|
65
|
+
@consumer, @user = consumer, user
|
|
66
|
+
@identity_url = "http://example.org/sso/users/#{user.id}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def matches?(target)
|
|
70
|
+
target.status.should == 302
|
|
71
|
+
|
|
72
|
+
redirect_params = Addressable::URI.parse(target.headers['Location']).query_values
|
|
73
|
+
|
|
74
|
+
redirect_params['openid.ns'].should == 'http://specs.openid.net/auth/2.0'
|
|
75
|
+
redirect_params['openid.mode'].should == 'id_res'
|
|
76
|
+
redirect_params['openid.return_to'].should == @consumer.url
|
|
77
|
+
redirect_params['openid.assoc_handle'].should =~ /^\{HMAC-SHA1\}\{[^\}]{8}\}\{[^\}]{8}\}$/
|
|
78
|
+
redirect_params['openid.op_endpoint'].should == 'http://example.org/sso'
|
|
79
|
+
redirect_params['openid.claimed_id'].should == @identity_url
|
|
80
|
+
redirect_params['openid.identity'].should == @identity_url
|
|
81
|
+
|
|
82
|
+
redirect_params['openid.sreg.email'].should == @user.email
|
|
83
|
+
redirect_params['openid.sreg.last_name'].should == @user.last_name
|
|
84
|
+
redirect_params['openid.sreg.first_name'].should == @user.first_name
|
|
85
|
+
|
|
86
|
+
redirect_params['openid.sig'].should_not be_nil
|
|
87
|
+
redirect_params['openid.signed'].should_not be_nil
|
|
88
|
+
redirect_params['openid.response_nonce'].should_not be_nil
|
|
89
|
+
true
|
|
90
|
+
end
|
|
91
|
+
def failure_message
|
|
92
|
+
puts "Expected a redirect to the consumer"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def be_a_redirect_to_the_consumer(consumer, user)
|
|
97
|
+
RedirectToConsumer.new(consumer, user)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
class ReturnAnOpenIDAssociateResponse
|
|
101
|
+
include Spec::Matchers
|
|
102
|
+
include Webrat::Methods
|
|
103
|
+
include Webrat::Matchers
|
|
104
|
+
def initialize(session)
|
|
105
|
+
@openid_session = session
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def matches?(target)
|
|
109
|
+
message = OpenID::Message.from_kvform("#{target.body}") # wtf do i have to interpolate this!
|
|
110
|
+
secret = @openid_session.extract_secret(message)
|
|
111
|
+
secret.should_not be_nil
|
|
112
|
+
|
|
113
|
+
args = message.get_args(OpenID::OPENID_NS)
|
|
114
|
+
|
|
115
|
+
args['assoc_type'].should == 'HMAC-SHA1'
|
|
116
|
+
args['assoc_handle'].should =~ /^\{HMAC-SHA1\}\{[^\}]{8}\}\{[^\}]{8}\}$/
|
|
117
|
+
args['session_type'].should == 'DH-SHA1'
|
|
118
|
+
args['enc_mac_key'].size.should == 28
|
|
119
|
+
args['expires_in'].should =~ /^\d+$/
|
|
120
|
+
args['dh_server_public'].size.should == 172
|
|
121
|
+
true
|
|
122
|
+
end
|
|
123
|
+
def failure_message
|
|
124
|
+
puts "Expected an OpenID Associate Response"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
def be_an_openid_associate_response(openid_session)
|
|
128
|
+
ReturnAnOpenIDAssociateResponse.new(openid_session)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
class ReturnAnOpenIDImmediateResponse
|
|
132
|
+
include Spec::Matchers
|
|
133
|
+
include Webrat::Methods
|
|
134
|
+
include Webrat::Matchers
|
|
135
|
+
def initialize(consumer, user)
|
|
136
|
+
@consumer, @user = consumer, user
|
|
137
|
+
@identity_url = "http://example.org/sso/users/#{user.id}"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def matches?(target)
|
|
141
|
+
target.status.should == 302
|
|
142
|
+
|
|
143
|
+
redirect_params = Addressable::URI.parse(target.headers['Location']).query_values
|
|
144
|
+
|
|
145
|
+
redirect_params['openid.ns'].should == 'http://specs.openid.net/auth/2.0'
|
|
146
|
+
redirect_params['openid.mode'].should == 'id_res'
|
|
147
|
+
redirect_params['openid.return_to'].should == @consumer.url
|
|
148
|
+
redirect_params['openid.assoc_handle'].should =~ /^\{HMAC-SHA1\}\{[^\}]{8}\}\{[^\}]{8}\}$/
|
|
149
|
+
redirect_params['openid.op_endpoint'].should == 'http://example.org/sso'
|
|
150
|
+
redirect_params['openid.claimed_id'].should == @identity_url
|
|
151
|
+
redirect_params['openid.identity'].should == @identity_url
|
|
152
|
+
|
|
153
|
+
redirect_params['openid.sreg.email'].should == @user.email
|
|
154
|
+
redirect_params['openid.sreg.last_name'].should == @user.last_name
|
|
155
|
+
redirect_params['openid.sreg.first_name'].should == @user.first_name
|
|
156
|
+
|
|
157
|
+
redirect_params['openid.sig'].should_not be_nil
|
|
158
|
+
redirect_params['openid.signed'].should_not be_nil
|
|
159
|
+
redirect_params['openid.response_nonce'].should_not be_nil
|
|
160
|
+
true
|
|
161
|
+
end
|
|
162
|
+
def failure_message
|
|
163
|
+
puts "Expected an OpenID Associate Response"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
def be_an_openid_immediate_response(consumer, user)
|
|
167
|
+
ReturnAnOpenIDImmediateResponse.new(consumer, user)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'pp'
|
|
3
|
+
gem 'rspec', '~>1.2.0'
|
|
4
|
+
require 'spec'
|
|
5
|
+
require 'sinatra/test'
|
|
6
|
+
require 'dm-sweatshop'
|
|
7
|
+
|
|
8
|
+
$:.push File.join(File.dirname(__FILE__), '..', 'lib')
|
|
9
|
+
require 'hancock'
|
|
10
|
+
gem 'webrat', '~>0.4.2'
|
|
11
|
+
require 'webrat/sinatra'
|
|
12
|
+
|
|
13
|
+
gem 'rack-test', '~>0.1.0'
|
|
14
|
+
require 'rack/test'
|
|
15
|
+
|
|
16
|
+
require File.expand_path(File.dirname(__FILE__) + '/app')
|
|
17
|
+
require File.expand_path(File.dirname(__FILE__) + '/matchers')
|
|
18
|
+
require File.expand_path(File.dirname(__FILE__) + '/fixtures')
|
|
19
|
+
|
|
20
|
+
DataMapper.setup(:default, 'sqlite3::memory:')
|
|
21
|
+
DataMapper.auto_migrate!
|
|
22
|
+
Sinatra::Mailer.delivery_method = :test_send
|
|
23
|
+
|
|
24
|
+
Webrat.configure do |config|
|
|
25
|
+
if ENV['SELENIUM'].nil?
|
|
26
|
+
config.mode = :sinatra
|
|
27
|
+
else
|
|
28
|
+
gem 'selenium-client', '~>1.2.10'
|
|
29
|
+
config.mode = :selenium
|
|
30
|
+
config.application_framework = :sinatra
|
|
31
|
+
config.application_port = 4567
|
|
32
|
+
require 'webrat/selenium'
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
Hancock::App.set :environment, :development
|
|
37
|
+
Hancock::App.set :do_not_reply, 'sso@example.com'
|
|
38
|
+
|
|
39
|
+
Spec::Runner.configure do |config|
|
|
40
|
+
def app
|
|
41
|
+
@app = Rack::Builder.new do
|
|
42
|
+
run Hancock::App
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def login(user)
|
|
47
|
+
post '/sso/login', :email => user.email, :password => user.password
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
config.include(Rack::Test::Methods)
|
|
51
|
+
config.include(Webrat::Methods)
|
|
52
|
+
config.include(Webrat::Matchers)
|
|
53
|
+
config.include(Hancock::Matchers)
|
|
54
|
+
config.after do
|
|
55
|
+
Sinatra::Mailer::Email.deliveries = [ ]
|
|
56
|
+
end
|
|
57
|
+
unless ENV['SELENIUM'].nil?
|
|
58
|
+
config.include(Webrat::Selenium::Methods)
|
|
59
|
+
config.include(Webrat::Selenium::Matchers)
|
|
60
|
+
end
|
|
61
|
+
end
|