oauth2 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ module OAuth2
2
+ class Error < StandardError
3
+ attr_reader :response, :code, :description
4
+
5
+ # standard error values include:
6
+ # :invalid_request, :invalid_client, :invalid_token, :invalid_grant, :unsupported_grant_type, :invalid_scope
7
+ def initialize(response)
8
+ response.error = self
9
+ @response = response
10
+ if response.parsed.is_a?(Hash)
11
+ @code = response.parsed['error']
12
+ @description = response.parsed['error_description']
13
+ end
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,90 @@
1
+ require 'multi_json'
2
+
3
+ module OAuth2
4
+ # OAuth2::Response class
5
+ class Response
6
+ attr_reader :response
7
+ attr_accessor :error, :options
8
+
9
+ # Adds a new content type parser.
10
+ #
11
+ # @param [Symbol] key A descriptive symbol key such as :json or :query.
12
+ # @param [Array] One or more mime types to which this parser applies.
13
+ # @yield [String] A block returning parsed content.
14
+ def self.register_parser(key, mime_types, &block)
15
+ key = key.to_sym
16
+ PARSERS[key] = block
17
+ Array(mime_types).each do |mime_type|
18
+ CONTENT_TYPES[mime_type] = key
19
+ end
20
+ end
21
+
22
+ # Initializes a Response instance
23
+ #
24
+ # @param [Faraday::Response] response The Faraday response instance
25
+ # @param [Hash] opts options in which to initialize the instance
26
+ # @option opts [Symbol] :parse (:automatic) how to parse the response body. one of :url (for x-www-form-urlencoded),
27
+ # :json, or :automatic (determined by Content-Type response header)
28
+ def initialize(response, opts={})
29
+ @response = response
30
+ @options = {:parse => :automatic}.merge(opts)
31
+ end
32
+
33
+ # The HTTP response headers
34
+ def headers
35
+ response.headers
36
+ end
37
+
38
+ # The HTTP response status code
39
+ def status
40
+ response.status
41
+ end
42
+
43
+ # The HTTP resposne body
44
+ def body
45
+ response.body || ''
46
+ end
47
+
48
+ # Procs that, when called, will parse a response body according
49
+ # to the specified format.
50
+ PARSERS = {
51
+ :json => lambda{|body| MultiJson.decode(body) rescue body },
52
+ :query => lambda{|body| Rack::Utils.parse_query(body) },
53
+ :text => lambda{|body| body}
54
+ }
55
+
56
+ # Content type assignments for various potential HTTP content types.
57
+ CONTENT_TYPES = {
58
+ 'application/json' => :json,
59
+ 'text/javascript' => :json,
60
+ 'application/x-www-form-urlencoded' => :query,
61
+ 'text/plain' => :text
62
+ }
63
+
64
+ # The parsed response body.
65
+ # Will attempt to parse application/x-www-form-urlencoded and
66
+ # application/json Content-Type response bodies
67
+ def parsed
68
+ return nil unless PARSERS.key?(parser)
69
+ @parsed ||= PARSERS[parser].call(body)
70
+ end
71
+
72
+ # Attempts to determine the content type of the response.
73
+ def content_type
74
+ ((response.headers.values_at('content-type', 'Content-Type').compact.first || '').split(';').first || '').strip
75
+ end
76
+
77
+ # Determines the parser that will be used to supply the content of #parsed
78
+ def parser
79
+ return options[:parse].to_sym if PARSERS.key?(options[:parse])
80
+ CONTENT_TYPES[content_type]
81
+ end
82
+ end
83
+ end
84
+
85
+ begin
86
+ require 'multi_xml'
87
+ OAuth2::Response.register_parser(:xml, ['text/xml', 'application/rss+xml', 'application/rdf+xml', 'application/atom+xml']) do |body|
88
+ MultiXml.parse(body) rescue body
89
+ end
90
+ rescue LoadError; end
@@ -0,0 +1,32 @@
1
+ module OAuth2
2
+ module Strategy
3
+ # The Authorization Code Strategy
4
+ #
5
+ # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.1
6
+ class AuthCode < Base
7
+ # The required query parameters for the authorize URL
8
+ #
9
+ # @param [Hash] params additional query parameters
10
+ def authorize_params(params={})
11
+ params.merge('response_type' => 'code', 'client_id' => @client.id)
12
+ end
13
+
14
+ # The authorization URL endpoint of the provider
15
+ #
16
+ # @param [Hash] params additional query parameters for the URL
17
+ def authorize_url(params={})
18
+ @client.authorize_url(authorize_params.merge(params))
19
+ end
20
+
21
+ # Retrieve an access token given the specified validation code.
22
+ #
23
+ # @param [String] code The Authorization Code value
24
+ # @param [Hash] params additional params
25
+ # @note that you must also provide a :redirect_uri with most OAuth 2.0 providers
26
+ def get_token(code, params={})
27
+ params = {'grant_type' => 'authorization_code', 'code' => code}.merge(client_params).merge(params)
28
+ @client.get_token(params)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,37 +1,15 @@
1
1
  module OAuth2
2
2
  module Strategy
3
- class Base #:nodoc:
4
- def initialize(client)#:nodoc:
3
+ class Base
4
+ def initialize(client)
5
5
  @client = client
6
6
  end
7
7
 
8
- def authorize_url(options={}) #:nodoc:
9
- @client.authorize_url(authorize_params(options))
10
- end
11
-
12
- def authorize_params(options={}) #:nodoc:
13
- options = options.inject({}){|h, (k, v)| h[k.to_s] = v; h}
14
- {'client_id' => @client.id}.merge(options)
15
- end
16
-
17
- def access_token_url(options={})
18
- @client.access_token_url(access_token_params(options))
19
- end
20
-
21
- def access_token_params(options={})
22
- return default_params(options)
23
- end
24
-
25
- def refresh_token_params(options={})
26
- return default_params(options)
27
- end
28
-
29
- private
30
- def default_params(options={})
31
- {
32
- 'client_id' => @client.id,
33
- 'client_secret' => @client.secret
34
- }.merge(options)
8
+ # The OAuth client_id and client_secret
9
+ #
10
+ # @return [Hash]
11
+ def client_params
12
+ {'client_id' => @client.id, 'client_secret' => @client.secret}
35
13
  end
36
14
  end
37
15
  end
@@ -1,37 +1,26 @@
1
- require 'multi_json'
2
-
3
1
  module OAuth2
4
2
  module Strategy
3
+ # The Resource Owner Password Credentials Authorization Strategy
4
+ #
5
+ # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.3
5
6
  class Password < Base
7
+ # Not used for this strategy
8
+ #
9
+ # @raise [NotImplementedError]
6
10
  def authorize_url
7
11
  raise NotImplementedError, "The authorization endpoint is not used in this strategy"
8
12
  end
9
- # Retrieve an access token given the specified validation code.
10
- # Note that you must also provide a <tt>:redirect_uri</tt> option
11
- # in order to successfully verify your request for most OAuth 2.0
12
- # endpoints.
13
- def get_access_token(username, password, options={})
14
- response = @client.request(:post, @client.access_token_url, access_token_params(username, password, options))
15
-
16
- params = MultiJson.decode(response) rescue nil
17
- # the ActiveSupport JSON parser won't cause an exception when
18
- # given a formencoded string, so make sure that it was
19
- # actually parsed in an Hash. This covers even the case where
20
- # it caused an exception since it'll still be nil.
21
- params = Rack::Utils.parse_query(response) unless params.is_a? Hash
22
-
23
- access = params.delete('access_token')
24
- refresh = params.delete('refresh_token')
25
- expires_in = params.delete('expires_in')
26
- OAuth2::AccessToken.new(@client, access, refresh, expires_in, params)
27
- end
28
13
 
29
- def access_token_params(username, password, options={}) #:nodoc:
30
- super(options).merge({
31
- 'grant_type' => 'password',
32
- 'username' => username,
33
- 'password' => password
34
- })
14
+ # Retrieve an access token given the specified End User username and password.
15
+ #
16
+ # @param [String] username the End User username
17
+ # @param [String] password the End User password
18
+ # @param [Hash] params additional params
19
+ def get_token(username, password, params={})
20
+ params = {'grant_type' => 'password',
21
+ 'username' => username,
22
+ 'password' => password}.merge(client_params).merge(params)
23
+ @client.get_token(params)
35
24
  end
36
25
  end
37
26
  end
@@ -1,3 +1,3 @@
1
1
  module OAuth2
2
- VERSION = "0.4.1"
2
+ VERSION = "0.5.0"
3
3
  end
data/oauth2.gemspec CHANGED
@@ -1,24 +1,26 @@
1
1
  # -*- encoding: utf-8 -*-
2
- require File.expand_path("../lib/oauth2/version", __FILE__)
2
+ require File.expand_path('../lib/oauth2/version', __FILE__)
3
3
 
4
- Gem::Specification.new do |s|
5
- s.name = "oauth2"
6
- s.version = OAuth2::VERSION
7
- s.required_rubygems_version = Gem::Requirement.new(">= 1.3.6") if s.respond_to? :required_rubygems_version=
8
- s.authors = ["Michael Bleigh"]
9
- s.description = %q{A Ruby wrapper for the OAuth 2.0 protocol built with a similar style to the original OAuth gem.}
10
- s.summary = %q{A Ruby wrapper for the OAuth 2.0 protocol.}
11
- s.email = "michael@intridea.com"
12
- s.homepage = "http://github.com/intridea/oauth2"
13
- s.require_paths = ["lib"]
14
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
15
- s.files = `git ls-files`.split("\n")
16
- s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
- s.add_runtime_dependency("faraday", "~> 0.6.1")
18
- s.add_runtime_dependency("multi_json", ">= 0.0.5")
19
- s.add_development_dependency("json_pure", "~> 1.5")
20
- s.add_development_dependency("rake", "~> 0.8")
21
- s.add_development_dependency("simplecov", "~> 0.4")
22
- s.add_development_dependency("rspec", "~> 2.5")
23
- s.add_development_dependency("ZenTest", "~> 4.5")
4
+ Gem::Specification.new do |gem|
5
+ gem.add_development_dependency 'rake', '~> 0.9'
6
+ gem.add_development_dependency 'rdoc', '~> 3.6'
7
+ gem.add_development_dependency 'rspec', '~> 2.6'
8
+ gem.add_development_dependency 'simplecov', '~> 0.4'
9
+ gem.add_development_dependency 'yard', '~> 0.7'
10
+ gem.add_development_dependency 'ZenTest', '~> 4.5'
11
+ gem.add_development_dependency 'multi_xml'
12
+ gem.add_runtime_dependency 'faraday', ['>= 0.6.1', '< 0.8']
13
+ gem.add_runtime_dependency 'multi_json', '~> 1.0.0'
14
+ gem.authors = ["Michael Bleigh", "Erik Michaels-Ober"]
15
+ gem.description = %q{A Ruby wrapper for the OAuth 2.0 protocol built with a similar style to the original OAuth gem.}
16
+ gem.email = ['michael@intridea.com', 'sferik@gmail.com']
17
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ gem.files = `git ls-files`.split("\n")
19
+ gem.homepage = 'http://github.com/intridea/oauth2'
20
+ gem.name = 'oauth2'
21
+ gem.require_paths = ['lib']
22
+ gem.required_rubygems_version = Gem::Requirement.new('>= 1.3.6')
23
+ gem.summary = %q{A Ruby wrapper for the OAuth 2.0 protocol.}
24
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
25
+ gem.version = OAuth2::VERSION
24
26
  end
data/spec/helper.rb ADDED
@@ -0,0 +1,13 @@
1
+ $:.unshift File.expand_path('..', __FILE__)
2
+ $:.unshift File.expand_path('../../lib', __FILE__)
3
+ require 'simplecov'
4
+ SimpleCov.start
5
+ require 'oauth2'
6
+ require 'rspec'
7
+ require 'rspec/autorun'
8
+
9
+ Faraday.default_adapter = :test
10
+
11
+ RSpec.configure do |conf|
12
+ include OAuth2
13
+ end
@@ -1,90 +1,144 @@
1
- require 'spec_helper'
1
+ require 'helper'
2
2
 
3
- describe OAuth2::AccessToken do
3
+ VERBS = [:get, :post, :put, :delete]
4
+
5
+ describe AccessToken do
6
+ let(:token) {'monkey'}
7
+ let(:token_body) {MultiJson.encode(:access_token => 'foo', :expires_in => 600, :refresh_token => 'bar')}
8
+ let(:refresh_body) {MultiJson.encode(:access_token => 'refreshed_foo', :expires_in => 600, :refresh_token => 'refresh_bar')}
4
9
  let(:client) do
5
- cli = OAuth2::Client.new('abc', 'def', :site => 'https://api.example.com')
6
- cli.connection.build do |b|
7
- b.adapter :test do |stub|
8
- stub.get('/client?oauth_token=monkey') {|env| [200, {}, 'get']}
9
- stub.post('/client') {|env| [200, {}, 'oauth_token=' << env[:body]['oauth_token']]}
10
- stub.get('/empty_get?oauth_token=monkey') {|env| [204, {}, nil]}
11
- stub.put('/client') {|env| [200, {}, 'oauth_token=' << env[:body]['oauth_token']]}
12
- stub.delete('/client?oauth_token=monkey') {|env| [200, {}, 'delete']}
10
+ Client.new('abc', 'def', :site => 'https://api.example.com') do |builder|
11
+ builder.request :url_encoded
12
+ builder.adapter :test do |stub|
13
+ VERBS.each do |verb|
14
+ stub.send(verb, '/token/header') {|env| [200, {}, env[:request_headers]['Authorization']]}
15
+ stub.send(verb, "/token/query?bearer_token=#{token}") {|env| [200, {}, Addressable::URI.parse(env[:url]).query_values['bearer_token']]}
16
+ stub.send(verb, '/token/body') {|env| [200, {}, env[:body]]}
17
+ end
18
+ stub.post('/oauth/token') {|env| [200, {'Content-Type' => 'application/json'}, refresh_body]}
13
19
  end
14
20
  end
15
- cli
16
21
  end
17
22
 
18
- let(:token) {'monkey'}
19
-
20
- subject {OAuth2::AccessToken.new(client, token)}
23
+ subject {AccessToken.new(client, token)}
21
24
 
22
25
  describe '#initialize' do
23
- it 'should assign client and token' do
26
+ it 'assigns client and token' do
24
27
  subject.client.should == client
25
28
  subject.token.should == token
26
29
  end
27
30
 
28
- it 'should assign extra params' do
29
- target = OAuth2::AccessToken.new(client, token, nil, nil, {'foo' => 'bar'})
31
+ it 'assigns extra params' do
32
+ target = AccessToken.new(client, token, 'foo' => 'bar')
30
33
  target.params.should include('foo')
31
34
  target.params['foo'].should == 'bar'
32
35
  end
33
36
 
34
- %w(get delete).each do |http_method|
35
- it "makes #{http_method.upcase} requests with access token" do
36
- subject.send(http_method.to_sym, 'client').should == http_method
37
- end
37
+ def assert_initialized_token(target)
38
+ target.token.should eq(token)
39
+ target.should be_expires
40
+ target.params.keys.should include('foo')
41
+ target.params['foo'].should == 'bar'
38
42
  end
39
43
 
40
- %w(post put).each do |http_method|
41
- it "makes #{http_method.upcase} requests with access token" do
42
- subject.send(http_method.to_sym, 'client').should == 'oauth_token=monkey'
43
- end
44
+ it 'initializes with a Hash' do
45
+ hash = {:access_token => token, :expires_at => Time.now.to_i + 200, 'foo' => 'bar'}
46
+ target = AccessToken.from_hash(client, hash)
47
+ assert_initialized_token(target)
44
48
  end
45
-
46
- it "works with a null response body" do
47
- subject.get('empty_get').should == ''
49
+
50
+ it 'initalizes with a form-urlencoded key/value string' do
51
+ kvform = "access_token=#{token}&expires_at=#{Time.now.to_i+200}&foo=bar"
52
+ target = AccessToken.from_kvform(client, kvform)
53
+ assert_initialized_token(target)
54
+ end
55
+
56
+ it 'sets options' do
57
+ target = AccessToken.new(client, token, :param_name => 'foo', :header_format => 'Bearer %', :mode => :body)
58
+ target.options[:param_name].should == 'foo'
59
+ target.options[:header_format].should == 'Bearer %'
60
+ target.options[:mode].should == :body
48
61
  end
49
62
  end
50
63
 
51
- describe '#expires?' do
52
- it 'should be false if there is no expires_at' do
53
- OAuth2::AccessToken.new(client, token).should_not be_expires
64
+ describe '#request' do
65
+ context ':mode => :header' do
66
+ before :all do
67
+ subject.options[:mode] = :header
68
+ end
69
+
70
+ VERBS.each do |verb|
71
+ it "sends the token in the Authorization header for a #{verb.to_s.upcase} request" do
72
+ subject.post('/token/header').body.should include(token)
73
+ end
74
+ end
54
75
  end
55
76
 
56
- it 'should be true if there is an expires_at' do
57
- OAuth2::AccessToken.new(client, token, 'abaca', 600).should be_expires
77
+ context ':mode => :query' do
78
+ before :all do
79
+ subject.options[:mode] = :query
80
+ end
81
+
82
+ VERBS.each do |verb|
83
+ it "sends the token in the Authorization header for a #{verb.to_s.upcase} request" do
84
+ subject.post('/token/query').body.should == token
85
+ end
86
+ end
87
+ end
88
+
89
+ context ':mode => :body' do
90
+ before :all do
91
+ subject.options[:mode] = :body
92
+ end
93
+
94
+ VERBS.each do |verb|
95
+ it "sends the token in the Authorization header for a #{verb.to_s.upcase} request" do
96
+ subject.post('/token/body').body.split('=').last.should == token
97
+ end
98
+ end
58
99
  end
59
100
  end
60
101
 
61
- describe '#expires_at' do
62
- before do
63
- @now = Time.now
64
- Time.stub!(:now).and_return(@now)
102
+ describe '#expires?' do
103
+ it 'should be false if there is no expires_at' do
104
+ AccessToken.new(client, token).should_not be_expires
65
105
  end
66
106
 
67
- subject{OAuth2::AccessToken.new(client, token, 'abaca', 600)}
107
+ it 'should be true if there is an expires_in' do
108
+ AccessToken.new(client, token, :refresh_token => 'abaca', :expires_in => 600).should be_expires
109
+ end
68
110
 
69
- it 'should be a time representation of #expires_in' do
70
- subject.expires_at.should == (@now + 600)
111
+ it 'should be true if there is an expires_at' do
112
+ AccessToken.new(client, token, :refresh_token => 'abaca', :expires_in => Time.now.getutc.to_i+600).should be_expires
71
113
  end
72
114
  end
73
115
 
74
116
  describe '#expired?' do
75
- it 'should be false if there is no expires_at' do
76
- OAuth2::AccessToken.new(client, token).should_not be_expired
117
+ it 'should be false if there is no expires_in or expires_at' do
118
+ AccessToken.new(client, token).should_not be_expired
77
119
  end
78
120
 
79
- it 'should be false if expires_at is in the future' do
80
- OAuth2::AccessToken.new(client, token, 'abaca', 10800).should_not be_expired
121
+ it 'should be false if expires_in is in the future' do
122
+ AccessToken.new(client, token, :refresh_token => 'abaca', :expires_in => 10800).should_not be_expired
81
123
  end
82
124
 
83
125
  it 'should be true if expires_at is in the past' do
84
- access = OAuth2::AccessToken.new(client, token, 'abaca', 600)
126
+ access = AccessToken.new(client, token, :refresh_token => 'abaca', :expires_in => 600)
85
127
  @now = Time.now + 10800
86
128
  Time.stub!(:now).and_return(@now)
87
129
  access.should be_expired
88
130
  end
131
+
132
+ end
133
+
134
+ describe '#refresh!' do
135
+ it 'returns a refresh token with appropriate values carried over' do
136
+ access = AccessToken.new(client, token, :refresh_token => 'abaca',
137
+ :expires_in => 600,
138
+ :param_name => 'o_param')
139
+ refreshed = access.refresh!
140
+ access.client.should == refreshed.client
141
+ access.options[:param_name].should == refreshed.options[:param_name]
142
+ end
89
143
  end
90
144
  end