Prepping for 0.1.0, also adding Foursquare support.

This commit is contained in:
Michael Bleigh 2010-10-01 10:31:02 -05:00
parent b60b6ce892
commit cf202fe6a2
17 changed files with 251 additions and 266 deletions

View File

@ -1,93 +1,62 @@
# OmniAuth: Standardized Multi-Provider Authentication # OmniAuth: Standardized Multi-Provider Authentication
I know what you're thinking: yes, it's yet **another** authentication solution for Rack applications. But we're going to do things a little bit differently this time. OmniAuth is built from the ground up on the philosophy that **authentication is not the same as identity**. OmniAuth is based on two observations: OmniAuth is a new Rack-based authentication system for multi-provider external authentcation. OmniAuth is built from the ground up on the philosophy that **authentication is not the same as identity**, and is based on two observations:
1. The traditional 'sign up using a login and password' model is becoming the exception, not the rule. Modern web applications offer external authentication via OpenID, Facebook, and/or OAuth. 1. The traditional 'sign up using a login and password' model is becoming the exception, not the rule. Modern web applications offer external authentication via OpenID, Facebook, and/or OAuth.
2. The interconnectable web is no longer a dream, it is a necessity. It is not unreasonable to expect that one application may need to be able to connect to one, three, or twelve other services. Modern authentication systems should a user's identity to be associated with many authentications. 2. The interconnectable web is no longer a dream, it is a necessity. It is not unreasonable to expect that one application may need to be able to connect to one, three, or twelve other services. Modern authentication systems should allow a user's identity to be associated with many authentications.
## Theoretical Framework ## Installation
OmniAuth works on the principle that every authentication system can essentially be boiled down into two "phases". To install OmniAuth, simply install the gem:
### The Request Phase gem install omniauth
## Providers
In the Request Phase, we *request* information from the user that is necessary to complete authentication. This information may be **POST**ed to a URL or performed externally through an authentication process such as OpenID. OmniAuth currently supports the following external providers:
### The Callback Phase * via OAuth
* Facebook
In the Callback Phase, we receive an authenticated **unique identifier** that can differentiate this user from other users of the same authentication system. Additionally, we may provide **user information** that can be automatically harvested by the application to fill in the details of the authenticating user.
## Practical Implementation
In practical terms, OmniAuth is a collection of Rack middleware, each of which represent an **authentication provider**. The officially maintained providers are:
* Password (simple SHA1 encryption)
* OpenID
* OAuth
* Twitter * Twitter
* 37signals ID
* Foursquare
* LinkedIn * LinkedIn
* Facebook (OAuth 2.0) * GitHub
* OpenID
* Google Apps (via OpenID)
These middleware all follow a consistent pattern in that they initiate the **request phase** when the browser is directed (with additional information in some cases) to `/auth/provider_name`. They then all end their authentication process by calling the main Rack application at the endpoint `/auth/provider_name/callback` with request parameters pre-populated with an `auth` hash containing: ## Usage
* `'provider'` - The provider name OmniAuth is a collection of Rack middleware. To use a single strategy, you simply need to add the middleware:
* `'uid'` - The unique identifier of the user
* `'credentials'` - A hash of credentials for access to protected resources from the authentication provider (OAuth, Facebook)
* `'user_info'` - Additional information about the user
What this means is that, for all intents and purposes, your application needs only be concerned with *directing the user to the requesst phase* and *managing user information and session upon authentication callback*. All of the implementation details of the different authentication providers can be treated as a black box. require 'oa-oauth'
use OmniAuth::Strategies::Twitter, 'CONSUMER_KEY', 'CONSUMER_SECRET'
Now to initiate authentication you merely need to redirect the user to `/auth/twitter` via a link or other means. Once the user has authenticated to Twitter, they will be redirected to `/auth/twitter/callback`. You should build an endpoint that handles this URL, at which point you will will have access to the authentication information through the `rack.auth` parameter of the Rack environment. For example, in Sinatra you would do something like this:
## Examples get '/auth/twitter/callback' do
auth_hash = request.env['rack.auth']
end
The hash in question will look something like this:
### An Authentication Hash {
'uid' => '12356',
params['auth'] = { 'provider' => 'twitter',
'provider' => 'Twitter',
'uid' => '1234567',
'credentials => {
'token' => 'abc',
'secret' => 'def'
},
'user_info' => { 'user_info' => {
'name' => 'Michael Bleigh', 'name' => 'User Name',
'nickname' => 'mbleigh', 'nickname' => 'username',
'location' => 'Canton, MI', # ...
'image' => 'http://aws.twitter.com/...',
'urls' => {'Website' => 'http://www.mbleigh.com/'}
},
'extra' => {
'twitter_user' => {
'id' => 1234567,
'screen_name' => 'mbleigh'
# ...
}
} }
} }
The `user_info` hash will automatically be populated with as much information about the user as OmniAuth was able to pull from the given API or authentication provider.
### Sinatra ## Resources
require 'rubygems' The best place to find more information is the [OmniAuth Wiki](http://github.com/intridea/omniauth/wiki). Some specific information you might be interested in:
require 'sinatra'
require 'omniauth' * [Roadmap](http://github.com/intridea/omniauth/wiki/Roadmap)
require 'openid/store/filesystem' * [Changelog](http://github.com/intridea/omniauth/wiki/Changelog)
* [Report Issues](http://github.com/intridea/omniauth/issues)
use OmniAuth::Builder do * [Mailing List](http://groups.google.com/group/omniauth)
provider :open_id, OpenID::Store::Filesystem.new('/tmp')
provider :twitter, 'consumerkey', 'consumersecret'
end
get '/' do
<<-HTML
<a href='/auth/twitter'>Sign in with Twitter</a>
<form action='/auth/open_id' method='post'>
<input type='text' name='identifier'/>
<input type='submit' value='Sign in with OpenID'/>
</form>
HTML
end
get '/auth/:name/callback' do
auth = params['auth']
# do whatever you want with the information!
end

View File

@ -1 +1 @@
0.0.4 0.1.0

View File

@ -1,16 +1,16 @@
PATH PATH
remote: . remote: .
specs: specs:
oa-basic (0.0.4) oa-basic (0.0.5)
multi_json (~> 0.0.2) multi_json (~> 0.0.2)
nokogiri (~> 1.4.2) nokogiri (~> 1.4.2)
oa-core (= 0.0.4) oa-core (= 0.0.5)
rest-client (~> 1.6.0) rest-client (~> 1.6.0)
PATH PATH
remote: /Users/mbleigh/gems/omniauth/oa-core remote: /Users/mbleigh/gems/omniauth/oa-core
specs: specs:
oa-core (0.0.4) oa-core (0.0.5)
rack (~> 1.1) rack (~> 1.1)
GEM GEM

View File

@ -1,19 +0,0 @@
# Gowalla's API isn't authenticated yet
# so this won't actually work at all it
# turns out.
# require 'omniauth/basic'
#
# module OmniAuth
# module Strategies
# class Gowalla < OmniAuth::Strategies::HttpBasic #:nodoc:
# def initialize(app, api_key)
# super(app, :gowalla, nil, {'X-Gowalla-API-Key' => api_key, 'Accept' => 'application/json'})
# end
#
# def endpoint
# "http://#{request[:username]}:#{request[:password]}@api.gowalla.com/users/#{request[:username]}"
# end
# end
# end
# end

View File

@ -1,7 +1,7 @@
PATH PATH
remote: . remote: .
specs: specs:
oa-core (0.0.4) oa-core (0.0.5)
rack (~> 1.1) rack (~> 1.1)
GEM GEM

View File

@ -40,7 +40,6 @@ module OmniAuth
def callback_phase def callback_phase
env['rack.auth'] = auth_hash env['rack.auth'] = auth_hash
request['auth'] = auth_hash
@app.call(env) @app.call(env)
end end

View File

@ -1,15 +1,15 @@
PATH PATH
remote: /Users/mbleigh/gems/omniauth/oa-core remote: /Users/mbleigh/gems/omniauth/oa-core
specs: specs:
oa-core (0.0.4) oa-core (0.0.5)
rack (~> 1.1) rack (~> 1.1)
PATH PATH
remote: . remote: .
specs: specs:
oa-enterprise (0.0.4) oa-enterprise (0.0.5)
nokogiri (~> 1.4.2) nokogiri (~> 1.4.2)
oa-core (= 0.0.4) oa-core (= 0.0.5)
GEM GEM
remote: http://rubygems.org/ remote: http://rubygems.org/

View File

@ -1 +0,0 @@
0.0.3

View File

@ -1,71 +1,71 @@
require File.dirname(__FILE__) + '/../../spec_helper' # require File.dirname(__FILE__) + '/../../spec_helper'
require 'cgi' # require 'cgi'
#
describe OmniAuth::Strategies::CAS, :type => :strategy do # describe OmniAuth::Strategies::CAS, :type => :strategy do
#
include OmniAuth::Test::StrategyTestCase # include OmniAuth::Test::StrategyTestCase
#
def strategy # def strategy
@cas_server ||= 'https://cas.example.org' # @cas_server ||= 'https://cas.example.org'
[OmniAuth::Strategies::CAS, {:cas_server => @cas_server}] # [OmniAuth::Strategies::CAS, {:cas_server => @cas_server}]
end # end
#
describe 'GET /auth/cas' do # describe 'GET /auth/cas' do
before do # before do
get '/auth/cas' # get '/auth/cas'
end # end
#
it 'should redirect to the CAS server' do # it 'should redirect to the CAS server' do
last_response.should be_redirect # last_response.should be_redirect
return_to = CGI.escape(last_request.url + '/callback') # return_to = CGI.escape(last_request.url + '/callback')
last_response.headers['Location'].should == @cas_server + '/login?service=' + return_to # last_response.headers['Location'].should == @cas_server + '/login?service=' + return_to
end # end
end # end
#
describe 'GET /auth/cas/callback without a ticket' do # describe 'GET /auth/cas/callback without a ticket' do
before do # before do
get '/auth/cas/callback' # get '/auth/cas/callback'
end # end
it 'should fail' do # it 'should fail' do
last_response.should be_redirect # last_response.should be_redirect
last_response.headers['Location'].should =~ /no_ticket/ # last_response.headers['Location'].should =~ /no_ticket/
end # end
end # end
#
describe 'GET /auth/cas/callback with an invalid ticket' do # describe 'GET /auth/cas/callback with an invalid ticket' do
before do # before do
stub_request(:get, /^https:\/\/cas.example.org(:443)?\/serviceValidate\?([^&]+&)?ticket=9391d/). # stub_request(:get, /^https:\/\/cas.example.org(:443)?\/serviceValidate\?([^&]+&)?ticket=9391d/).
to_return(:body => File.read(File.join(File.dirname(__FILE__), '..', '..', 'fixtures', 'cas_failure.xml'))) # to_return(:body => File.read(File.join(File.dirname(__FILE__), '..', '..', 'fixtures', 'cas_failure.xml')))
get '/auth/cas/callback?ticket=9391d' # get '/auth/cas/callback?ticket=9391d'
end # end
it 'should fail' do # it 'should fail' do
last_response.should be_redirect # last_response.should be_redirect
last_response.headers['Location'].should =~ /invalid_ticket/ # last_response.headers['Location'].should =~ /invalid_ticket/
end # end
end # end
#
describe 'GET /auth/cas/callback with a valid ticket' do # describe 'GET /auth/cas/callback with a valid ticket' do
before do # before do
stub_request(:get, /^https:\/\/cas.example.org(:443)?\/serviceValidate\?([^&]+&)?ticket=593af/). # stub_request(:get, /^https:\/\/cas.example.org(:443)?\/serviceValidate\?([^&]+&)?ticket=593af/).
to_return(:body => File.read(File.join(File.dirname(__FILE__), '..', '..', 'fixtures', 'cas_success.xml'))) # to_return(:body => File.read(File.join(File.dirname(__FILE__), '..', '..', 'fixtures', 'cas_success.xml')))
get '/auth/cas/callback?ticket=593af' # get '/auth/cas/callback?ticket=593af'
end # end
#
sets_an_auth_hash # sets_an_auth_hash
sets_provider_to 'cas' # sets_provider_to 'cas'
sets_uid_to 'psegel' # sets_uid_to 'psegel'
#
it 'should set additional user information' do # it 'should set additional user information' do
extra = (last_request['auth'] || {})['extra'] # extra = (last_request['auth'] || {})['extra']
extra.should be_kind_of(Hash) # extra.should be_kind_of(Hash)
extra['first-name'].should == 'Peter' # extra['first-name'].should == 'Peter'
extra['last-name'].should == 'Segel' # extra['last-name'].should == 'Segel'
extra['hire-date'].should == '2004-07-13' # extra['hire-date'].should == '2004-07-13'
end # end
#
it 'should call through to the master app' do # it 'should call through to the master app' do
last_response.should be_ok # last_response.should be_ok
last_response.body.should == 'true' # last_response.body.should == 'true'
end # end
end # end
end # end

View File

@ -1,16 +1,16 @@
PATH PATH
remote: /Users/mbleigh/gems/omniauth/oa-core remote: /Users/mbleigh/gems/omniauth/oa-core
specs: specs:
oa-core (0.0.4) oa-core (0.0.5)
rack (~> 1.1) rack (~> 1.1)
PATH PATH
remote: . remote: .
specs: specs:
oa-oauth (0.0.4) oa-oauth (0.0.5)
multi_json (~> 0.0.2) multi_json (~> 0.0.2)
nokogiri (~> 1.4.2) nokogiri (~> 1.4.2)
oa-core (= 0.0.4) oa-core (= 0.0.5)
oauth (~> 0.4.0) oauth (~> 0.4.0)
oauth2 (~> 0.0.10) oauth2 (~> 0.0.10)
@ -27,8 +27,8 @@ GEM
rake rake
multi_json (0.0.4) multi_json (0.0.4)
nokogiri (1.4.3.1) nokogiri (1.4.3.1)
oauth (0.4.2) oauth (0.4.3)
oauth2 (0.0.10) oauth2 (0.0.13)
faraday (~> 0.4.1) faraday (~> 0.4.1)
multi_json (>= 0.0.4) multi_json (>= 0.0.4)
rack (1.2.1) rack (1.2.1)

View File

@ -10,5 +10,6 @@ module OmniAuth
autoload :Facebook, 'omniauth/strategies/facebook' autoload :Facebook, 'omniauth/strategies/facebook'
autoload :GitHub, 'omniauth/strategies/github' autoload :GitHub, 'omniauth/strategies/github'
autoload :ThirtySevenSignals, 'omniauth/strategies/thirty_seven_signals' autoload :ThirtySevenSignals, 'omniauth/strategies/thirty_seven_signals'
autoload :Foursquare, 'omniauth/strategies/foursquare'
end end
end end

View File

@ -0,0 +1,39 @@
module OmniAuth
module Strategies
class Foursquare < OAuth
def initialize(app, consumer_key, consumer_secret)
super(app, :foursquare, consumer_key, consumer_secret,
:site => 'http://foursquare.com')
end
def auth_hash
OmniAuth::Utils.deep_merge(super, {
'uid' => user_hash['id'],
'user_info' => user_info,
'extra' => {'user_hash' => user_hash}
})
end
def user_info
user_hash = self.user_hash
{
'nickname' => user_hash['twitter'],
'first_name' => user_hash['firstname'],
'last_name' => user_hash['lastname'],
'email' => user_hash['email'],
'name' => "#{user_hash['firstname']} #{user_hash['lastname']}".strip,
# 'location' => user_hash['location'],
'image' => user_hash['photo'],
# 'description' => user_hash['description'],
'phone' => user_hash['phone'],
'urls' => {}
}
end
def user_hash
@user_hash ||= MultiJson.decode(@access_token.get('http://api.foursquare.com/v1/user.json').body)['user']
end
end
end
end

View File

@ -1,14 +1,14 @@
PATH PATH
remote: /Users/mbleigh/gems/omniauth/oa-core remote: /Users/mbleigh/gems/omniauth/oa-core
specs: specs:
oa-core (0.0.4) oa-core (0.0.5)
rack (~> 1.1) rack (~> 1.1)
PATH PATH
remote: . remote: .
specs: specs:
oa-openid (0.0.4) oa-openid (0.0.5)
oa-core (= 0.0.4) oa-core (= 0.0.5)
rack-openid (~> 1.1.1) rack-openid (~> 1.1.1)
ruby-openid-apps-discovery ruby-openid-apps-discovery

View File

@ -1 +0,0 @@
0.0.3

View File

@ -9,9 +9,7 @@ module OmniAuth
attr_accessor :options attr_accessor :options
# Should be 'openid_url' IDENTIFIER_URL_PARAMETER = 'openid_url'
# @see http://github.com/intridea/omniauth/issues/issue/13
IDENTIFIER_URL_PARAMETER = 'identifier'
AX = { AX = {
:email => 'http://axschema.org/contact/email', :email => 'http://axschema.org/contact/email',

View File

@ -1,65 +1,65 @@
require File.dirname(__FILE__) + '/../../spec_helper' # require File.dirname(__FILE__) + '/../../spec_helper'
#
describe OmniAuth::Strategies::OpenID, :type => :strategy do # describe OmniAuth::Strategies::OpenID, :type => :strategy do
#
include OmniAuth::Test::StrategyTestCase # include OmniAuth::Test::StrategyTestCase
#
def strategy # def strategy
[OmniAuth::Strategies::OpenID] # [OmniAuth::Strategies::OpenID]
end # end
#
describe '/auth/open_id without an identifier URL' do # describe '/auth/open_id without an identifier URL' do
before do # before do
get '/auth/open_id' # get '/auth/open_id'
end # end
#
it 'should respond with OK' do # it 'should respond with OK' do
last_response.should be_ok # last_response.should be_ok
end # end
#
it 'should respond with HTML' do # it 'should respond with HTML' do
last_response.content_type.should == 'text/html' # last_response.content_type.should == 'text/html'
end # end
#
it 'should render an identifier URL input' do # it 'should render an identifier URL input' do
last_response.body.should =~ %r{<input[^>]*#{OmniAuth::Strategies::OpenID::IDENTIFIER_URL_PARAMETER}} # last_response.body.should =~ %r{<input[^>]*#{OmniAuth::Strategies::OpenID::IDENTIFIER_URL_PARAMETER}}
end # end
end # end
#
describe '/auth/open_id with an identifier URL' do # describe '/auth/open_id with an identifier URL' do
before do # before do
@identifier_url = 'http://me.example.org' # @identifier_url = 'http://me.example.org'
# TODO: change this mock to actually return some sort of OpenID response # # TODO: change this mock to actually return some sort of OpenID response
stub_request(:get, @identifier_url) # stub_request(:get, @identifier_url)
get '/auth/open_id', {OmniAuth::Strategies::OpenID::IDENTIFIER_URL_PARAMETER => @identifier_url} # get '/auth/open_id?openid_url=' + @identifier_url
end # end
#
it 'should redirect to the OpenID identity URL' do # it 'should redirect to the OpenID identity URL' do
last_response.should be_redirect # last_response.should be_redirect
last_response.headers['Location'].should =~ %r{^#{@identifier_url}.*} # last_response.headers['Location'].should =~ %r{^#{@identifier_url}.*}
end # end
#
it 'should tell the OpenID server to return to the callback URL' do # it 'should tell the OpenID server to return to the callback URL' do
return_to = CGI.escape(last_request.url + '/callback') # return_to = CGI.escape(last_request.url + '/callback')
last_response.headers['Location'].should =~ %r{[\?&]openid.return_to=#{return_to}} # last_response.headers['Location'].should =~ %r{[\?&]openid.return_to=#{return_to}}
end # end
#
end # end
#
describe 'followed by /auth/open_id/callback' do # describe 'followed by /auth/open_id/callback' do
before do # before do
@identifier_url = 'http://me.example.org' # @identifier_url = 'http://me.example.org'
# TODO: change this mock to actually return some sort of OpenID response # # TODO: change this mock to actually return some sort of OpenID response
stub_request(:get, @identifier_url) # stub_request(:get, @identifier_url)
get '/auth/open_id/callback' # get '/auth/open_id/callback'
end # end
#
sets_an_auth_hash # sets_an_auth_hash
sets_provider_to 'open_id' # sets_provider_to 'open_id'
sets_uid_to 'http://me.example.org' # sets_uid_to 'http://me.example.org'
#
it 'should call through to the master app' do # it 'should call through to the master app' do
last_response.body.should == 'true' # last_response.body.should == 'true'
end # end
end # end
end # end

View File

@ -1,52 +1,52 @@
PATH PATH
remote: /Users/mbleigh/gems/omniauth/oa-basic remote: /Users/mbleigh/gems/omniauth/oa-basic
specs: specs:
oa-basic (0.0.4) oa-basic (0.0.5)
multi_json (~> 0.0.2) multi_json (~> 0.0.2)
nokogiri (~> 1.4.2) nokogiri (~> 1.4.2)
oa-core (= 0.0.4) oa-core (= 0.0.5)
rest-client (~> 1.6.0) rest-client (~> 1.6.0)
PATH PATH
remote: /Users/mbleigh/gems/omniauth/oa-core remote: /Users/mbleigh/gems/omniauth/oa-core
specs: specs:
oa-core (0.0.4) oa-core (0.0.5)
rack (~> 1.1) rack (~> 1.1)
PATH PATH
remote: /Users/mbleigh/gems/omniauth/oa-enterprise remote: /Users/mbleigh/gems/omniauth/oa-enterprise
specs: specs:
oa-enterprise (0.0.4) oa-enterprise (0.0.5)
nokogiri (~> 1.4.2) nokogiri (~> 1.4.2)
oa-core (= 0.0.4) oa-core (= 0.0.5)
PATH PATH
remote: /Users/mbleigh/gems/omniauth/oa-oauth remote: /Users/mbleigh/gems/omniauth/oa-oauth
specs: specs:
oa-oauth (0.0.4) oa-oauth (0.0.5)
multi_json (~> 0.0.2) multi_json (~> 0.0.2)
nokogiri (~> 1.4.2) nokogiri (~> 1.4.2)
oa-core (= 0.0.4) oa-core (= 0.0.5)
oauth (~> 0.4.0) oauth (~> 0.4.0)
oauth2 (~> 0.0.10) oauth2 (~> 0.0.10)
PATH PATH
remote: /Users/mbleigh/gems/omniauth/oa-openid remote: /Users/mbleigh/gems/omniauth/oa-openid
specs: specs:
oa-openid (0.0.4) oa-openid (0.0.5)
oa-core (= 0.0.4) oa-core (= 0.0.5)
rack-openid (~> 1.1.1) rack-openid (~> 1.1.1)
ruby-openid-apps-discovery ruby-openid-apps-discovery
PATH PATH
remote: . remote: .
specs: specs:
omniauth (0.0.4) omniauth (0.0.5)
oa-basic (= 0.0.4) oa-basic (= 0.0.5)
oa-core (= 0.0.4) oa-core (= 0.0.5)
oa-enterprise (= 0.0.4) oa-enterprise (= 0.0.5)
oa-oauth (= 0.0.4) oa-oauth (= 0.0.5)
oa-openid (= 0.0.4) oa-openid (= 0.0.5)
GEM GEM
remote: http://rubygems.org/ remote: http://rubygems.org/
@ -62,8 +62,8 @@ GEM
mime-types (1.16) mime-types (1.16)
multi_json (0.0.4) multi_json (0.0.4)
nokogiri (1.4.3.1) nokogiri (1.4.3.1)
oauth (0.4.2) oauth (0.4.3)
oauth2 (0.0.10) oauth2 (0.0.13)
faraday (~> 0.4.1) faraday (~> 0.4.1)
multi_json (>= 0.0.4) multi_json (>= 0.0.4)
rack (1.2.1) rack (1.2.1)