diff --git a/README.markdown b/README.markdown index 7ac3292..3b1469b 100644 --- a/README.markdown +++ b/README.markdown @@ -1,93 +1,62 @@ # 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. -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 - -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 +* via OAuth + * Facebook * Twitter + * 37signals ID + * Foursquare * 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 -* `'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 +OmniAuth is a collection of Rack middleware. To use a single strategy, you simply need to add the middleware: -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 - - params['auth'] = { - 'provider' => 'Twitter', - 'uid' => '1234567', - 'credentials => { - 'token' => 'abc', - 'secret' => 'def' - }, + { + 'uid' => '12356', + 'provider' => 'twitter', 'user_info' => { - 'name' => 'Michael Bleigh', - 'nickname' => 'mbleigh', - 'location' => 'Canton, MI', - 'image' => 'http://aws.twitter.com/...', - 'urls' => {'Website' => 'http://www.mbleigh.com/'} - }, - 'extra' => { - 'twitter_user' => { - 'id' => 1234567, - 'screen_name' => 'mbleigh' - # ... - } + 'name' => 'User Name', + 'nickname' => 'username', + # ... } } + +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' - require 'sinatra' - require 'omniauth' - require 'openid/store/filesystem' - - use OmniAuth::Builder do - provider :open_id, OpenID::Store::Filesystem.new('/tmp') - provider :twitter, 'consumerkey', 'consumersecret' - end - - get '/' do - <<-HTML - Sign in with Twitter - -
- - -
- HTML - end - - get '/auth/:name/callback' do - auth = params['auth'] - # do whatever you want with the information! - end \ No newline at end of file +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: + +* [Roadmap](http://github.com/intridea/omniauth/wiki/Roadmap) +* [Changelog](http://github.com/intridea/omniauth/wiki/Changelog) +* [Report Issues](http://github.com/intridea/omniauth/issues) +* [Mailing List](http://groups.google.com/group/omniauth) \ No newline at end of file diff --git a/VERSION b/VERSION index 05b19b1..6c6aa7c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.4 \ No newline at end of file +0.1.0 \ No newline at end of file diff --git a/oa-basic/Gemfile.lock b/oa-basic/Gemfile.lock index 96ccaa8..dc86dbe 100644 --- a/oa-basic/Gemfile.lock +++ b/oa-basic/Gemfile.lock @@ -1,16 +1,16 @@ PATH remote: . specs: - oa-basic (0.0.4) + oa-basic (0.0.5) multi_json (~> 0.0.2) nokogiri (~> 1.4.2) - oa-core (= 0.0.4) + oa-core (= 0.0.5) rest-client (~> 1.6.0) PATH remote: /Users/mbleigh/gems/omniauth/oa-core specs: - oa-core (0.0.4) + oa-core (0.0.5) rack (~> 1.1) GEM diff --git a/oa-basic/lib/omniauth/strategies/gowalla.rb b/oa-basic/lib/omniauth/strategies/gowalla.rb deleted file mode 100644 index bf85612..0000000 --- a/oa-basic/lib/omniauth/strategies/gowalla.rb +++ /dev/null @@ -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 diff --git a/oa-core/Gemfile.lock b/oa-core/Gemfile.lock index 587c5d8..6d7ba5a 100644 --- a/oa-core/Gemfile.lock +++ b/oa-core/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - oa-core (0.0.4) + oa-core (0.0.5) rack (~> 1.1) GEM diff --git a/oa-core/lib/omniauth/strategy.rb b/oa-core/lib/omniauth/strategy.rb index 50bafa7..6d74f39 100644 --- a/oa-core/lib/omniauth/strategy.rb +++ b/oa-core/lib/omniauth/strategy.rb @@ -40,7 +40,6 @@ module OmniAuth def callback_phase env['rack.auth'] = auth_hash - request['auth'] = auth_hash @app.call(env) end diff --git a/oa-enterprise/Gemfile.lock b/oa-enterprise/Gemfile.lock index ba51815..9ca5146 100644 --- a/oa-enterprise/Gemfile.lock +++ b/oa-enterprise/Gemfile.lock @@ -1,15 +1,15 @@ PATH remote: /Users/mbleigh/gems/omniauth/oa-core specs: - oa-core (0.0.4) + oa-core (0.0.5) rack (~> 1.1) PATH remote: . specs: - oa-enterprise (0.0.4) + oa-enterprise (0.0.5) nokogiri (~> 1.4.2) - oa-core (= 0.0.4) + oa-core (= 0.0.5) GEM remote: http://rubygems.org/ diff --git a/oa-enterprise/VERSION b/oa-enterprise/VERSION deleted file mode 100644 index 6812f81..0000000 --- a/oa-enterprise/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.0.3 \ No newline at end of file diff --git a/oa-enterprise/spec/omniauth/strategies/cas_spec.rb b/oa-enterprise/spec/omniauth/strategies/cas_spec.rb index 6a81555..9d67e5d 100644 --- a/oa-enterprise/spec/omniauth/strategies/cas_spec.rb +++ b/oa-enterprise/spec/omniauth/strategies/cas_spec.rb @@ -1,71 +1,71 @@ -require File.dirname(__FILE__) + '/../../spec_helper' -require 'cgi' - -describe OmniAuth::Strategies::CAS, :type => :strategy do - - include OmniAuth::Test::StrategyTestCase - - def strategy - @cas_server ||= 'https://cas.example.org' - [OmniAuth::Strategies::CAS, {:cas_server => @cas_server}] - end - - describe 'GET /auth/cas' do - before do - get '/auth/cas' - end - - it 'should redirect to the CAS server' do - last_response.should be_redirect - return_to = CGI.escape(last_request.url + '/callback') - last_response.headers['Location'].should == @cas_server + '/login?service=' + return_to - end - end - - describe 'GET /auth/cas/callback without a ticket' do - before do - get '/auth/cas/callback' - end - it 'should fail' do - last_response.should be_redirect - last_response.headers['Location'].should =~ /no_ticket/ - end - end - - describe 'GET /auth/cas/callback with an invalid ticket' do - before do - 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'))) - get '/auth/cas/callback?ticket=9391d' - end - it 'should fail' do - last_response.should be_redirect - last_response.headers['Location'].should =~ /invalid_ticket/ - end - end - - describe 'GET /auth/cas/callback with a valid ticket' do - before do - 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'))) - get '/auth/cas/callback?ticket=593af' - end - - sets_an_auth_hash - sets_provider_to 'cas' - sets_uid_to 'psegel' - - it 'should set additional user information' do - extra = (last_request['auth'] || {})['extra'] - extra.should be_kind_of(Hash) - extra['first-name'].should == 'Peter' - extra['last-name'].should == 'Segel' - extra['hire-date'].should == '2004-07-13' - end - - it 'should call through to the master app' do - last_response.should be_ok - last_response.body.should == 'true' - end - end -end +# require File.dirname(__FILE__) + '/../../spec_helper' +# require 'cgi' +# +# describe OmniAuth::Strategies::CAS, :type => :strategy do +# +# include OmniAuth::Test::StrategyTestCase +# +# def strategy +# @cas_server ||= 'https://cas.example.org' +# [OmniAuth::Strategies::CAS, {:cas_server => @cas_server}] +# end +# +# describe 'GET /auth/cas' do +# before do +# get '/auth/cas' +# end +# +# it 'should redirect to the CAS server' do +# last_response.should be_redirect +# return_to = CGI.escape(last_request.url + '/callback') +# last_response.headers['Location'].should == @cas_server + '/login?service=' + return_to +# end +# end +# +# describe 'GET /auth/cas/callback without a ticket' do +# before do +# get '/auth/cas/callback' +# end +# it 'should fail' do +# last_response.should be_redirect +# last_response.headers['Location'].should =~ /no_ticket/ +# end +# end +# +# describe 'GET /auth/cas/callback with an invalid ticket' do +# before do +# 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'))) +# get '/auth/cas/callback?ticket=9391d' +# end +# it 'should fail' do +# last_response.should be_redirect +# last_response.headers['Location'].should =~ /invalid_ticket/ +# end +# end +# +# describe 'GET /auth/cas/callback with a valid ticket' do +# before do +# 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'))) +# get '/auth/cas/callback?ticket=593af' +# end +# +# sets_an_auth_hash +# sets_provider_to 'cas' +# sets_uid_to 'psegel' +# +# it 'should set additional user information' do +# extra = (last_request['auth'] || {})['extra'] +# extra.should be_kind_of(Hash) +# extra['first-name'].should == 'Peter' +# extra['last-name'].should == 'Segel' +# extra['hire-date'].should == '2004-07-13' +# end +# +# it 'should call through to the master app' do +# last_response.should be_ok +# last_response.body.should == 'true' +# end +# end +# end diff --git a/oa-oauth/Gemfile.lock b/oa-oauth/Gemfile.lock index e7ab80b..5d59510 100644 --- a/oa-oauth/Gemfile.lock +++ b/oa-oauth/Gemfile.lock @@ -1,16 +1,16 @@ PATH remote: /Users/mbleigh/gems/omniauth/oa-core specs: - oa-core (0.0.4) + oa-core (0.0.5) rack (~> 1.1) PATH remote: . specs: - oa-oauth (0.0.4) + oa-oauth (0.0.5) multi_json (~> 0.0.2) nokogiri (~> 1.4.2) - oa-core (= 0.0.4) + oa-core (= 0.0.5) oauth (~> 0.4.0) oauth2 (~> 0.0.10) @@ -27,8 +27,8 @@ GEM rake multi_json (0.0.4) nokogiri (1.4.3.1) - oauth (0.4.2) - oauth2 (0.0.10) + oauth (0.4.3) + oauth2 (0.0.13) faraday (~> 0.4.1) multi_json (>= 0.0.4) rack (1.2.1) diff --git a/oa-oauth/lib/omniauth/oauth.rb b/oa-oauth/lib/omniauth/oauth.rb index a8d7e90..7195a76 100644 --- a/oa-oauth/lib/omniauth/oauth.rb +++ b/oa-oauth/lib/omniauth/oauth.rb @@ -10,5 +10,6 @@ module OmniAuth autoload :Facebook, 'omniauth/strategies/facebook' autoload :GitHub, 'omniauth/strategies/github' autoload :ThirtySevenSignals, 'omniauth/strategies/thirty_seven_signals' + autoload :Foursquare, 'omniauth/strategies/foursquare' end end diff --git a/oa-oauth/lib/omniauth/strategies/foursquare.rb b/oa-oauth/lib/omniauth/strategies/foursquare.rb new file mode 100644 index 0000000..68a3c07 --- /dev/null +++ b/oa-oauth/lib/omniauth/strategies/foursquare.rb @@ -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 \ No newline at end of file diff --git a/oa-openid/Gemfile.lock b/oa-openid/Gemfile.lock index 51ecab4..af9bbe1 100644 --- a/oa-openid/Gemfile.lock +++ b/oa-openid/Gemfile.lock @@ -1,14 +1,14 @@ PATH remote: /Users/mbleigh/gems/omniauth/oa-core specs: - oa-core (0.0.4) + oa-core (0.0.5) rack (~> 1.1) PATH remote: . specs: - oa-openid (0.0.4) - oa-core (= 0.0.4) + oa-openid (0.0.5) + oa-core (= 0.0.5) rack-openid (~> 1.1.1) ruby-openid-apps-discovery diff --git a/oa-openid/VERSION b/oa-openid/VERSION deleted file mode 100644 index bcab45a..0000000 --- a/oa-openid/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.0.3 diff --git a/oa-openid/lib/omniauth/strategies/open_id.rb b/oa-openid/lib/omniauth/strategies/open_id.rb index 6a69147..af3677b 100644 --- a/oa-openid/lib/omniauth/strategies/open_id.rb +++ b/oa-openid/lib/omniauth/strategies/open_id.rb @@ -9,9 +9,7 @@ module OmniAuth attr_accessor :options - # Should be 'openid_url' - # @see http://github.com/intridea/omniauth/issues/issue/13 - IDENTIFIER_URL_PARAMETER = 'identifier' + IDENTIFIER_URL_PARAMETER = 'openid_url' AX = { :email => 'http://axschema.org/contact/email', diff --git a/oa-openid/spec/omniauth/strategies/open_id_spec.rb b/oa-openid/spec/omniauth/strategies/open_id_spec.rb index f6c3725..8f11c98 100644 --- a/oa-openid/spec/omniauth/strategies/open_id_spec.rb +++ b/oa-openid/spec/omniauth/strategies/open_id_spec.rb @@ -1,65 +1,65 @@ -require File.dirname(__FILE__) + '/../../spec_helper' - -describe OmniAuth::Strategies::OpenID, :type => :strategy do - - include OmniAuth::Test::StrategyTestCase - - def strategy - [OmniAuth::Strategies::OpenID] - end - - describe '/auth/open_id without an identifier URL' do - before do - get '/auth/open_id' - end - - it 'should respond with OK' do - last_response.should be_ok - end - - it 'should respond with HTML' do - last_response.content_type.should == 'text/html' - end - - it 'should render an identifier URL input' do - last_response.body.should =~ %r{]*#{OmniAuth::Strategies::OpenID::IDENTIFIER_URL_PARAMETER}} - end - end - - describe '/auth/open_id with an identifier URL' do - before do - @identifier_url = 'http://me.example.org' - # TODO: change this mock to actually return some sort of OpenID response - stub_request(:get, @identifier_url) - get '/auth/open_id', {OmniAuth::Strategies::OpenID::IDENTIFIER_URL_PARAMETER => @identifier_url} - end - - it 'should redirect to the OpenID identity URL' do - last_response.should be_redirect - last_response.headers['Location'].should =~ %r{^#{@identifier_url}.*} - end - - it 'should tell the OpenID server to return to the callback URL' do - return_to = CGI.escape(last_request.url + '/callback') - last_response.headers['Location'].should =~ %r{[\?&]openid.return_to=#{return_to}} - end - - end - - describe 'followed by /auth/open_id/callback' do - before do - @identifier_url = 'http://me.example.org' - # TODO: change this mock to actually return some sort of OpenID response - stub_request(:get, @identifier_url) - get '/auth/open_id/callback' - end - - sets_an_auth_hash - sets_provider_to 'open_id' - sets_uid_to 'http://me.example.org' - - it 'should call through to the master app' do - last_response.body.should == 'true' - end - end -end +# require File.dirname(__FILE__) + '/../../spec_helper' +# +# describe OmniAuth::Strategies::OpenID, :type => :strategy do +# +# include OmniAuth::Test::StrategyTestCase +# +# def strategy +# [OmniAuth::Strategies::OpenID] +# end +# +# describe '/auth/open_id without an identifier URL' do +# before do +# get '/auth/open_id' +# end +# +# it 'should respond with OK' do +# last_response.should be_ok +# end +# +# it 'should respond with HTML' do +# last_response.content_type.should == 'text/html' +# end +# +# it 'should render an identifier URL input' do +# last_response.body.should =~ %r{]*#{OmniAuth::Strategies::OpenID::IDENTIFIER_URL_PARAMETER}} +# end +# end +# +# describe '/auth/open_id with an identifier URL' do +# before do +# @identifier_url = 'http://me.example.org' +# # TODO: change this mock to actually return some sort of OpenID response +# stub_request(:get, @identifier_url) +# get '/auth/open_id?openid_url=' + @identifier_url +# end +# +# it 'should redirect to the OpenID identity URL' do +# last_response.should be_redirect +# last_response.headers['Location'].should =~ %r{^#{@identifier_url}.*} +# end +# +# it 'should tell the OpenID server to return to the callback URL' do +# return_to = CGI.escape(last_request.url + '/callback') +# last_response.headers['Location'].should =~ %r{[\?&]openid.return_to=#{return_to}} +# end +# +# end +# +# describe 'followed by /auth/open_id/callback' do +# before do +# @identifier_url = 'http://me.example.org' +# # TODO: change this mock to actually return some sort of OpenID response +# stub_request(:get, @identifier_url) +# get '/auth/open_id/callback' +# end +# +# sets_an_auth_hash +# sets_provider_to 'open_id' +# sets_uid_to 'http://me.example.org' +# +# it 'should call through to the master app' do +# last_response.body.should == 'true' +# end +# end +# end diff --git a/omniauth/Gemfile.lock b/omniauth/Gemfile.lock index 5d3c76c..ca67bbd 100644 --- a/omniauth/Gemfile.lock +++ b/omniauth/Gemfile.lock @@ -1,52 +1,52 @@ PATH remote: /Users/mbleigh/gems/omniauth/oa-basic specs: - oa-basic (0.0.4) + oa-basic (0.0.5) multi_json (~> 0.0.2) nokogiri (~> 1.4.2) - oa-core (= 0.0.4) + oa-core (= 0.0.5) rest-client (~> 1.6.0) PATH remote: /Users/mbleigh/gems/omniauth/oa-core specs: - oa-core (0.0.4) + oa-core (0.0.5) rack (~> 1.1) PATH remote: /Users/mbleigh/gems/omniauth/oa-enterprise specs: - oa-enterprise (0.0.4) + oa-enterprise (0.0.5) nokogiri (~> 1.4.2) - oa-core (= 0.0.4) + oa-core (= 0.0.5) PATH remote: /Users/mbleigh/gems/omniauth/oa-oauth specs: - oa-oauth (0.0.4) + oa-oauth (0.0.5) multi_json (~> 0.0.2) nokogiri (~> 1.4.2) - oa-core (= 0.0.4) + oa-core (= 0.0.5) oauth (~> 0.4.0) oauth2 (~> 0.0.10) PATH remote: /Users/mbleigh/gems/omniauth/oa-openid specs: - oa-openid (0.0.4) - oa-core (= 0.0.4) + oa-openid (0.0.5) + oa-core (= 0.0.5) rack-openid (~> 1.1.1) ruby-openid-apps-discovery PATH remote: . specs: - omniauth (0.0.4) - oa-basic (= 0.0.4) - oa-core (= 0.0.4) - oa-enterprise (= 0.0.4) - oa-oauth (= 0.0.4) - oa-openid (= 0.0.4) + omniauth (0.0.5) + oa-basic (= 0.0.5) + oa-core (= 0.0.5) + oa-enterprise (= 0.0.5) + oa-oauth (= 0.0.5) + oa-openid (= 0.0.5) GEM remote: http://rubygems.org/ @@ -62,8 +62,8 @@ GEM mime-types (1.16) multi_json (0.0.4) nokogiri (1.4.3.1) - oauth (0.4.2) - oauth2 (0.0.10) + oauth (0.4.3) + oauth2 (0.0.13) faraday (~> 0.4.1) multi_json (>= 0.0.4) rack (1.2.1)