oauth2-aptible 0.9.4.aptible

Sign up to get free protection for your applications and to get access to all the features.
@@ -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