oauth2-aptible 0.9.4.aptible

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,90 @@
1
+ require 'multi_json'
2
+ require 'multi_xml'
3
+ require 'rack'
4
+
5
+ module OAuth2
6
+ # OAuth2::Response class
7
+ class Response
8
+ attr_reader :response
9
+ attr_accessor :error, :options
10
+
11
+ # Adds a new content type parser.
12
+ #
13
+ # @param [Symbol] key A descriptive symbol key such as :json or :query.
14
+ # @param [Array] One or more mime types to which this parser applies.
15
+ # @yield [String] A block returning parsed content.
16
+ def self.register_parser(key, mime_types, &block)
17
+ key = key.to_sym
18
+ PARSERS[key] = block
19
+ Array(mime_types).each do |mime_type|
20
+ CONTENT_TYPES[mime_type] = key
21
+ end
22
+ end
23
+
24
+ # Initializes a Response instance
25
+ #
26
+ # @param [Faraday::Response] response The Faraday response instance
27
+ # @param [Hash] opts options in which to initialize the instance
28
+ # @option opts [Symbol] :parse (:automatic) how to parse the response body. one of :query (for x-www-form-urlencoded),
29
+ # :json, or :automatic (determined by Content-Type response header)
30
+ def initialize(response, opts = {})
31
+ @response = response
32
+ @options = {:parse => :automatic}.merge(opts)
33
+ end
34
+
35
+ # The HTTP response headers
36
+ def headers
37
+ response.headers
38
+ end
39
+
40
+ # The HTTP response status code
41
+ def status
42
+ response.status
43
+ end
44
+
45
+ # The HTTP response body
46
+ def body
47
+ response.body || ''
48
+ end
49
+
50
+ # Procs that, when called, will parse a response body according
51
+ # to the specified format.
52
+ PARSERS = {
53
+ :query => lambda { |body| Rack::Utils.parse_query(body) },
54
+ :text => lambda { |body| body }
55
+ }
56
+
57
+ # Content type assignments for various potential HTTP content types.
58
+ CONTENT_TYPES = {
59
+ 'application/x-www-form-urlencoded' => :query,
60
+ 'text/plain' => :text
61
+ }
62
+
63
+ # The parsed response body.
64
+ # Will attempt to parse application/x-www-form-urlencoded and
65
+ # application/json Content-Type response bodies
66
+ def parsed
67
+ return nil unless PARSERS.key?(parser)
68
+ @parsed ||= PARSERS[parser].call(body)
69
+ end
70
+
71
+ # Attempts to determine the content type of the response.
72
+ def content_type
73
+ ((response.headers.values_at('content-type', 'Content-Type').compact.first || '').split(';').first || '').strip
74
+ end
75
+
76
+ # Determines the parser that will be used to supply the content of #parsed
77
+ def parser
78
+ return options[:parse].to_sym if PARSERS.key?(options[:parse])
79
+ CONTENT_TYPES[content_type]
80
+ end
81
+ end
82
+ end
83
+
84
+ OAuth2::Response.register_parser(:xml, ['text/xml', 'application/rss+xml', 'application/rdf+xml', 'application/atom+xml']) do |body|
85
+ MultiXml.parse(body) rescue body # rubocop:disable RescueModifier
86
+ end
87
+
88
+ OAuth2::Response.register_parser(:json, ['application/json', 'text/javascript', 'application/hal+json']) do |body|
89
+ MultiJson.load(body) rescue body # rubocop:disable RescueModifier
90
+ end
@@ -0,0 +1,72 @@
1
+ require 'jwt'
2
+
3
+ module OAuth2
4
+ module Strategy
5
+ # The Client Assertion Strategy
6
+ #
7
+ # @see http://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-07#section-3.1
8
+ #
9
+ # Sample usage:
10
+ # client = OAuth2::Client.new(client_id, client_secret,
11
+ # :site => 'http://localhost:8080')
12
+ #
13
+ # params = {:hmac_secret => "some secret",
14
+ # # or :private_key => "private key string",
15
+ # :iss => "http://localhost:3001",
16
+ # :sub => "me@here.com",
17
+ # :exp => Time.now.utc.to_i + 3600}
18
+ #
19
+ # access = client.assertion.get_token(params)
20
+ # access.token # actual access_token string
21
+ # access.get("/api/stuff") # making api calls with access token in header
22
+ #
23
+ class Assertion < Base
24
+ # Not used for this strategy
25
+ #
26
+ # @raise [NotImplementedError]
27
+ def authorize_url
28
+ fail(NotImplementedError, 'The authorization endpoint is not used in this strategy')
29
+ end
30
+
31
+ # Retrieve an access token given the specified client.
32
+ #
33
+ # @param [Hash] params assertion params
34
+ # pass either :hmac_secret or :private_key, but not both.
35
+ #
36
+ # params :hmac_secret, secret string.
37
+ # params :private_key, private key string.
38
+ #
39
+ # params :iss, issuer
40
+ # params :aud, audience, optional
41
+ # params :sub, principal, current user
42
+ # params :exp, expired at, in seconds, like Time.now.utc.to_i + 3600
43
+ #
44
+ # @param [Hash] opts options
45
+ def get_token(params = {}, opts = {})
46
+ hash = build_request(params)
47
+ @client.get_token(hash, opts.merge('refresh_token' => nil))
48
+ end
49
+
50
+ def build_request(params)
51
+ assertion = build_assertion(params)
52
+ {:grant_type => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
53
+ :assertion => assertion,
54
+ :scope => params[:scope]
55
+ }.merge(client_params)
56
+ end
57
+
58
+ def build_assertion(params)
59
+ claims = {:iss => params[:iss],
60
+ :aud => params[:aud],
61
+ :sub => params[:sub],
62
+ :exp => params[:exp]
63
+ }
64
+ if params[:hmac_secret]
65
+ JWT.encode(claims, params[:hmac_secret], params[:algorithm] || 'HS256')
66
+ elsif params[:private_key]
67
+ JWT.encode(claims, params[:private_key], params[:algorithm] || 'RS256')
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -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,36 @@
1
+ require 'base64'
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
+ fail(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' => authorization(client_params['client_id'], client_params['client_secret'])}})
24
+ @client.get_token(params, opts.merge('refresh_token' => nil))
25
+ end
26
+
27
+ # Returns the Authorization header value for Basic Authentication
28
+ #
29
+ # @param [String] The client ID
30
+ # @param [String] the client secret
31
+ def authorization(client_id, client_secret)
32
+ 'Basic ' + Base64.encode64(client_id + ':' + client_secret).gsub("\n", '')
33
+ end
34
+ end
35
+ end
36
+ 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
+ fail(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
+ fail(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,15 @@
1
+ module OAuth2
2
+ class Version
3
+ MAJOR = 0
4
+ MINOR = 9
5
+ PATCH = 4
6
+ PRE = 'aptible'
7
+
8
+ class << self
9
+ # @return [String]
10
+ def to_s
11
+ [MAJOR, MINOR, PATCH, PRE].compact.join('.')
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'oauth2/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.add_development_dependency 'bundler', '~> 1.0'
8
+ spec.add_dependency 'faraday', ['>= 0.8', '< 0.10']
9
+ spec.add_dependency 'multi_json', '~> 1.3'
10
+ spec.add_dependency 'multi_xml', '~> 0.5'
11
+ spec.add_dependency 'rack', '~> 1.2'
12
+ spec.add_dependency 'jwt', '~> 0.1.8'
13
+ spec.authors = ['Frank Macreery']
14
+ spec.description = %q{A Ruby wrapper for the OAuth 2.0 protocol built with a similar style to the original OAuth spec.}
15
+ spec.email = ['frank@macreery.com']
16
+ spec.files = %w(.document CONTRIBUTING.md LICENSE.md README.md Rakefile oauth2.gemspec)
17
+ spec.files += Dir.glob('lib/**/*.rb')
18
+ spec.files += Dir.glob('spec/**/*')
19
+ spec.homepage = 'http://github.com/fancyremarker/oauth2-aptible'
20
+ spec.licenses = ['MIT']
21
+ spec.name = 'oauth2-aptible'
22
+ spec.require_paths = ['lib']
23
+ spec.required_rubygems_version = '>= 1.3.5'
24
+ spec.summary = %q{A Ruby wrapper for the OAuth 2.0 protocol.}
25
+ spec.test_files = Dir.glob('spec/**/*')
26
+ spec.version = OAuth2::Version
27
+ end
@@ -0,0 +1,29 @@
1
+ require 'simplecov'
2
+ require 'coveralls'
3
+
4
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
5
+ SimpleCov::Formatter::HTMLFormatter,
6
+ Coveralls::SimpleCov::Formatter
7
+ ]
8
+
9
+ SimpleCov.start do
10
+ add_filter '/spec/'
11
+ minimum_coverage(95.29)
12
+ end
13
+
14
+ require 'oauth2'
15
+ require 'addressable/uri'
16
+ require 'rspec'
17
+ require 'rspec/autorun'
18
+
19
+ RSpec.configure do |config|
20
+ config.expect_with :rspec do |c|
21
+ c.syntax = :expect
22
+ end
23
+ end
24
+
25
+ Faraday.default_adapter = :test
26
+
27
+ RSpec.configure do |conf|
28
+ include OAuth2
29
+ end
@@ -0,0 +1,172 @@
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?access_token=#{token}") { |env| [200, {}, Addressable::URI.parse(env[:url]).query_values['access_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
+ expect(subject.client).to eq(client)
28
+ expect(subject.token).to eq(token)
29
+ end
30
+
31
+ it 'assigns extra params' do
32
+ target = AccessToken.new(client, token, 'foo' => 'bar')
33
+ expect(target.params).to include('foo')
34
+ expect(target.params['foo']).to eq('bar')
35
+ end
36
+
37
+ def assert_initialized_token(target)
38
+ expect(target.token).to eq(token)
39
+ expect(target).to be_expires
40
+ expect(target.params.keys).to include('foo')
41
+ expect(target.params['foo']).to eq('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
+ expect(target.options[:param_name]).to eq('foo')
59
+ expect(target.options[:header_format]).to eq('Bearer %')
60
+ expect(target.options[:mode]).to eq(: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 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
+ expect(subject.post('/token/header').body).to include(token)
80
+ end
81
+ end
82
+ end
83
+
84
+ context ':mode => :query' do
85
+ before 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
+ expect(subject.post('/token/query').body).to eq(token)
92
+ end
93
+ end
94
+ end
95
+
96
+ context ':mode => :body' do
97
+ before 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
+ expect(subject.post('/token/body').body.split('=').last).to eq(token)
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ describe '#expires?' do
110
+ it 'is false if there is no expires_at' do
111
+ expect(AccessToken.new(client, token)).not_to be_expires
112
+ end
113
+
114
+ it 'is true if there is an expires_in' do
115
+ expect(AccessToken.new(client, token, :refresh_token => 'abaca', :expires_in => 600)).to be_expires
116
+ end
117
+
118
+ it 'is true if there is an expires_at' do
119
+ expect(AccessToken.new(client, token, :refresh_token => 'abaca', :expires_in => Time.now.getutc.to_i + 600)).to be_expires
120
+ end
121
+ end
122
+
123
+ describe '#expired?' do
124
+ it 'is false if there is no expires_in or expires_at' do
125
+ expect(AccessToken.new(client, token)).not_to be_expired
126
+ end
127
+
128
+ it 'is false if expires_in is in the future' do
129
+ expect(AccessToken.new(client, token, :refresh_token => 'abaca', :expires_in => 10_800)).not_to be_expired
130
+ end
131
+
132
+ it 'is 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 + 10_800
135
+ allow(Time).to receive(:now).and_return(@now)
136
+ expect(access).to be_expired
137
+ end
138
+
139
+ end
140
+
141
+ describe '#refresh!' do
142
+ let(:access) do
143
+ AccessToken.new(client, token, :refresh_token => 'abaca',
144
+ :expires_in => 600,
145
+ :param_name => 'o_param')
146
+ end
147
+
148
+ it 'returns a refresh token with appropriate values carried over' do
149
+ refreshed = access.refresh!
150
+ expect(access.client).to eq(refreshed.client)
151
+ expect(access.options[:param_name]).to eq(refreshed.options[:param_name])
152
+ end
153
+
154
+ context 'with a nil refresh_token in the response' do
155
+ let(:refresh_body) { MultiJson.encode(:access_token => 'refreshed_foo', :expires_in => 600, :refresh_token => nil) }
156
+
157
+ it 'copies the refresh_token from the original token' do
158
+ refreshed = access.refresh!
159
+
160
+ expect(refreshed.refresh_token).to eq(access.refresh_token)
161
+ end
162
+ end
163
+ end
164
+
165
+ describe '#to_hash' do
166
+ it 'return a hash equals to the hash used to initialize access token' do
167
+ hash = {:access_token => token, :refresh_token => 'foobar', :expires_at => Time.now.to_i + 200, 'foo' => 'bar'}
168
+ access_token = AccessToken.from_hash(client, hash.clone)
169
+ expect(access_token.to_hash).to eq(hash)
170
+ end
171
+ end
172
+ end