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.
- data/.autotest +1 -0
- data/.gitignore +4 -1
- data/.travis.yml +1 -3
- data/Gemfile +1 -1
- data/README.md +62 -57
- data/Rakefile +3 -1
- data/lib/oauth2.rb +3 -9
- data/lib/oauth2/access_token.rb +122 -24
- data/lib/oauth2/client.rb +115 -74
- data/lib/oauth2/error.rb +17 -0
- data/lib/oauth2/response.rb +90 -0
- data/lib/oauth2/strategy/auth_code.rb +32 -0
- data/lib/oauth2/strategy/base.rb +7 -29
- data/lib/oauth2/strategy/password.rb +16 -27
- data/lib/oauth2/version.rb +1 -1
- data/oauth2.gemspec +23 -21
- data/spec/helper.rb +13 -0
- data/spec/oauth2/access_token_spec.rb +99 -45
- data/spec/oauth2/client_spec.rb +81 -69
- data/spec/oauth2/response_spec.rb +90 -0
- data/spec/oauth2/strategy/auth_code_spec.rb +88 -0
- data/spec/oauth2/strategy/base_spec.rb +1 -1
- data/spec/oauth2/strategy/password_spec.rb +7 -7
- metadata +114 -90
- data/CHANGELOG.md +0 -21
- data/lib/oauth2/response_object.rb +0 -58
- data/lib/oauth2/strategy/web_server.rb +0 -58
- data/spec/oauth2/strategy/web_server_spec.rb +0 -138
- data/spec/spec_helper.rb +0 -11
data/lib/oauth2/error.rb
ADDED
@@ -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
|
data/lib/oauth2/strategy/base.rb
CHANGED
@@ -1,37 +1,15 @@
|
|
1
1
|
module OAuth2
|
2
2
|
module Strategy
|
3
|
-
class Base
|
4
|
-
def initialize(client)
|
3
|
+
class Base
|
4
|
+
def initialize(client)
|
5
5
|
@client = client
|
6
6
|
end
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
data/lib/oauth2/version.rb
CHANGED
data/oauth2.gemspec
CHANGED
@@ -1,24 +1,26 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
|
-
require File.expand_path(
|
2
|
+
require File.expand_path('../lib/oauth2/version', __FILE__)
|
3
3
|
|
4
|
-
Gem::Specification.new do |
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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 '
|
1
|
+
require 'helper'
|
2
2
|
|
3
|
-
|
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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
19
|
-
|
20
|
-
subject {OAuth2::AccessToken.new(client, token)}
|
23
|
+
subject {AccessToken.new(client, token)}
|
21
24
|
|
22
25
|
describe '#initialize' do
|
23
|
-
it '
|
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 '
|
29
|
-
target =
|
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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
47
|
-
|
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 '#
|
52
|
-
|
53
|
-
|
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
|
-
|
57
|
-
|
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 '#
|
62
|
-
|
63
|
-
|
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
|
-
|
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
|
70
|
-
|
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
|
-
|
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
|
80
|
-
|
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 =
|
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
|