├── spec ├── features │ ├── step_definitions │ │ ├── sso_steps.rb │ │ ├── sessions_steps.rb │ │ └── signup_steps.rb │ ├── sso.feature │ ├── support │ │ └── env.rb │ ├── sessions.feature │ └── signup.feature ├── units │ ├── user_spec.rb │ ├── logout_spec.rb │ ├── identity_provider_spec.rb │ ├── consumer_spec.rb │ ├── signup_spec.rb │ ├── landing_page_spec.rb │ ├── login_spec.rb │ └── openid_spec.rb ├── spec_helper.rb ├── fixtures.rb ├── matchers.rb └── acceptance │ └── signing_up_spec.rb ├── .gitignore ├── AUTHORS ├── examples └── dragon │ ├── public │ ├── bodybg.gif │ ├── front.png │ ├── dragonmini.png │ └── andreas05.css │ ├── config.ru │ └── views │ └── layout.haml ├── lib ├── models │ ├── consumer.rb │ └── user.rb ├── hancock.rb └── sinatra │ └── hancock │ ├── defaults.rb │ ├── sessions.rb │ ├── users.rb │ └── openid_server.rb ├── LICENSE ├── Rakefile └── README.md /spec/features/step_definitions/sso_steps.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | coverage 3 | development.db 4 | tmp 5 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | http://github.com/atmos 2 | http://github.com/halorgium 3 | http://github.com/adelcambre 4 | -------------------------------------------------------------------------------- /examples/dragon/public/bodybg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b/hancock/master/examples/dragon/public/bodybg.gif -------------------------------------------------------------------------------- /examples/dragon/public/front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b/hancock/master/examples/dragon/public/front.png -------------------------------------------------------------------------------- /examples/dragon/public/dragonmini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b/hancock/master/examples/dragon/public/dragonmini.png -------------------------------------------------------------------------------- /spec/features/sso.feature: -------------------------------------------------------------------------------- 1 | Feature: Authenticating Against the SSO Provider 2 | In order to authenticate existing users on a consumer via openid 3 | Scenario: OpenID Mode CheckIDSetup 4 | Scenario: OpenID Mode Immediate 5 | Scenario: OpenID Mode Associate 6 | -------------------------------------------------------------------------------- /spec/units/user_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__)+'/../spec_helper') 2 | 3 | describe Hancock::User do 4 | before(:each) do 5 | @user = Hancock::User.gen 6 | end 7 | it "should save successfully" do 8 | @user.save.should be_true 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/features/support/env.rb: -------------------------------------------------------------------------------- 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 17 | -------------------------------------------------------------------------------- /lib/models/consumer.rb: -------------------------------------------------------------------------------- 1 | class Hancock::Consumer 2 | include DataMapper::Resource 3 | 4 | property :id, Serial 5 | property :url, String, :nullable => false, :unique => true, :unique_index => true, :length => 1024 6 | property :label, String, :nullable => true, :default => nil 7 | property :internal, Boolean, :nullable => false, :defalut => false 8 | 9 | def self.allowed?(host) 10 | !first(:url => host).nil? 11 | end 12 | 13 | def self.visible 14 | all(:internal => false).select do |c| 15 | c.label 16 | end 17 | end 18 | 19 | def self.internal 20 | all(:internal => true).select do |c| 21 | c.label 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/units/logout_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__)+'/../spec_helper') 2 | 3 | describe "visiting /sso/logout" do 4 | before(:each) do 5 | @user = Hancock::User.gen 6 | @consumer = Hancock::Consumer.gen(:internal) 7 | end 8 | describe "when authenticated" do 9 | it "should clear the session and redirec to /" do 10 | get '/sso/logout' 11 | last_response.status.should eql(302) 12 | last_response.headers['Location'].should eql('/') 13 | end 14 | end 15 | describe "when unauthenticated" do 16 | it "should redirect to /" do 17 | get '/sso/logout' 18 | last_response.status.should eql(302) 19 | last_response.headers['Location'].should eql('/') 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/features/sessions.feature: -------------------------------------------------------------------------------- 1 | Feature: Logging In to an SSO Account 2 | In order to authenticate existing users 3 | As an existing user 4 | Scenario: logging in as a redirect from a consumer 5 | Given I am not logged in on the sso provider 6 | And a valid consumer and user exists 7 | When I request authentication returning to the consumer app 8 | Then I should see the login form 9 | When I login 10 | Then I should be redirected to the consumer app to start the handshake 11 | Scenario: logging in 12 | Given I am not logged in on the sso provider 13 | And a valid consumer and user exists 14 | When I request authentication 15 | Then I should see the login form 16 | When I login 17 | Then I should be redirected to the sso provider root on login 18 | -------------------------------------------------------------------------------- /spec/features/step_definitions/sessions_steps.rb: -------------------------------------------------------------------------------- 1 | Given /^a valid consumer and user exists$/ do 2 | @consumer = ::Hancock::Consumer.gen(:internal) 3 | @user = ::Hancock::User.gen 4 | end 5 | 6 | Then /^I login$/ do 7 | post "/sso/login", :email => @user.email, 8 | :password => @user.password 9 | end 10 | 11 | Then /^I should be redirected to the consumer app to start the handshake$/ do 12 | redirection = Addressable::URI.parse(last_response.headers['Location']) 13 | 14 | "#{redirection.scheme}://#{redirection.host}#{redirection.path}".should eql(@consumer.url) 15 | redirection.query_values['id'].to_i.should eql(@user.id) 16 | end 17 | 18 | Then /^I should be redirected to the sso provider root on login$/ do 19 | last_response.headers['Location'].should eql('/sso/login') 20 | follow_redirect! 21 | end 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Corey Donohoe , Tim Carey-Smith 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /examples/dragon/config.ru: -------------------------------------------------------------------------------- 1 | # thin start -p PORT -R config.ru 2 | require 'ruby-debug' 3 | gem 'sinatra', '~>0.9.1' 4 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'lib', 'hancock')) 5 | 6 | DataMapper.setup(:default, "sqlite3:///#{Dir.pwd}/development.db") 7 | DataMapper.auto_migrate! 8 | 9 | Hancock::Consumer.create(:url => 'http://localhost:5000/sso/login', :label => 'Local Dev', :internal => false) 10 | Hancock::Consumer.create(:url => 'http://localhost:5001/sso/login', :label => 'Human Resources', :internal => true) 11 | Hancock::Consumer.create(:url => 'http://localhost:5002/sso/login', :label => 'Remote Dev', :internal => false) 12 | Hancock::Consumer.create(:url => 'http://localhost:5003/sso/login', :label => 'Remote Calendaring', :internal => false) 13 | Hancock::Consumer.create(:url => 'http://localhost:5004/sso/login', :label => 'Break Dance Pool', :internal => true) 14 | Hancock::Consumer.create(:url => 'http://localhost:5003/sso/login', :label => 'Library Book Reminder', :internal => false) 15 | 16 | Hancock::App.set :views, 'views' 17 | Hancock::App.set :public, 'public' 18 | Hancock::App.set :environment, :production 19 | run Hancock::App 20 | -------------------------------------------------------------------------------- /lib/hancock.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | gem 'dm-core', '~>0.9.10' 4 | require 'dm-core' 5 | require 'dm-validations' 6 | require 'dm-timestamps' 7 | 8 | gem 'ruby-openid', '~>2.1.2' 9 | require 'openid' 10 | require 'openid/store/filesystem' 11 | require 'openid/extensions/sreg' 12 | 13 | gem 'sinatra', '~>0.9.1.1' 14 | require 'sinatra/base' 15 | 16 | gem 'guid', '~>0.1.1' 17 | require 'guid' 18 | 19 | module Hancock; end 20 | 21 | require File.expand_path(File.dirname(__FILE__)+'/models/user') 22 | require File.expand_path(File.dirname(__FILE__)+'/models/consumer') 23 | require File.expand_path(File.dirname(__FILE__)+'/sinatra/hancock/defaults') 24 | require File.expand_path(File.dirname(__FILE__)+'/sinatra/hancock/sessions') 25 | require File.expand_path(File.dirname(__FILE__)+'/sinatra/hancock/users') 26 | require File.expand_path(File.dirname(__FILE__)+'/sinatra/hancock/openid_server') 27 | 28 | module Hancock 29 | class App < Sinatra::Default 30 | enable :sessions 31 | 32 | register Sinatra::Hancock::Defaults 33 | register Sinatra::Hancock::Sessions 34 | register Sinatra::Hancock::Users 35 | register Sinatra::Hancock::OpenIDServer 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/units/identity_provider_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__)+'/../spec_helper') 2 | 3 | describe "Requesting the server's xrds" do 4 | describe "when accepting xrds+xml" do 5 | it "renders the provider idp page" do 6 | get '/sso/xrds' 7 | last_response.headers['Content-Type'].should eql('application/xrds+xml') 8 | last_response.should have_xpath("//xrd/service[uri='http://example.org/sso']") 9 | last_response.should have_xpath("//xrd/service[type='http://specs.openid.net/auth/2.0/server']") 10 | end 11 | end 12 | end 13 | 14 | describe "Requesting a user's xrds" do 15 | before(:each) do 16 | @user = Hancock::User.gen 17 | end 18 | 19 | it "renders the users idp page" do 20 | get "/sso/users/#{@user.id}" 21 | 22 | last_response.headers['Content-Type'].should eql('application/xrds+xml') 23 | last_response.headers['X-XRDS-Location'].should eql("http://example.org/sso/users/#{@user.id}") 24 | last_response.body.should have_xpath("//xrd/service[uri='http://example.org/sso']") 25 | last_response.body.should have_xpath("//xrd/service[type='http://specs.openid.net/auth/2.0/signon']") 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/features/signup.feature: -------------------------------------------------------------------------------- 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 27 | -------------------------------------------------------------------------------- /spec/units/consumer_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__)+'/../spec_helper') 2 | 3 | describe Hancock::Consumer do 4 | describe "when queried about a disallowed host" do 5 | it "returns false" do 6 | Hancock::Consumer.allowed?('http://blogspot.com').should be_false 7 | end 8 | end 9 | 10 | describe "visible to staff" do 11 | before(:each) do 12 | @consumer = Hancock::Consumer.gen(:internal) 13 | @consumer.save 14 | end 15 | describe "when queried about an allowed host" do 16 | it "returns true" do 17 | Hancock::Consumer.allowed?(@consumer.url).should be_true 18 | end 19 | end 20 | end 21 | describe "visible to customers and staff" do 22 | before(:each) do 23 | @consumer = Hancock::Consumer.gen(:visible_to_all) 24 | @consumer.save 25 | end 26 | describe "when queried about an allowed host" do 27 | it "returns true" do 28 | Hancock::Consumer.allowed?(@consumer.url).should be_true 29 | end 30 | end 31 | end 32 | describe "hidden (API) apps" do 33 | before(:each) do 34 | @consumer = Hancock::Consumer.gen(:hidden) 35 | @consumer.save 36 | end 37 | describe "when queried about an allowed host" do 38 | it "returns true" do 39 | Hancock::Consumer.allowed?(@consumer.url).should be_true 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/sinatra/hancock/defaults.rb: -------------------------------------------------------------------------------- 1 | module Sinatra 2 | module Hancock 3 | module Defaults 4 | module Helpers 5 | def forbidden! 6 | throw :halt, [403, 'Forbidden'] 7 | end 8 | 9 | def absolute_url(suffix = nil) 10 | port_part = case request.scheme 11 | when "http" 12 | request.port == 80 ? "" : ":#{request.port}" 13 | when "https" 14 | request.port == 443 ? "" : ":#{request.port}" 15 | end 16 | "#{request.scheme}://#{request.host}#{port_part}#{suffix}" 17 | end 18 | def landing_page 19 | <<-HAML 20 | %h3 Hello #{session_user.first_name} #{session_user.last_name}! 21 | - unless @consumers.empty? 22 | %ul#consumers 23 | - @consumers.each do |consumer| 24 | %li 25 | %a{:href => consumer.url}= consumer.label 26 | HAML 27 | end 28 | end 29 | 30 | def self.registered(app) 31 | app.send(:include, Sinatra::Hancock::Defaults::Helpers) 32 | app.set :sessions, true 33 | app.get '/' do 34 | ensure_authenticated 35 | @consumers = ::Hancock::Consumer.visible 36 | @consumers += ::Hancock::Consumer.internal if session_user.internal? 37 | haml landing_page 38 | end 39 | end 40 | end 41 | end 42 | register Hancock::Defaults 43 | end 44 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'pp' 3 | gem 'selenium-client', '~>1.2.10' 4 | gem 'rspec', '~>1.1.12' 5 | require 'spec' 6 | require 'sinatra/test' 7 | require 'dm-sweatshop' 8 | 9 | $:.push File.join(File.dirname(__FILE__), '..', 'lib') 10 | require 'hancock' 11 | gem 'webrat', '~>0.4.2' 12 | require 'webrat/sinatra' 13 | 14 | gem 'rack-test', '~>0.1.0' 15 | require 'rack/test' 16 | 17 | require File.dirname(__FILE__)+'/matchers' 18 | 19 | require File.expand_path(File.dirname(__FILE__) + '/fixtures') 20 | DataMapper.setup(:default, 'sqlite3::memory:') 21 | DataMapper.auto_migrate! 22 | 23 | Webrat.configure do |config| 24 | if ENV['SELENIUM'].nil? 25 | config.mode = :sinatra 26 | else 27 | config.mode = :selenium 28 | config.application_framework = :sinatra 29 | config.application_port = 4567 30 | require 'webrat/selenium' 31 | end 32 | end 33 | 34 | Hancock::App.set :environment, :development 35 | 36 | Spec::Runner.configure do |config| 37 | def app 38 | @app = Rack::Builder.new do 39 | run Hancock::App 40 | end 41 | end 42 | 43 | def login(user) 44 | post '/sso/login', :email => user.email, :password => user.password 45 | end 46 | 47 | config.include(Rack::Test::Methods) 48 | config.include(Webrat::Methods) 49 | config.include(Webrat::Matchers) 50 | config.include(Hancock::Matchers) 51 | 52 | unless ENV['SELENIUM'].nil? 53 | config.include(Webrat::Selenium::Methods) 54 | config.include(Webrat::Selenium::Matchers) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/fixtures.rb: -------------------------------------------------------------------------------- 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 | }} 40 | -------------------------------------------------------------------------------- /spec/units/signup_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__)+'/../spec_helper') 2 | 3 | describe "posting to /sso/signup" do 4 | before(:each) do 5 | @existing_user = Hancock::User.gen 6 | @user = Hancock::User.new(:email => /\w+@\w+\.\w{2,3}/.gen.downcase, 7 | :first_name => /\w+/.gen.capitalize, 8 | :last_name => /\w+/.gen.capitalize) 9 | @consumer = Hancock::Consumer.gen(:internal) 10 | end 11 | describe "with valid information" do 12 | it "should sign the user up" do 13 | post '/sso/signup', :email => @user.email, 14 | :first_name => @user.first_name, 15 | :last_name => @user.last_name 16 | 17 | last_response.body.to_s.should have_selector("h3:contains('Success')") 18 | last_response.body.to_s.should have_selector('p:contains("Check your email and you\'ll see a registration link!")') 19 | last_response.body.to_s.should match(%r!href='http://example.org/sso/register/\w{40}'!) 20 | end 21 | end 22 | describe "with invalid information" do 23 | it "should not sign the user up" do 24 | post '/sso/signup', :email => @existing_user.email, 25 | :first_name => @existing_user.first_name, 26 | :last_name => @existing_user.last_name 27 | last_response.should have_selector("h3:contains('Signup Failed')") 28 | last_response.should have_selector("p a[href='/sso/signup']:contains('Try Again?')") 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/units/landing_page_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__)+'/../spec_helper') 2 | 3 | describe "visiting /" do 4 | before(:each) do 5 | @last = Hancock::Consumer.gen(:internal) 6 | @first = Hancock::Consumer.gen(:visible_to_all) 7 | end 8 | describe "when authenticated" do 9 | describe "as an internal user" do 10 | before(:each) do 11 | @user = Hancock::User.gen(:internal) 12 | end 13 | it "should greet the user" do 14 | login(@user) 15 | get '/' 16 | 17 | last_response.body.to_s.should have_selector("h3:contains('Hello #{@user.first_name} #{@user.last_name}')") 18 | last_response.body.to_s.should have_selector("ul#consumers li a[href='#{@first.url}']:contains('#{@first.label}')") 19 | last_response.body.to_s.should have_selector("ul#consumers li a[href='#{@last.url}']:contains('#{@last.label}')") 20 | end 21 | end 22 | describe "as an external user" do 23 | before(:each) do 24 | @user = Hancock::User.gen 25 | end 26 | it "should greet the user" do 27 | login(@user) 28 | get '/' 29 | 30 | last_response.body.to_s.should have_selector("h3:contains('Hello #{@user.first_name} #{@user.last_name}')") 31 | last_response.body.to_s.should have_selector("ul#consumers li a[href='#{@first.url}']:contains('#{@first.label}')") 32 | last_response.body.to_s.should_not have_selector("ul#consumers li a[href='#{@last.url}']:contains('#{@last.label}')") 33 | end 34 | end 35 | end 36 | describe "when unauthenticated" do 37 | it "should prompt the user to login" do 38 | get '/' 39 | last_response.should be_a_login_form 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/matchers.rb: -------------------------------------------------------------------------------- 1 | module Hancock 2 | module Matchers 3 | 4 | class LoginForm 5 | include Webrat::Methods 6 | include Webrat::Matchers 7 | def matches?(target) 8 | target.should have_selector("form[action='/sso/login'][method='POST']") 9 | target.should have_selector("form[action='/sso/login'][method='POST'] input[type='text'][name='email']") 10 | target.should have_selector("form[action='/sso/login'][method='POST'] input[type='password'][name='password']") 11 | target.should have_selector("form[action='/sso/login'][method='POST'] input[type='submit'][value='Login']") 12 | true 13 | end 14 | 15 | def failure_message 16 | puts "Expected a login form to be displayed, it wasn't" 17 | end 18 | end 19 | 20 | def be_a_login_form 21 | LoginForm.new 22 | end 23 | 24 | class SignupForm 25 | include Webrat::Methods 26 | include Webrat::Matchers 27 | def matches?(target) 28 | target.should have_selector("form[action='/sso/signup'][method='POST']") 29 | target.should have_selector("form[action='/sso/signup'][method='POST'] input[type='text'][name='email']") 30 | target.should have_selector("form[action='/sso/signup'][method='POST'] input[type='text'][name='first_name']") 31 | target.should have_selector("form[action='/sso/signup'][method='POST'] input[type='text'][name='last_name']") 32 | target.should have_selector("form[action='/sso/signup'][method='POST'] input[type='submit'][value='Signup']") 33 | true 34 | end 35 | 36 | def failure_message 37 | puts "Expected a signup form to be displayed, it wasn't" 38 | end 39 | end 40 | 41 | def be_a_signup_form 42 | SignupForm.new 43 | end 44 | 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /examples/dragon/views/layout.haml: -------------------------------------------------------------------------------- 1 | %html{:xmlns=> "http://www.w3.org/1999/xhtml", 'xml:lang' => "en", :lang => "en"} 2 | %head 3 | %meta{'http-equiv' => "Content-Type", 'content' => "text/html; charset=utf-8"} 4 | %meta{'name' => "description", 'content' => "Hancock Single Sign On Server"} 5 | %meta{'name' => "author", 'content' => "Corey Donohoe / Original design: Andreas Viklund - http://andreasviklund.com/"} 6 | %link{'rel' => 'stylesheet', 'type' => 'text/css', 'href' => '/andreas05.css'} 7 | %title Step Right Up 8 | %body 9 | #title 10 | %h1 Hancock: Single Sign On 11 | #container 12 | #sidebar 13 | - if session['user_id'] 14 | %h2 Applications 15 | - ::Hancock::Consumer.visible.each do |consumer| 16 | %a{:class => 'menu', :href => consumer.url}= consumer.label 17 | 18 | %h2 Interact 19 | - if session['user_id'] 20 | %a{:class => 'menu', :href => '/sso/logout'} Logout 21 | - else 22 | %a{:class => 'menu', :href => '/sso/login'} Login 23 | %a{:class => 'menu', :href => 'http://github.com/atmos/hancock'} Contribute 24 | %a{:class => 'menu', :href => 'http://wiki.github.com/atmos/hancock'} Documentation 25 | 26 | %h2 Thank You 27 | %a{:class => 'menu', :href => 'http://engineyard.com'} Engine Yard 28 | %a{:class => 'menu', :href => 'http://www.sinatrarb.com'} Sinatra 29 | %a{:class => 'menu', :href => 'http://www.datamapper.org'} DataMapper 30 | %a{:class => 'menu', :href => 'http://andreasviklund.com'} Andreas Viklund 31 | #main 32 | %p= yield 33 | - if session['user_id'] 34 | %h2 35 | %a{:href => 'mailto:#{session[:email]}'}= "#{session[:first_name]} #{session[:last_name]}" 36 | 37 | #footer 38 | 39 | -------------------------------------------------------------------------------- /spec/features/step_definitions/signup_steps.rb: -------------------------------------------------------------------------------- 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 57 | -------------------------------------------------------------------------------- /lib/sinatra/hancock/sessions.rb: -------------------------------------------------------------------------------- 1 | module Sinatra 2 | module Hancock 3 | module Sessions 4 | module Helpers 5 | def session_user 6 | session['user_id'].nil? ? nil : ::Hancock::User.get(session['user_id']) 7 | end 8 | 9 | def ensure_authenticated 10 | login_view = <<-HAML 11 | %fieldset 12 | %legend You need to log in, buddy. 13 | %form{:action => '/sso/login', :method => 'POST'} 14 | %label{:for => 'email'} 15 | Email: 16 | %input{:type => 'text', :name => 'email'} 17 | %br 18 | %label{:for => 'password'} 19 | Password: 20 | %input{:type => 'password', :name => 'password'} 21 | %br 22 | %input{:type => 'submit', :value => 'Login'} 23 | or 24 | %a{:href => '/sso/signup'} Signup 25 | HAML 26 | if trust_root = session['return_to'] || params['return_to'] 27 | if ::Hancock::Consumer.allowed?(trust_root) 28 | if session_user 29 | redirect "#{trust_root}?id=#{session_user.id}" 30 | else 31 | session['return_to'] = trust_root 32 | end 33 | else 34 | throw(:halt, [403, 'Forbidden']) 35 | end 36 | end 37 | throw(:halt, [401, haml(login_view)]) unless session_user 38 | end 39 | end 40 | 41 | def self.registered(app) 42 | app.send(:include, Sinatra::Hancock::Sessions::Helpers) 43 | app.get '/sso/login' do 44 | ensure_authenticated 45 | end 46 | app.post '/sso/login' do 47 | @user = ::Hancock::User.authenticate(params['email'], params['password']) 48 | if @user 49 | session['user_id'] = @user.id 50 | end 51 | ensure_authenticated 52 | redirect '/sso/login' 53 | end 54 | 55 | app.get '/sso/logout' do 56 | session.clear 57 | redirect '/' 58 | end 59 | end 60 | end 61 | end 62 | register Hancock::Sessions 63 | end 64 | -------------------------------------------------------------------------------- /lib/models/user.rb: -------------------------------------------------------------------------------- 1 | class Hancock::User 2 | include DataMapper::Resource 3 | 4 | property :id, Serial 5 | property :first_name, String 6 | property :last_name, String 7 | property :email, String, :unique => true, :unique_index => true 8 | property :internal, Boolean, :default => false 9 | 10 | property :salt, String 11 | property :crypted_password, String 12 | 13 | property :enabled, Boolean, :default => false 14 | property :access_token, String 15 | 16 | attr_accessor :password, :password_confirmation 17 | 18 | def reset_access_token 19 | @access_token = Digest::SHA1.hexdigest(Guid.new.to_s) 20 | end 21 | 22 | def authenticated?(password) 23 | crypted_password == encrypt(password) 24 | end 25 | 26 | def encrypt(password) 27 | self.class.encrypt(password, salt) 28 | end 29 | 30 | def password_required? 31 | crypted_password.blank? || !password.blank? 32 | end 33 | 34 | def encrypt_password 35 | return if password.blank? 36 | @salt = Digest::SHA1.hexdigest("--#{Guid.new.to_s}}--email--") if new_record? 37 | @crypted_password = encrypt(password) 38 | end 39 | 40 | validates_present :password, :if => proc{|m| m.password_required?} 41 | validates_is_confirmed :password, :if => proc{|m| m.password_required?} 42 | 43 | before :save, :encrypt_password 44 | before :save, :reset_access_token 45 | 46 | def self.encrypt(password, salt) 47 | Digest::SHA1.hexdigest("--#{salt}--#{password}--") 48 | end 49 | 50 | def self.signup(params) 51 | seed = Guid.new.to_s 52 | new(:email => params['email'], 53 | :first_name => params['first_name'], 54 | :last_name => params['last_name'], 55 | :password => Digest::SHA1.hexdigest(seed), 56 | :password_confirmation => Digest::SHA1.hexdigest(seed)) 57 | end 58 | 59 | def self.authenticate(email, password) 60 | u = first(:email => email) 61 | u && u.authenticated?(password) && u.enabled ? u : nil 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/units/login_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__)+'/../spec_helper') 2 | 3 | describe "posting to /sso/login" do 4 | before(:each) do 5 | @user = Hancock::User.gen 6 | @consumer = Hancock::Consumer.gen(:internal) 7 | end 8 | describe "with a valid password" do 9 | it "should authenticate a user and redirect to /" do 10 | post '/sso/login', :email => @user.email, :password => @user.password 11 | last_response.status.should eql(302) 12 | last_response.headers['Location'].should eql('/sso/login') 13 | end 14 | end 15 | describe "with an invalid password" do 16 | it "should display a form to login" do 17 | post '/sso/login', :email => @user.email, :password => 's3cr3t' 18 | last_response.body.should be_a_login_form 19 | end 20 | end 21 | end 22 | describe "getting /sso/login" do 23 | before(:each) do 24 | @user = Hancock::User.gen 25 | @consumer = Hancock::Consumer.gen(:internal) 26 | end 27 | describe "with a valid session" do 28 | it "should redirect to the consumer w/ the id for openid discovery" do 29 | get '/sso/login', :return_to => @consumer.url 30 | 31 | post '/sso/login', :email => @user.email, :password => @user.password 32 | follow_redirect! 33 | 34 | get '/sso/login' 35 | last_response.status.should eql(302) 36 | 37 | uri = Addressable::URI.parse(last_response.headers['Location']) 38 | @consumer.url.should eql("#{uri.scheme}://#{uri.host}#{uri.path}") 39 | 40 | uri.query_values['id'].should eql("#{@user.id}") 41 | end 42 | 43 | describe "from an invalid consumer" do 44 | it "should return forbidden" do 45 | get '/sso/login', { 'return_to' => 'http://rogueconsumerapp.com/login' } 46 | 47 | login(@user) 48 | 49 | get '/sso/login', { 'return_to' => 'http://rogueconsumerapp.com/login' } 50 | 51 | last_response.status.should eql(403) 52 | end 53 | end 54 | end 55 | describe "without a valid session" do 56 | it "should prompt the user to login" do 57 | get '/sso/login', { 'return_to' => @consumer.url } 58 | last_response.body.should be_a_login_form 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake/gempackagetask' 3 | require 'rubygems/specification' 4 | require 'date' 5 | require 'spec/rake/spectask' 6 | require 'cucumber/rake/task' 7 | 8 | GEM = "hancock" 9 | GEM_VERSION = "0.0.1" 10 | AUTHOR = ["Corey Donohoe", "Tim Carey-Smith"] 11 | EMAIL = [ "atmos@atmos.org", "tim@spork.in" ] 12 | HOMEPAGE = "http://github.com/atmos/hancock" 13 | SUMMARY = "A gem that provides a Single Sign On server" 14 | 15 | spec = Gem::Specification.new do |s| 16 | s.name = GEM 17 | s.version = GEM_VERSION 18 | s.platform = Gem::Platform::RUBY 19 | s.has_rdoc = true 20 | s.extra_rdoc_files = ["README.md", "LICENSE"] 21 | s.summary = SUMMARY 22 | s.description = s.summary 23 | s.authors = AUTHOR 24 | s.email = EMAIL 25 | s.homepage = HOMEPAGE 26 | 27 | # Uncomment this to add a dependency 28 | s.add_dependency "dm-core", "~>0.9.10" 29 | s.add_dependency "ruby-openid", "~>2.1.2" 30 | s.add_dependency "sinatra", "~>0.9.1.1" 31 | s.add_dependency "guid", "~>0.1.1" 32 | 33 | s.require_path = 'lib' 34 | s.autorequire = GEM 35 | s.files = %w(LICENSE README.md Rakefile) + Dir.glob("{lib,spec}/**/*") 36 | end 37 | 38 | task :default => [:spec, :features] 39 | 40 | desc "Run specs" 41 | Spec::Rake::SpecTask.new do |t| 42 | t.spec_files = FileList['spec/**/*_spec.rb'] 43 | t.spec_opts = %w(-fp --color) 44 | 45 | t.rcov = true 46 | t.rcov_opts << '--text-summary' 47 | t.rcov_opts << '--sort' << 'coverage' << '--sort-reverse' 48 | t.rcov_opts << '--exclude' << '.gem/,spec,examples' 49 | #t.rcov_opts << '--only-uncovered' 50 | end 51 | 52 | Rake::GemPackageTask.new(spec) do |pkg| 53 | pkg.gem_spec = spec 54 | end 55 | 56 | desc "create a gemspec file" 57 | task :make_spec do 58 | File.open("#{GEM}.gemspec", "w") do |file| 59 | file.puts spec.to_ruby 60 | end 61 | end 62 | 63 | Cucumber::Rake::Task.new(:features) do |t| 64 | t.libs << 'lib' 65 | t.cucumber_opts = "--format pretty" 66 | t.step_list = 'spec/features/**/*.rb' 67 | t.feature_list = 'spec/features/**/*.feature' 68 | t.rcov = true 69 | t.rcov_opts << '--text-summary' 70 | t.rcov_opts << '--sort' << 'coverage' << '--sort-reverse' 71 | t.rcov_opts << '--exclude' << '.gem/,spec,examples' 72 | end 73 | -------------------------------------------------------------------------------- /examples/dragon/public/andreas05.css: -------------------------------------------------------------------------------- 1 | /* andreas05 - an open source xhtml/css website layout by Andreas Viklund (http://andreasviklund.com). Made for OSWD.org, free to use as-is for any purpose as long as the proper credits are given for the original design work. More free templates are available at: http://oswd.org/userinfo.phtml?user=Andreas 2 | Version: 1.0, September 27, 2005 */ 3 | 4 | body{ 5 | padding:0; 6 | margin:0; 7 | font:76% verdana,tahoma,sans-serif; 8 | background:#cccccc url(/bodybg.gif) repeat; 9 | color:#303030; 10 | } 11 | 12 | a{ 13 | text-decoration:none; 14 | background-color:inherit; 15 | font-weight:bold; 16 | color:#286ea0; 17 | } 18 | 19 | a:hover{ 20 | background-color:inherit; 21 | color:#303030; 22 | } 23 | 24 | h1{ 25 | margin:0; 26 | font-size:3.6em; 27 | letter-spacing:-2px; 28 | text-align:right; 29 | background-color:inherit; 30 | color:#505050; 31 | } 32 | 33 | h2{ 34 | margin:5px 0 10px 0; 35 | font-size:1.6em; 36 | letter-spacing:-1px; 37 | font-weight:normal; 38 | } 39 | 40 | p{ 41 | margin:0 0 15px 0; 42 | line-height:1.3em; 43 | } 44 | 45 | img{ 46 | float:left; 47 | margin:0 10px 8px 0; 48 | } 49 | 50 | #title{ 51 | margin:20px auto -9px auto; 52 | width:700px; 53 | } 54 | 55 | #container{ 56 | min-height: 340px; 57 | margin:0 auto 15px auto; 58 | width:700px; 59 | padding:10px; 60 | background:#ffffff url(/front.png) bottom left no-repeat; 61 | color:#303030; 62 | border:20px solid #505050; 63 | } 64 | 65 | #sidebar{ 66 | float:left; 67 | width:110px; 68 | padding-left:175px; 69 | } 70 | 71 | #main{ 72 | width:385px; 73 | float:right; 74 | } 75 | 76 | #footer{ 77 | clear:both; 78 | } 79 | 80 | .menu{ 81 | display:block; 82 | width:110px; 83 | padding:4px 2px 4px 10px; 84 | font-size:1.1em; 85 | font-weight:bold; 86 | background-color:inherit; 87 | color:#286ea0; 88 | border:1px solid #ffffff; 89 | } 90 | 91 | .menu:hover{ 92 | background-color:#f8f8f8; 93 | color:#286ea0; 94 | border:1px solid #dadada; 95 | } 96 | 97 | .credits{ 98 | margin-bottom:0; 99 | font-size:0.8em; 100 | background-color:inherit; 101 | color:#aaaaaa; 102 | } 103 | 104 | .credits a{ 105 | background-color:inherit; 106 | color:#aaaaaa; 107 | } 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | hancock 2 | ======= 3 | 4 | It's like your [John Hancock][johnhancock] for all of your company's apps. 5 | 6 | A lot of this is extracted from our internal single sign on server at [Engine Yard][ey]. We 7 | use a different [datamapper][datamapper] backend but it should be a decent 8 | start for most people. 9 | 10 | Features 11 | ======== 12 | An [OpenID][openid] based [Single Sign On][sso] server that provides: 13 | 14 | * a [whitelist][whitelist] for consumers 15 | * integration with the big ruby frameworks(rails,merb,[sinatra][sinatra_examples]) 16 | * [sreg][sreg] parameters to consumers(first name, last name, email, identity_url) 17 | 18 | 19 | How it Works 20 | ============ 21 | ![SSO Handshake](http://img.skitch.com/20090305-be6wwmbc4gfsi9euy3w7np31mm.jpg) 22 | 23 | This handshake seems kind of complex but it only happens when you need to 24 | validate a user session on the consumer. 25 | 26 | Installation 27 | ============ 28 | % gem sources 29 | *** CURRENT SOURCES *** 30 | 31 | http://gems.rubyforge.org/ 32 | http://gems.engineyard.com 33 | http://gems.github.com 34 | 35 | You need a few gems to function 36 | 37 | % sudo gem install dm-core do_sqlite3 38 | % sudo gem install sinatra guid rspec ruby-openid 39 | 40 | You need a few more to test, including [sr][sr]'s [fork][srfork] of [webrat][webrat] 41 | % sudo gem install selenium-client rspec 42 | % git clone git://github.com/sr/webrat.git 43 | % cd webrat 44 | % git checkout -b sinatra origin/sinatra 45 | % rake repackage 46 | % sudo gem uninstall -aI webrat 47 | % sudo gem install pkg/webrat-0.4.2.gem 48 | 49 | Plans 50 | ===== 51 | * configurable sreg parameters to consumers 52 | * signup with email based validation 53 | * single sign off 54 | * some kinda awesome [oauth][oauth] hooks 55 | * [simpledb][simpledb] integration, srsly 56 | 57 | Sponsored By 58 | ============ 59 | * [Engine Yard][ey] 60 | 61 | [johnhancock]: http://www.urbandictionary.com/define.php?term=john+hancock 62 | [ey]: http://www.engineyard.com/ 63 | [sr]: http://github.com/sr 64 | [atmos]: http://github.com/atmos 65 | [halorgium]: http://github.com/halorgium 66 | [adelcambre]: http://github.com/adelcambre 67 | [srfork]: http://github.com/sr/webrat/tree/sinatra 68 | [webrat]: http://github.com/brynary/webrat 69 | [sinatra_examples]: http://github.com/atmos/hancock/blob/e51f7ef2f0aae5cd5e3f816399c8212c00585abc/examples/dragon/config.ru 70 | [datamapper]: http://datamapper.org 71 | [openid]: http://openid.net/ 72 | [sso]: http://en.wikipedia.org/wiki/Single_sign-on 73 | [whitelist]: http://en.wikipedia.org/wiki/Whitelist 74 | [oauth]: http://oauth.net/ 75 | [sreg]: http://openid.net/specs/openid-simple-registration-extension-1_0.html#response_format 76 | [simpledb]: http://aws.amazon.com/simpledb/ 77 | -------------------------------------------------------------------------------- /lib/sinatra/hancock/users.rb: -------------------------------------------------------------------------------- 1 | module Sinatra 2 | module Hancock 3 | module Users 4 | module Helpers 5 | def register_form 6 | <<-HAML 7 | %fieldset 8 | %legend Enter your new password 9 | %form{:action => '/sso/register/#{params['token']}', :method => 'POST'} 10 | %label{:for => 'password'} 11 | Password: 12 | %input{:type => 'password', :name => 'password'} 13 | %br 14 | %label{:for => 'password_confirmation'} 15 | Password(Again): 16 | %input{:type => 'password', :name => 'password_confirmation'} 17 | %br 18 | %input{:type => 'submit', :value => 'Am I Done Yet?'} 19 | HAML 20 | end 21 | def signup_form 22 | <<-HAML 23 | %fieldset 24 | %legend Signup for an account 25 | %form{:action => '/sso/signup', :method => 'POST'} 26 | %label{:for => 'first_name'} 27 | First Name: 28 | %input{:type => 'text', :name => 'first_name'} 29 | %br 30 | %label{:for => 'last_name'} 31 | Last Name: 32 | %input{:type => 'text', :name => 'last_name'} 33 | %br 34 | %label{:for => 'email'} 35 | Email: 36 | %input{:type => 'text', :name => 'email'} 37 | %br 38 | %input{:type => 'submit', :value => 'Signup'} 39 | or 40 | %a{:href => '/'} Cancel 41 | HAML 42 | end 43 | def signup_confirmation(user) 44 | if user.save 45 | <<-HAML 46 | %h3 Success! 47 | %p Check your email and you'll see a registration link! 48 | - if Hancock::App.environment == :development 49 | / 50 | %a{:href => absolute_url("/sso/register/#{user.access_token}")} Clicky Clicky 51 | HAML 52 | else 53 | <<-HAML 54 | %h3 Signup Failed 55 | #errors 56 | %p= #{user.errors.inspect} 57 | %p 58 | %a{:href => '/sso/signup'} Try Again? 59 | HAML 60 | end 61 | end 62 | 63 | def user_by_token(token) 64 | user = ::Hancock::User.first(:access_token => token) 65 | throw(:halt, [400, 'BadRequest']) unless user 66 | session['user_id'] = user.id 67 | user 68 | end 69 | end 70 | 71 | def self.registered(app) 72 | app.helpers Helpers 73 | 74 | app.get '/sso/register/:token' do 75 | user_by_token(params['token']) 76 | haml register_form 77 | end 78 | 79 | app.post '/sso/register/:token' do 80 | user = user_by_token(params['token']) 81 | user.update_attributes(:enabled => true, 82 | :access_token => nil, 83 | :password => params['password'], 84 | :password_confirmation => params['password_confirmation']) 85 | destination = session.delete('return_to') || '/' 86 | session.reject! { |key,value| key != 'user_id' } 87 | redirect destination 88 | end 89 | 90 | app.get '/sso/signup' do 91 | haml signup_form 92 | end 93 | 94 | app.post '/sso/signup' do 95 | user = ::Hancock::User.signup(params) 96 | haml signup_confirmation(user) 97 | end 98 | end 99 | end 100 | end 101 | register Hancock::Users 102 | end 103 | -------------------------------------------------------------------------------- /spec/acceptance/signing_up_spec.rb: -------------------------------------------------------------------------------- 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://moi.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 | # sent back to be greeted on the consumer 82 | @browser.html.should match(%r!Hancock Client: Sinatra!) 83 | @browser.html.should have_selector("h2 a[href='mailto:#{@user.email}']:contains('#{@user.first_name} #{@user.last_name}')") 84 | end 85 | end 86 | rescue; end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/sinatra/hancock/openid_server.rb: -------------------------------------------------------------------------------- 1 | module Sinatra 2 | module Hancock 3 | module OpenIDServer 4 | module Helpers 5 | def server 6 | if @server.nil? 7 | server_url = absolute_url('/sso') 8 | dir = File.join(Dir.tmpdir, 'openid-store') 9 | store = OpenID::Store::Filesystem.new(dir) 10 | @server = OpenID::Server::Server.new(store, server_url) 11 | end 12 | return @server 13 | end 14 | 15 | def yadis 16 | <<-ERB 17 | 18 | 21 | 22 | 23 | <% @types.each do |typ| %> 24 | <%= typ %> 25 | <% end %> 26 | <%= absolute_url('/sso') %> 27 | 28 | 29 | 30 | ERB 31 | end 32 | 33 | def url_for_user 34 | absolute_url("/sso/users/#{session_user.id}") 35 | end 36 | 37 | def render_response(oidresp) 38 | if oidresp.needs_signing 39 | signed_response = server.signatory.sign(oidresp) 40 | end 41 | web_response = server.encode_response(oidresp) 42 | 43 | case web_response.code 44 | when 302 45 | session.delete(:return_to) 46 | redirect web_response.headers['location'] 47 | else 48 | web_response.body 49 | end 50 | end 51 | end 52 | 53 | def self.registered(app) 54 | app.send(:include, Sinatra::Hancock::OpenIDServer::Helpers) 55 | 56 | app.get '/sso/xrds' do 57 | response.headers['Content-Type'] = 'application/xrds+xml' 58 | @types = [ OpenID::OPENID_IDP_2_0_TYPE ] 59 | erb yadis, :layout => false 60 | end 61 | 62 | app.get '/sso/users/:id' do 63 | @types = [ OpenID::OPENID_2_0_TYPE, OpenID::SREG_URI ] 64 | response.headers['Content-Type'] = 'application/xrds+xml' 65 | response.headers['X-XRDS-Location'] = absolute_url("/sso/users/#{params['id']}") 66 | 67 | erb yadis, :layout => false 68 | end 69 | 70 | [:get, :post].each do |meth| 71 | app.send(meth, '/sso') do 72 | begin 73 | oidreq = server.decode_request(params) 74 | rescue OpenID::Server::ProtocolError => e 75 | oidreq = session[:last_oidreq] 76 | end 77 | throw(:halt, [400, 'Bad Request']) unless oidreq 78 | 79 | oidresp = nil 80 | if oidreq.kind_of?(OpenID::Server::CheckIDRequest) 81 | session[:last_oidreq] = oidreq 82 | session[:return_to] = absolute_url('/sso') 83 | 84 | ensure_authenticated 85 | unless oidreq.identity == url_for_user 86 | forbidden! 87 | end 88 | forbidden! unless ::Hancock::Consumer.allowed?(oidreq.trust_root) 89 | 90 | oidresp = oidreq.answer(true, nil, oidreq.identity) 91 | sreg_data = { 92 | 'last_name' => session_user.last_name, 93 | 'first_name' => session_user.first_name, 94 | 'email' => session_user.email 95 | } 96 | sregresp = OpenID::SReg::Response.new(sreg_data) 97 | oidresp.add_extension(sregresp) 98 | else 99 | oidresp = server.handle_request(oidreq) #associate and more? 100 | end 101 | render_response(oidresp) 102 | end 103 | end 104 | end 105 | end 106 | end 107 | register Hancock::OpenIDServer 108 | end 109 | -------------------------------------------------------------------------------- /spec/units/openid_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__)+'/../spec_helper') 2 | 3 | describe "visiting /sso" do 4 | before(:each) do 5 | @user = Hancock::User.gen 6 | @consumer = Hancock::Consumer.gen(:internal) 7 | @identity_url = "http://example.org/sso/users/#{@user.id}" 8 | end 9 | it "should throw a bad request if there aren't any openid params" do 10 | get '/sso' 11 | last_response.status.should eql(400) 12 | end 13 | describe "with openid mode of associate" do 14 | it "should respond with Diffie Hellman data in kv format" do 15 | session = OpenID::Consumer::AssociationManager.create_session("DH-SHA1") 16 | params = {"openid.ns" => 'http://specs.openid.net/auth/2.0', 17 | "openid.mode" => "associate", 18 | "openid.session_type" => 'DH-SHA1', 19 | "openid.assoc_type" => 'HMAC-SHA1', 20 | "openid.dh_consumer_public"=> session.get_request['dh_consumer_public']} 21 | 22 | get "/sso", params 23 | 24 | message = OpenID::Message.from_kvform("#{last_response.body}") # wtf do i have to interpolate this! 25 | secret = session.extract_secret(message) 26 | secret.should_not be_nil 27 | 28 | args = message.get_args(OpenID::OPENID_NS) 29 | 30 | args['assoc_type'].should == 'HMAC-SHA1' 31 | args['assoc_handle'].should =~ /^\{HMAC-SHA1\}\{[^\}]{8}\}\{[^\}]{8}\}$/ 32 | args['session_type'].should == 'DH-SHA1' 33 | args['enc_mac_key'].size.should == 28 34 | args['expires_in'].should =~ /^\d+$/ 35 | args['dh_server_public'].size.should == 172 36 | end 37 | end 38 | describe "with openid mode of checkid_setup" do 39 | describe "authenticated" do 40 | it "should redirect to the consumer app" do 41 | params = { 42 | "openid.ns" => "http://specs.openid.net/auth/2.0", 43 | "openid.mode" => "checkid_setup", 44 | "openid.return_to" => @consumer.url, 45 | "openid.identity" => @identity_url, 46 | "openid.claimed_id" => @identity_url} 47 | 48 | login(@user) 49 | get "/sso", params 50 | last_response.status.should == 302 51 | 52 | redirect_params = Addressable::URI.parse(last_response.headers['Location']).query_values 53 | 54 | redirect_params['openid.ns'].should == 'http://specs.openid.net/auth/2.0' 55 | redirect_params['openid.mode'].should == 'id_res' 56 | redirect_params['openid.return_to'].should == @consumer.url 57 | redirect_params['openid.assoc_handle'].should =~ /^\{HMAC-SHA1\}\{[^\}]{8}\}\{[^\}]{8}\}$/ 58 | redirect_params['openid.op_endpoint'].should == 'http://example.org/sso' 59 | redirect_params['openid.claimed_id'].should == @identity_url 60 | redirect_params['openid.identity'].should == @identity_url 61 | 62 | redirect_params['openid.sreg.email'].should == @user.email 63 | redirect_params['openid.sreg.last_name'].should == @user.last_name 64 | redirect_params['openid.sreg.first_name'].should == @user.first_name 65 | 66 | redirect_params['openid.sig'].should_not be_nil 67 | redirect_params['openid.signed'].should_not be_nil 68 | redirect_params['openid.response_nonce'].should_not be_nil 69 | end 70 | 71 | describe "attempting to access another identity" do 72 | it "should return forbidden" do 73 | params = { 74 | "openid.ns" => "http://specs.openid.net/auth/2.0", 75 | "openid.mode" => "checkid_setup", 76 | "openid.return_to" => @consumer.url, 77 | "openid.identity" => "http://example.org/sso/users/42", 78 | "openid.claimed_id" => "http://example.org/sso/users/42"} 79 | 80 | login(@user) 81 | get "/sso", params 82 | last_response.status.should == 403 83 | end 84 | end 85 | describe "attempting to access from an untrusted consumer" do 86 | it "cancel the openid request" do 87 | params = { 88 | "openid.ns" => "http://specs.openid.net/auth/2.0", 89 | "openid.mode" => "checkid_setup", 90 | "openid.return_to" => "http://rogueconsumerapp.com/", 91 | "openid.identity" => @identity_url, 92 | "openid.claimed_id" => @identity_url} 93 | 94 | login(@user) 95 | get "/sso", params 96 | last_response.status.should == 403 97 | end 98 | end 99 | end 100 | describe "unauthenticated user" do 101 | it "should require authentication" do 102 | params = { 103 | "openid.ns" => "http://specs.openid.net/auth/2.0", 104 | "openid.mode" => "checkid_setup", 105 | "openid.return_to" => @consumer.url, 106 | "openid.identity" => @identity_url, 107 | "openid.claimed_id" => @identity_url} 108 | 109 | get "/sso", params 110 | last_response.body.should be_a_login_form 111 | end 112 | end 113 | end 114 | describe "with openid mode of checkid_immediate" do 115 | describe "unauthenticated user" do 116 | it "should require authentication" do 117 | params = { 118 | "openid.ns" => "http://specs.openid.net/auth/2.0", 119 | "openid.mode" => "checkid_immediate", 120 | "openid.return_to" => @consumer.url, 121 | "openid.identity" => @identity_url, 122 | "openid.claimed_id" => @identity_url} 123 | 124 | get "/sso", params 125 | last_response.body.should be_a_login_form 126 | end 127 | end 128 | describe "authenticated user" do 129 | describe "with appropriate request parameters" do 130 | it "should redirect to the consumer app" do 131 | params = { 132 | "openid.ns" => "http://specs.openid.net/auth/2.0", 133 | "openid.mode" => "checkid_immediate", 134 | "openid.return_to" => @consumer.url, 135 | "openid.identity" => @identity_url, 136 | "openid.claimed_id" => @identity_url} 137 | 138 | login(@user) 139 | get "/sso", params 140 | last_response.status.should == 302 141 | 142 | redirect_params = Addressable::URI.parse(last_response.headers['Location']).query_values 143 | 144 | redirect_params['openid.ns'].should == 'http://specs.openid.net/auth/2.0' 145 | redirect_params['openid.mode'].should == 'id_res' 146 | redirect_params['openid.return_to'].should == @consumer.url 147 | redirect_params['openid.assoc_handle'].should =~ /^\{HMAC-SHA1\}\{[^\}]{8}\}\{[^\}]{8}\}$/ 148 | redirect_params['openid.op_endpoint'].should == 'http://example.org/sso' 149 | redirect_params['openid.claimed_id'].should == @identity_url 150 | redirect_params['openid.identity'].should == @identity_url 151 | 152 | redirect_params['openid.sreg.email'].should == @user.email 153 | redirect_params['openid.sreg.last_name'].should == @user.last_name 154 | redirect_params['openid.sreg.first_name'].should == @user.first_name 155 | 156 | redirect_params['openid.sig'].should_not be_nil 157 | redirect_params['openid.signed'].should_not be_nil 158 | redirect_params['openid.response_nonce'].should_not be_nil 159 | end 160 | end 161 | 162 | describe "attempting to access another identity" do 163 | it "should return forbidden" do 164 | params = { 165 | "openid.ns" => "http://specs.openid.net/auth/2.0", 166 | "openid.mode" => "checkid_immediate", 167 | "openid.return_to" => @consumer.url, 168 | "openid.identity" => "http://example.org/sso/users/42", 169 | "openid.claimed_id" => "http://example.org/sso/users/42" } 170 | 171 | login(@user) 172 | get "/sso", params 173 | last_response.status.should == 403 174 | end 175 | end 176 | 177 | describe "attempting to access from an untrusted consumer" do 178 | it "cancel the openid request" do 179 | params = { 180 | "openid.ns" => "http://specs.openid.net/auth/2.0", 181 | "openid.mode" => "checkid_immediate", 182 | "openid.return_to" => "http://rogueconsumerapp.com/", 183 | "openid.identity" => @identity_url, 184 | "openid.claimed_id" => @identity_url} 185 | 186 | login(@user) 187 | get "/sso", params 188 | last_response.status.should == 403 189 | end 190 | end 191 | end 192 | end 193 | end 194 | --------------------------------------------------------------------------------