oauth2 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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