dreamwords-oauth2 0.8.1

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,24 @@
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
+
11
+ message = []
12
+
13
+ if response.parsed.is_a?(Hash)
14
+ @code = response.parsed['error']
15
+ @description = response.parsed['error_description']
16
+ message << "#{@code}: #{@description}"
17
+ end
18
+
19
+ message << response.body
20
+
21
+ super(message.join("\n"))
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,93 @@
1
+ require 'multi_json'
2
+ require 'rack'
3
+
4
+ module OAuth2
5
+ # OAuth2::Response class
6
+ class Response
7
+ attr_reader :response
8
+ attr_accessor :error, :options
9
+
10
+ # Adds a new content type parser.
11
+ #
12
+ # @param [Symbol] key A descriptive symbol key such as :json or :query.
13
+ # @param [Array] One or more mime types to which this parser applies.
14
+ # @yield [String] A block returning parsed content.
15
+ def self.register_parser(key, mime_types, &block)
16
+ key = key.to_sym
17
+ PARSERS[key] = block
18
+ Array(mime_types).each do |mime_type|
19
+ CONTENT_TYPES[mime_type] = key
20
+ end
21
+ end
22
+
23
+ # Initializes a Response instance
24
+ #
25
+ # @param [Faraday::Response] response The Faraday response instance
26
+ # @param [Hash] opts options in which to initialize the instance
27
+ # @option opts [Symbol] :parse (:automatic) how to parse the response body. one of :query (for x-www-form-urlencoded),
28
+ # :json, or :automatic (determined by Content-Type response header)
29
+ def initialize(response, opts={})
30
+ @response = response
31
+ @options = {:parse => :automatic}.merge(opts)
32
+ end
33
+
34
+ # The HTTP response headers
35
+ def headers
36
+ response.headers
37
+ end
38
+
39
+ # The HTTP response status code
40
+ def status
41
+ response.status
42
+ end
43
+
44
+ # The HTTP response body
45
+ def body
46
+ response.body || ''
47
+ end
48
+
49
+ # Procs that, when called, will parse a response body according
50
+ # to the specified format.
51
+ PARSERS = {
52
+ # Can't reliably detect whether MultiJson responds to load, since it's
53
+ # a reserved word. Use adapter as a proxy for new features.
54
+ :json => lambda{ |body| MultiJson.respond_to?(:adapter) ? MultiJson.load(body) : MultiJson.decode(body) rescue body },
55
+ :query => lambda{ |body| Rack::Utils.parse_query(body) },
56
+ :text => lambda{ |body| body }
57
+ }
58
+
59
+ # Content type assignments for various potential HTTP content types.
60
+ CONTENT_TYPES = {
61
+ 'application/json' => :json,
62
+ 'text/javascript' => :json,
63
+ 'application/x-www-form-urlencoded' => :query,
64
+ 'text/plain' => :text
65
+ }
66
+
67
+ # The parsed response body.
68
+ # Will attempt to parse application/x-www-form-urlencoded and
69
+ # application/json Content-Type response bodies
70
+ def parsed
71
+ return nil unless PARSERS.key?(parser)
72
+ @parsed ||= PARSERS[parser].call(body)
73
+ end
74
+
75
+ # Attempts to determine the content type of the response.
76
+ def content_type
77
+ ((response.headers.values_at('content-type', 'Content-Type').compact.first || '').split(';').first || '').strip
78
+ end
79
+
80
+ # Determines the parser that will be used to supply the content of #parsed
81
+ def parser
82
+ return options[:parse].to_sym if PARSERS.key?(options[:parse])
83
+ CONTENT_TYPES[content_type]
84
+ end
85
+ end
86
+ end
87
+
88
+ begin
89
+ require 'multi_xml'
90
+ OAuth2::Response.register_parser(:xml, ['text/xml', 'application/rss+xml', 'application/rdf+xml', 'application/atom+xml']) do |body|
91
+ MultiXml.parse(body) rescue body
92
+ end
93
+ rescue LoadError; end
@@ -0,0 +1,75 @@
1
+ require 'httpauth'
2
+ require 'jwt'
3
+
4
+ module OAuth2
5
+ module Strategy
6
+ # The Client Assertion Strategy
7
+ #
8
+ # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.1.3
9
+ #
10
+ # Sample usage:
11
+ # client = OAuth2::Client.new(client_id, client_secret,
12
+ # :site => 'http://localhost:8080')
13
+ #
14
+ # params = {:hmac_secret => "some secret",
15
+ # # or :private_key => "private key string",
16
+ # :iss => "http://localhost:3001",
17
+ # :prn => "me@here.com",
18
+ # :exp => Time.now.utc.to_i + 3600}
19
+ #
20
+ # access = client.assertion.get_token(params)
21
+ # access.token # actual access_token string
22
+ # access.get("/api/stuff") # making api calls with access token in header
23
+ #
24
+ class Assertion < Base
25
+ # Not used for this strategy
26
+ #
27
+ # @raise [NotImplementedError]
28
+ def authorize_url
29
+ raise NotImplementedError, "The authorization endpoint is not used in this strategy"
30
+ end
31
+
32
+ # Retrieve an access token given the specified client.
33
+ #
34
+ # @param [Hash] params assertion params
35
+ # pass either :hmac_secret or :private_key, but not both.
36
+ #
37
+ # params :hmac_secret, secret string.
38
+ # params :private_key, private key string.
39
+ #
40
+ # params :iss, issuer
41
+ # params :aud, audience, optional
42
+ # params :prn, principal, current user
43
+ # params :exp, expired at, in seconds, like Time.now.utc.to_i + 3600
44
+ #
45
+ # @param [Hash] opts options
46
+ def get_token(params={}, opts={})
47
+ hash = build_request(params)
48
+ @client.get_token(hash, opts.merge('refresh_token' => nil))
49
+ end
50
+
51
+ def build_request(params)
52
+ assertion = build_assertion(params)
53
+ {:grant_type => "assertion",
54
+ :assertion_type => "urn:ietf:params:oauth:grant-type:jwt-bearer",
55
+ :assertion => assertion,
56
+ :scope => params[:scope]
57
+ }.merge(client_params)
58
+ end
59
+
60
+ def build_assertion(params)
61
+ claims = {:iss => params[:iss],
62
+ :aud => params[:aud],
63
+ :prn => params[:prn],
64
+ :exp => params[:exp]
65
+ }
66
+ if params[:hmac_secret]
67
+ jwt_assertion = JWT.encode(claims, params[:hmac_secret], "HS256")
68
+ elsif params[:private_key]
69
+ jwt_assertion = JWT.encode(claims, params[:private_key], "RS256")
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+
@@ -0,0 +1,33 @@
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
+ # @param [Hash] opts options
26
+ # @note that you must also provide a :redirect_uri with most OAuth 2.0 providers
27
+ def get_token(code, params={}, opts={})
28
+ params = {'grant_type' => 'authorization_code', 'code' => code}.merge(client_params).merge(params)
29
+ @client.get_token(params, opts)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,16 @@
1
+ module OAuth2
2
+ module Strategy
3
+ class Base
4
+ def initialize(client)
5
+ @client = client
6
+ end
7
+
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}
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,28 @@
1
+ require 'httpauth'
2
+
3
+ module OAuth2
4
+ module Strategy
5
+ # The Client Credentials Strategy
6
+ #
7
+ # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.4
8
+ class ClientCredentials < Base
9
+ # Not used for this strategy
10
+ #
11
+ # @raise [NotImplementedError]
12
+ def authorize_url
13
+ raise NotImplementedError, "The authorization endpoint is not used in this strategy"
14
+ end
15
+
16
+ # Retrieve an access token given the specified client.
17
+ #
18
+ # @param [Hash] params additional params
19
+ # @param [Hash] opts options
20
+ def get_token(params={}, opts={})
21
+ request_body = opts.delete('auth_scheme') == 'request_body'
22
+ params.merge!('grant_type' => 'client_credentials')
23
+ params.merge!(request_body ? client_params : {:headers => {'Authorization' => HTTPAuth::Basic.pack_authorization(client_params['client_id'], client_params['client_secret'])}})
24
+ @client.get_token(params, opts.merge('refresh_token' => nil))
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,29 @@
1
+ module OAuth2
2
+ module Strategy
3
+ # The Implicit Strategy
4
+ #
5
+ # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-26#section-4.2
6
+ class Implicit < 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' => 'token', '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
+ # Not used for this strategy
22
+ #
23
+ # @raise [NotImplementedError]
24
+ def get_token(*)
25
+ raise NotImplementedError, "The token is accessed differently in this strategy"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,27 @@
1
+ module OAuth2
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
6
+ class Password < Base
7
+ # Not used for this strategy
8
+ #
9
+ # @raise [NotImplementedError]
10
+ def authorize_url
11
+ raise NotImplementedError, "The authorization endpoint is not used in this strategy"
12
+ end
13
+
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={}, opts={})
20
+ params = {'grant_type' => 'password',
21
+ 'username' => username,
22
+ 'password' => password}.merge(client_params).merge(params)
23
+ @client.get_token(params, opts)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,18 @@
1
+ module OAuth2
2
+ class Version
3
+ MAJOR = 0 unless defined? MAJOR
4
+ MINOR = 8 unless defined? MINOR
5
+ PATCH = 1 unless defined? PATCH
6
+ PRE = nil unless defined? PRE
7
+
8
+ class << self
9
+
10
+ # @return [String]
11
+ def to_s
12
+ [MAJOR, MINOR, PATCH, PRE].compact.join('.')
13
+ end
14
+
15
+ end
16
+
17
+ end
18
+ end
data/oauth2.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # encoding: utf-8
2
+ require File.expand_path('../lib/oauth2/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.add_dependency 'faraday', '~> 0.8'
6
+ gem.add_dependency 'httpauth', '~> 0.1'
7
+ gem.add_dependency 'multi_json', '~> 1.0'
8
+ gem.add_dependency 'rack', '~> 1.1.6'
9
+ gem.add_dependency 'jwt', '~> 0.1.4'
10
+ gem.add_development_dependency 'addressable'
11
+ gem.add_development_dependency 'multi_xml'
12
+ gem.add_development_dependency 'rake'
13
+ gem.add_development_dependency 'rdoc'
14
+ gem.add_development_dependency 'rspec'
15
+ gem.add_development_dependency 'simplecov'
16
+ gem.authors = ["Michael Bleigh", "Erik Michaels-Ober"]
17
+ gem.description = %q{A Ruby wrapper for the OAuth 2.0 protocol built with a similar style to the original OAuth gem.}
18
+ gem.email = ['michael@intridea.com', 'sferik@gmail.com']
19
+ gem.files = `git ls-files`.split("\n")
20
+ gem.homepage = 'http://github.com/dreamwords/oauth2'
21
+ gem.name = 'dreamwords-oauth2'
22
+ gem.require_paths = ['lib']
23
+ gem.required_rubygems_version = Gem::Requirement.new('>= 1.3.6')
24
+ gem.summary = %q{A Ruby wrapper for the OAuth 2.0 protocol.}
25
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
26
+ gem.version = OAuth2::Version
27
+ end
data/spec/helper.rb ADDED
@@ -0,0 +1,16 @@
1
+ unless ENV['CI']
2
+ require 'simplecov'
3
+ SimpleCov.start do
4
+ add_filter 'spec'
5
+ end
6
+ end
7
+ require 'oauth2'
8
+ require 'addressable/uri'
9
+ require 'rspec'
10
+ require 'rspec/autorun'
11
+
12
+ Faraday.default_adapter = :test
13
+
14
+ RSpec.configure do |conf|
15
+ include OAuth2
16
+ end
@@ -0,0 +1,151 @@
1
+ require 'helper'
2
+
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')}
9
+ let(:client) do
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]}
19
+ end
20
+ end
21
+ end
22
+
23
+ subject {AccessToken.new(client, token)}
24
+
25
+ describe '#initialize' do
26
+ it 'assigns client and token' do
27
+ subject.client.should == client
28
+ subject.token.should == token
29
+ end
30
+
31
+ it 'assigns extra params' do
32
+ target = AccessToken.new(client, token, 'foo' => 'bar')
33
+ target.params.should include('foo')
34
+ target.params['foo'].should == 'bar'
35
+ end
36
+
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'
42
+ end
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)
48
+ end
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
61
+ end
62
+
63
+ it "initializes with a string expires_at" do
64
+ hash = {:access_token => token, :expires_at => '1361396829', 'foo' => 'bar'}
65
+ target = AccessToken.from_hash(client, hash)
66
+ assert_initialized_token(target)
67
+ expect(target.expires_at).to be_a(Integer)
68
+ end
69
+ end
70
+
71
+ describe '#request' do
72
+ context ':mode => :header' do
73
+ before :all do
74
+ subject.options[:mode] = :header
75
+ end
76
+
77
+ VERBS.each do |verb|
78
+ it "sends the token in the Authorization header for a #{verb.to_s.upcase} request" do
79
+ subject.post('/token/header').body.should include(token)
80
+ end
81
+ end
82
+ end
83
+
84
+ context ':mode => :query' do
85
+ before :all do
86
+ subject.options[:mode] = :query
87
+ end
88
+
89
+ VERBS.each do |verb|
90
+ it "sends the token in the Authorization header for a #{verb.to_s.upcase} request" do
91
+ subject.post('/token/query').body.should == token
92
+ end
93
+ end
94
+ end
95
+
96
+ context ':mode => :body' do
97
+ before :all do
98
+ subject.options[:mode] = :body
99
+ end
100
+
101
+ VERBS.each do |verb|
102
+ it "sends the token in the Authorization header for a #{verb.to_s.upcase} request" do
103
+ subject.post('/token/body').body.split('=').last.should == token
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ describe '#expires?' do
110
+ it 'should be false if there is no expires_at' do
111
+ AccessToken.new(client, token).should_not be_expires
112
+ end
113
+
114
+ it 'should be true if there is an expires_in' do
115
+ AccessToken.new(client, token, :refresh_token => 'abaca', :expires_in => 600).should be_expires
116
+ end
117
+
118
+ it 'should be true if there is an expires_at' do
119
+ AccessToken.new(client, token, :refresh_token => 'abaca', :expires_in => Time.now.getutc.to_i+600).should be_expires
120
+ end
121
+ end
122
+
123
+ describe '#expired?' do
124
+ it 'should be false if there is no expires_in or expires_at' do
125
+ AccessToken.new(client, token).should_not be_expired
126
+ end
127
+
128
+ it 'should be false if expires_in is in the future' do
129
+ AccessToken.new(client, token, :refresh_token => 'abaca', :expires_in => 10800).should_not be_expired
130
+ end
131
+
132
+ it 'should be true if expires_at is in the past' do
133
+ access = AccessToken.new(client, token, :refresh_token => 'abaca', :expires_in => 600)
134
+ @now = Time.now + 10800
135
+ Time.stub!(:now).and_return(@now)
136
+ access.should be_expired
137
+ end
138
+
139
+ end
140
+
141
+ describe '#refresh!' do
142
+ it 'returns a refresh token with appropriate values carried over' do
143
+ access = AccessToken.new(client, token, :refresh_token => 'abaca',
144
+ :expires_in => 600,
145
+ :param_name => 'o_param')
146
+ refreshed = access.refresh!
147
+ access.client.should == refreshed.client
148
+ access.options[:param_name].should == refreshed.options[:param_name]
149
+ end
150
+ end
151
+ end