oauth2-client 1.0.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/.gitignore +18 -0
- data/.travis.yml +6 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +37 -0
- data/LICENSE +20 -0
- data/README.md +152 -0
- data/Rakefile +11 -0
- data/TODO +10 -0
- data/examples/google_client.rb +159 -0
- data/lib/oauth2.rb +7 -0
- data/lib/oauth2/client.rb +63 -0
- data/lib/oauth2/connection.rb +188 -0
- data/lib/oauth2/error.rb +3 -0
- data/lib/oauth2/grant.rb +7 -0
- data/lib/oauth2/grant/authorization_code.rb +71 -0
- data/lib/oauth2/grant/base.rb +41 -0
- data/lib/oauth2/grant/client_credentials.rb +27 -0
- data/lib/oauth2/grant/device.rb +37 -0
- data/lib/oauth2/grant/implicit.rb +46 -0
- data/lib/oauth2/grant/password.rb +28 -0
- data/lib/oauth2/grant/refresh_token.rb +26 -0
- data/lib/oauth2/helper.rb +46 -0
- data/lib/oauth2/version.rb +11 -0
- data/oauth2-client.gemspec +13 -0
- data/spec/.DS_Store +0 -0
- data/spec/examples/google_client_spec.rb +223 -0
- data/spec/mocks/oauth_client.yml +60 -0
- data/spec/oauth2/client_spec.rb +157 -0
- data/spec/oauth2/connection_spec.rb +273 -0
- data/spec/oauth2/grant/authorization_code_spec.rb +89 -0
- data/spec/oauth2/grant/base_spec.rb +57 -0
- data/spec/oauth2/grant/client_credentials_spec.rb +28 -0
- data/spec/oauth2/grant/device_spec.rb +35 -0
- data/spec/oauth2/grant/implicit_spec.rb +36 -0
- data/spec/oauth2/grant/password_spec.rb +28 -0
- data/spec/oauth2/grant/refresh_token_spec.rb +27 -0
- data/spec/spec_helper.rb +15 -0
- metadata +83 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
module OAuth2
|
2
|
+
class Client
|
3
|
+
|
4
|
+
attr_reader :host, :connection_options
|
5
|
+
attr_accessor :client_id, :client_secret, :connection_client,
|
6
|
+
:authorize_path, :token_path, :device_path
|
7
|
+
|
8
|
+
DEFAULTS_PATHS = {
|
9
|
+
:authorize_path => '/oauth2/authorize',
|
10
|
+
:token_path => '/oauth2/token',
|
11
|
+
:device_path => '/oauth2/device/code',
|
12
|
+
}
|
13
|
+
|
14
|
+
def initialize(host, client_id, client_secret, options={})
|
15
|
+
@host = host
|
16
|
+
@client_id = client_id
|
17
|
+
@client_secret = client_secret
|
18
|
+
@connection_options = options.fetch(:connection_options, {})
|
19
|
+
@connection_client = options.fetch(:connection_client, OAuth2::HttpConnection)
|
20
|
+
DEFAULTS_PATHS.keys.each do |key|
|
21
|
+
instance_variable_set(:"@#{key}", options.fetch(key, DEFAULTS_PATHS[key]))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def host=(hostname)
|
26
|
+
@connection = nil
|
27
|
+
@host = hostname
|
28
|
+
end
|
29
|
+
|
30
|
+
def connection_options=(options)
|
31
|
+
@connection = nil
|
32
|
+
@connection_options = options
|
33
|
+
end
|
34
|
+
|
35
|
+
def implicit
|
36
|
+
OAuth2::Grant::Implicit.new(self)
|
37
|
+
end
|
38
|
+
|
39
|
+
def authorization_code
|
40
|
+
OAuth2::Grant::AuthorizationCode.new(self)
|
41
|
+
end
|
42
|
+
|
43
|
+
def refresh_token
|
44
|
+
OAuth2::Grant::RefreshToken.new(self)
|
45
|
+
end
|
46
|
+
|
47
|
+
def client_credentials
|
48
|
+
OAuth2::Grant::ClientCredentials.new(self)
|
49
|
+
end
|
50
|
+
|
51
|
+
def password
|
52
|
+
OAuth2::Grant::Password.new(self)
|
53
|
+
end
|
54
|
+
|
55
|
+
def device_code
|
56
|
+
OAuth2::Grant::DeviceCode.new(self)
|
57
|
+
end
|
58
|
+
|
59
|
+
def connection
|
60
|
+
@connection ||= @connection_client.new(@host, @connection_options)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
begin
|
2
|
+
require 'net/https'
|
3
|
+
rescue LoadError
|
4
|
+
warn "Warning: no such file to load -- net/https. Make sure openssl is installed if you want ssl support"
|
5
|
+
require 'net/http'
|
6
|
+
end
|
7
|
+
require 'zlib'
|
8
|
+
require 'addressable/uri'
|
9
|
+
|
10
|
+
module OAuth2
|
11
|
+
class HttpConnection
|
12
|
+
|
13
|
+
class UnhandledHTTPMethodError < StandardError; end
|
14
|
+
class UnsupportedSchemeError < StandardError; end
|
15
|
+
|
16
|
+
NET_HTTP_EXCEPTIONS = [
|
17
|
+
EOFError,
|
18
|
+
Errno::ECONNABORTED,
|
19
|
+
Errno::ECONNREFUSED,
|
20
|
+
Errno::ECONNRESET,
|
21
|
+
Errno::EINVAL,
|
22
|
+
Net::HTTPBadResponse,
|
23
|
+
Net::HTTPHeaderSyntaxError,
|
24
|
+
Net::ProtocolError,
|
25
|
+
SocketError,
|
26
|
+
Zlib::GzipFile::Error,
|
27
|
+
]
|
28
|
+
|
29
|
+
attr_accessor :config, :scheme, :host, :port, :max_redirects, :ssl,
|
30
|
+
:user_agent, :accept, :max_redirects, :headers
|
31
|
+
|
32
|
+
def self.default_options
|
33
|
+
{
|
34
|
+
:headers => {
|
35
|
+
'Accept' => 'application/json',
|
36
|
+
'User-Agent' => "OAuth2 Ruby Gem #{OAuth2::Version}"
|
37
|
+
},
|
38
|
+
:ssl => {:verify => true},
|
39
|
+
:max_redirects => 5
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(url, options={})
|
44
|
+
@uri = Addressable::URI.parse(url)
|
45
|
+
self.class.default_options.keys.each do |key|
|
46
|
+
instance_variable_set(:"@#{key}", options.fetch(key, self.class.default_options[key]))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def default_headers
|
51
|
+
self.class.default_options[:headers]
|
52
|
+
end
|
53
|
+
|
54
|
+
def scheme=(scheme)
|
55
|
+
unless ['http', 'https'].include? scheme
|
56
|
+
raise UnsupportedSchemeError.new "#{scheme} is not supported, only http and https"
|
57
|
+
end
|
58
|
+
@scheme = scheme
|
59
|
+
end
|
60
|
+
|
61
|
+
def scheme
|
62
|
+
@scheme ||= @uri.scheme
|
63
|
+
end
|
64
|
+
|
65
|
+
def host
|
66
|
+
@host ||= @uri.host
|
67
|
+
end
|
68
|
+
|
69
|
+
def port
|
70
|
+
_port = ssl? ? 443 : 80
|
71
|
+
@port = @uri.port || _port
|
72
|
+
end
|
73
|
+
|
74
|
+
def absolute_url(path='')
|
75
|
+
"#{scheme}://#{host}#{path}"
|
76
|
+
end
|
77
|
+
|
78
|
+
def ssl?
|
79
|
+
scheme == "https" ? true : false
|
80
|
+
end
|
81
|
+
|
82
|
+
def ssl=(opts)
|
83
|
+
raise "Expected Hash but got #{opts.class.name}" unless opts.is_a?(Hash)
|
84
|
+
@ssl.merge!(opts)
|
85
|
+
end
|
86
|
+
|
87
|
+
def http_connection(opts={})
|
88
|
+
_host = opts[:host] || host
|
89
|
+
_port = opts[:port] || port
|
90
|
+
_scheme = opts[:scheme] || scheme
|
91
|
+
|
92
|
+
@http_client = Net::HTTP.new(_host, _port)
|
93
|
+
|
94
|
+
configure_ssl(@http_client) if _scheme == 'https'
|
95
|
+
|
96
|
+
@http_client
|
97
|
+
end
|
98
|
+
|
99
|
+
def send_request(method, path, opts={})
|
100
|
+
headers = @headers.merge(opts.fetch(:headers, {}))
|
101
|
+
params = opts[:params] || {}
|
102
|
+
query = Addressable::URI.form_encode(params)
|
103
|
+
method = method.to_s.downcase
|
104
|
+
normalized_path = query.empty? ? path : [path, query].join("?")
|
105
|
+
client = http_connection(opts.fetch(:connection_options, {}))
|
106
|
+
|
107
|
+
if (method == 'post' || method == 'put')
|
108
|
+
headers['Content-Type'] ||= 'application/x-www-form-urlencoded'
|
109
|
+
end
|
110
|
+
|
111
|
+
case method
|
112
|
+
when 'get'
|
113
|
+
response = client.get(normalized_path, headers)
|
114
|
+
when 'post'
|
115
|
+
response = client.post(path, query, headers)
|
116
|
+
when 'put'
|
117
|
+
response = client.put(path, query, headers)
|
118
|
+
when 'delete'
|
119
|
+
response = client.delete(normalized_path, headers)
|
120
|
+
else
|
121
|
+
raise UnhandledHTTPMethodError.new("Unsupported HTTP method, #{method.inspect}")
|
122
|
+
end
|
123
|
+
|
124
|
+
status = response.code.to_i
|
125
|
+
|
126
|
+
case status
|
127
|
+
when 301, 302, 303, 307
|
128
|
+
unless redirect_limit_reached?
|
129
|
+
if status == 303
|
130
|
+
method = :get
|
131
|
+
params = nil
|
132
|
+
headers.delete('Content-Type')
|
133
|
+
end
|
134
|
+
redirect_uri = Addressable::URI.parse(response.header['Location'])
|
135
|
+
conn = {
|
136
|
+
:scheme => redirect_uri.scheme,
|
137
|
+
:host => redirect_uri.host,
|
138
|
+
:port => redirect_uri.port
|
139
|
+
}
|
140
|
+
return send_request(method, redirect_uri.path, :params => params, :headers => headers, :connection_options => conn)
|
141
|
+
end
|
142
|
+
when 100..599
|
143
|
+
@redirect_count = 0
|
144
|
+
else
|
145
|
+
raise "Unhandled status code value of #{response.code}"
|
146
|
+
end
|
147
|
+
response
|
148
|
+
rescue *NET_HTTP_EXCEPTIONS
|
149
|
+
raise "Error::ConnectionFailed, $!"
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
def configure_ssl(http)
|
155
|
+
http.use_ssl = true
|
156
|
+
http.verify_mode = ssl_verify_mode
|
157
|
+
http.cert_store = ssl_cert_store
|
158
|
+
|
159
|
+
http.cert = ssl[:client_cert] if ssl[:client_cert]
|
160
|
+
http.key = ssl[:client_key] if ssl[:client_key]
|
161
|
+
http.ca_file = ssl[:ca_file] if ssl[:ca_file]
|
162
|
+
http.ca_path = ssl[:ca_path] if ssl[:ca_path]
|
163
|
+
http.verify_depth = ssl[:verify_depth] if ssl[:verify_depth]
|
164
|
+
http.ssl_version = ssl[:version] if ssl[:version]
|
165
|
+
end
|
166
|
+
|
167
|
+
def ssl_verify_mode
|
168
|
+
if ssl.fetch(:verify, true)
|
169
|
+
OpenSSL::SSL::VERIFY_PEER
|
170
|
+
else
|
171
|
+
OpenSSL::SSL::VERIFY_NONE
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def ssl_cert_store
|
176
|
+
return ssl[:cert_store] if ssl[:cert_store]
|
177
|
+
cert_store = OpenSSL::X509::Store.new
|
178
|
+
cert_store.set_default_paths
|
179
|
+
cert_store
|
180
|
+
end
|
181
|
+
|
182
|
+
def redirect_limit_reached?
|
183
|
+
@redirect_count ||= 0
|
184
|
+
@redirect_count += 1
|
185
|
+
@redirect_count > @max_redirects
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
data/lib/oauth2/error.rb
ADDED
data/lib/oauth2/grant.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
module OAuth2
|
2
|
+
module Grant
|
3
|
+
# Authorization Code Grant
|
4
|
+
# @see http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.1
|
5
|
+
class AuthorizationCode < Base
|
6
|
+
|
7
|
+
def response_type
|
8
|
+
"code"
|
9
|
+
end
|
10
|
+
|
11
|
+
def grant_type
|
12
|
+
"authorization_code"
|
13
|
+
end
|
14
|
+
|
15
|
+
# Authorization Request
|
16
|
+
# @see http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.1.1
|
17
|
+
def authorization_path(params={})
|
18
|
+
params = params.merge(authorization_params)
|
19
|
+
"#{@authorize_path}?#{to_query(params)}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def authorization_url(params={})
|
23
|
+
params = params.merge(authorization_params)
|
24
|
+
build_url(host, :path => authorize_path, :params => params)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Access Token Request
|
28
|
+
# @see http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.1.3
|
29
|
+
def token_path(params={})
|
30
|
+
unless params.empty?
|
31
|
+
return "#{@token_path}?#{to_query(params)}"
|
32
|
+
end
|
33
|
+
@token_path
|
34
|
+
end
|
35
|
+
|
36
|
+
# Retrieve page at authorization path
|
37
|
+
#
|
38
|
+
# @param [Hash] opts options
|
39
|
+
def fetch_authorization_url(opts={})
|
40
|
+
opts[:method] ||= :get
|
41
|
+
opts[:params] ||= {}
|
42
|
+
opts[:params].merge!(authorization_params)
|
43
|
+
method = opts.delete(:method) || :get
|
44
|
+
make_request(method, @authorize_path, opts)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Retrieve an access token for a given auth code
|
48
|
+
#
|
49
|
+
# @param [String] code refresh token
|
50
|
+
# @param [Hash] params additional params
|
51
|
+
# @param [Hash] opts options
|
52
|
+
def get_token(code, opts={})
|
53
|
+
opts[:params] ||= {}
|
54
|
+
opts[:params][:code] = code
|
55
|
+
opts[:authenticate] ||= :headers
|
56
|
+
method = opts.delete(:method) || :post
|
57
|
+
make_request(method, token_path, opts)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# Default authorization request parameters
|
63
|
+
def authorization_params
|
64
|
+
{
|
65
|
+
:response_type => response_type,
|
66
|
+
:client_id => @client_id
|
67
|
+
}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module OAuth2
|
2
|
+
module Grant
|
3
|
+
class Base
|
4
|
+
include OAuth2::UrlHelper
|
5
|
+
|
6
|
+
class InvalidAuthorizationTypeError < StandardError; end
|
7
|
+
|
8
|
+
attr_accessor :client_id, :client_secret, :connection, :host,
|
9
|
+
:authorize_path, :token_path, :device_path
|
10
|
+
|
11
|
+
def initialize(client)
|
12
|
+
@host = client.host
|
13
|
+
@connection = client.connection
|
14
|
+
@client_id = client.client_id
|
15
|
+
@client_secret = client.client_secret
|
16
|
+
@token_path = client.token_path
|
17
|
+
@authorize_path = client.authorize_path
|
18
|
+
@device_path = client.device_path
|
19
|
+
end
|
20
|
+
|
21
|
+
def make_request(method, path, opts={})
|
22
|
+
if auth_type = opts.delete(:authenticate)
|
23
|
+
case auth_type.to_sym
|
24
|
+
when :body
|
25
|
+
opts[:params] ||= {}
|
26
|
+
opts[:params].merge!({
|
27
|
+
:client_id => @client_id,
|
28
|
+
:client_secret => @client_secret
|
29
|
+
})
|
30
|
+
when :headers
|
31
|
+
opts[:headers] ||= {}
|
32
|
+
opts[:headers]['Authorization'] = http_basic_encode(@client_id, @client_secret)
|
33
|
+
else
|
34
|
+
#do nothing
|
35
|
+
end
|
36
|
+
end
|
37
|
+
@connection.send_request(method, path, opts)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "base64"
|
2
|
+
|
3
|
+
module OAuth2
|
4
|
+
module Grant
|
5
|
+
# Client Credentials Grant
|
6
|
+
#
|
7
|
+
# @see http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.4
|
8
|
+
class ClientCredentials < Base
|
9
|
+
|
10
|
+
def grant_type
|
11
|
+
"client_credentials"
|
12
|
+
end
|
13
|
+
|
14
|
+
# Retrieve an access token for the given client credentials
|
15
|
+
#
|
16
|
+
# @param [Hash] params additional params
|
17
|
+
# @param [Hash] opts options
|
18
|
+
def get_token(opts={})
|
19
|
+
opts[:params] ||= {}
|
20
|
+
opts[:params][:grant_type] = grant_type
|
21
|
+
opts[:authenticate] ||= :headers
|
22
|
+
method = opts.delete(:method) || :post
|
23
|
+
make_request(method, @token_path, opts)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module OAuth2
|
2
|
+
module Grant
|
3
|
+
# Device Grant
|
4
|
+
# @see https://developers.google.com/accounts/docs/OAuth2ForDevices
|
5
|
+
class DeviceCode < Base
|
6
|
+
|
7
|
+
def grant_type
|
8
|
+
"http://oauth.net/grant_type/device/1.0"
|
9
|
+
end
|
10
|
+
|
11
|
+
# Generate the authorization path using the given parameters .
|
12
|
+
#
|
13
|
+
# @param [Hash] query parameters
|
14
|
+
def get_code(opts={})
|
15
|
+
opts[:params] ||= {}
|
16
|
+
opts[:params][:client_id] = @client_id
|
17
|
+
method = opts.delete(:method) || :post
|
18
|
+
make_request(method, @device_path, opts)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Retrieve an access token given the specified client.
|
22
|
+
#
|
23
|
+
# @param [Hash] params additional params
|
24
|
+
# @param [Hash] opts options
|
25
|
+
def get_token(code, opts={})
|
26
|
+
opts[:params] ||= {}
|
27
|
+
opts[:params].merge!({
|
28
|
+
:code => code,
|
29
|
+
:grant_type => grant_type
|
30
|
+
})
|
31
|
+
opts[:authenticate] ||= :headers
|
32
|
+
method = opts.delete(:method) || :post
|
33
|
+
make_request(method, @token_path, opts)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module OAuth2
|
2
|
+
module Grant
|
3
|
+
# Implicit Grant
|
4
|
+
# @see http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.2
|
5
|
+
class Implicit < Base
|
6
|
+
|
7
|
+
def response_type
|
8
|
+
"token"
|
9
|
+
end
|
10
|
+
|
11
|
+
# Generate a token path using the given parameters .
|
12
|
+
#
|
13
|
+
# @param [Hash] query parameters
|
14
|
+
def token_path(params={})
|
15
|
+
params = params.merge(token_params)
|
16
|
+
"#{@authorize_path}?#{to_query(params)}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def token_url(params={})
|
20
|
+
params = params.merge(token_params)
|
21
|
+
build_url(host, :path => authorize_path, :params => params)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Retrieve an access token given the specified client.
|
25
|
+
#
|
26
|
+
# @param [Hash] params additional params
|
27
|
+
# @param [Hash] opts options
|
28
|
+
def get_token(opts={})
|
29
|
+
opts[:params] ||= {}
|
30
|
+
opts[:params].merge!(token_params)
|
31
|
+
opts[:authenticate] ||= :headers
|
32
|
+
method = opts.delete(:method) || :get
|
33
|
+
make_request(method, @token_path, opts)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def token_params
|
39
|
+
{
|
40
|
+
:response_type => response_type,
|
41
|
+
:client_id => @client_id
|
42
|
+
}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|