motionauth-oauth2 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.
@@ -0,0 +1,177 @@
1
+ module OAuth2
2
+ class AccessToken
3
+ attr_reader :client, :token, :expires_in, :expires_at, :params
4
+ attr_accessor :options, :refresh_token
5
+
6
+ class << self
7
+ # Initializes an AccessToken from a Hash
8
+ #
9
+ # @param [Client] the OAuth2::Client instance
10
+ # @param [Hash] a hash of AccessToken property values
11
+ # @return [AccessToken] the initalized AccessToken
12
+ def from_hash(client, hash)
13
+ new(client, hash.delete("access_token") || hash.delete(:access_token), hash)
14
+ end
15
+
16
+ # Initializes an AccessToken from a key/value application/x-www-form-urlencoded string
17
+ #
18
+ # @param [Client] client the OAuth2::Client instance
19
+ # @param [String] kvform the application/x-www-form-urlencoded string
20
+ # @return [AccessToken] the initalized AccessToken
21
+ def from_kvform(client, kvform)
22
+ from_hash(client, Utils.params_from_query(kvform))
23
+ end
24
+ end
25
+
26
+ # Initalize an AccessToken
27
+ #
28
+ # @param [Client] client the OAuth2::Client instance
29
+ # @param [String] token the Access Token value
30
+ # @param [Hash] opts the options to create the Access Token with
31
+ # @option opts [String] :refresh_token (nil) the refresh_token value
32
+ # @option opts [FixNum, String] :expires_in (nil) the number of seconds in which the AccessToken will expire
33
+ # @option opts [FixNum, String] :expires_at (nil) the epoch time in seconds in which AccessToken will expire
34
+ # @option opts [Symbol] :mode (:header) the transmission mode of the Access Token parameter value
35
+ # one of :header, :body or :query
36
+ # @option opts [String] :header_format ("Bearer %s") the string format to use for the Authorization header
37
+ # @option opts [String] :param_name ("access_token") the parameter name to use for transmission of the
38
+ # Access Token value in :body or :query transmission mode
39
+ def initialize(client, token, opts = {})
40
+ @client = client
41
+ @token = token.to_s
42
+ [:refresh_token, :expires_in, :expires_at].each do |arg|
43
+ instance_variable_set("@#{arg}", opts.delete(arg) || opts.delete(arg.to_s))
44
+ end
45
+ @expires_in ||= opts.delete("expires")
46
+ @expires_in &&= @expires_in.to_i
47
+ @expires_at &&= @expires_at.to_i
48
+ @expires_at ||= Time.now.to_i + @expires_in if @expires_in
49
+ @options = {
50
+ mode: opts.delete(:mode) || :header,
51
+ header_format: opts.delete(:header_format) || "Bearer %s",
52
+ param_name: opts.delete(:param_name) || "access_token"
53
+ }
54
+ @params = opts
55
+ end
56
+
57
+ # Indexer to additional params present in token response
58
+ #
59
+ # @param [String] key entry key to Hash
60
+ def [](key)
61
+ @params[key]
62
+ end
63
+
64
+ # Whether or not the token expires
65
+ #
66
+ # @return [Boolean]
67
+ def expires?
68
+ !!@expires_at # rubocop:disable DoubleNegation
69
+ end
70
+
71
+ # Whether or not the token is expired
72
+ #
73
+ # @return [Boolean]
74
+ def expired?
75
+ expires? && (expires_at < Time.now.to_i)
76
+ end
77
+
78
+ # Refreshes the current Access Token
79
+ #
80
+ # @return [AccessToken] a new AccessToken
81
+ # @note options should be carried over to the new AccessToken
82
+ def refresh!(params = {})
83
+ fail("A refresh_token is not available") unless refresh_token
84
+ params.merge!(
85
+ client_id: @client.id,
86
+ client_secret: @client.secret,
87
+ grant_type: "refresh_token",
88
+ refresh_token: refresh_token
89
+ )
90
+ new_token = @client.get_token(params)
91
+ new_token.options = options
92
+ new_token.refresh_token = refresh_token unless new_token.refresh_token
93
+ new_token
94
+ end
95
+
96
+ # Convert AccessToken to a hash which can be used to rebuild itself with AccessToken.from_hash
97
+ #
98
+ # @return [Hash] a hash of AccessToken property values
99
+ def to_hash
100
+ params.merge(access_token: token, refresh_token: refresh_token, expires_at: expires_at)
101
+ end
102
+
103
+ # Make a request with the Access Token
104
+ #
105
+ # @param [Symbol] verb the HTTP request method
106
+ # @param [String] path the HTTP URL path of the request
107
+ # @param [Hash] opts the options to make the request with
108
+ # @see Client#request
109
+ def request(verb, path, opts = {}, &block)
110
+ self.token = opts
111
+ @client.request(verb, path, opts, &block)
112
+ end
113
+
114
+ # Make a GET request with the Access Token
115
+ #
116
+ # @see AccessToken#request
117
+ def get(path, opts = {}, &block)
118
+ request(:get, path, opts, &block)
119
+ end
120
+
121
+ # Make a POST request with the Access Token
122
+ #
123
+ # @see AccessToken#request
124
+ def post(path, opts = {}, &block)
125
+ request(:post, path, opts, &block)
126
+ end
127
+
128
+ # Make a PUT request with the Access Token
129
+ #
130
+ # @see AccessToken#request
131
+ def put(path, opts = {}, &block)
132
+ request(:put, path, opts, &block)
133
+ end
134
+
135
+ # Make a PATCH request with the Access Token
136
+ #
137
+ # @see AccessToken#request
138
+ def patch(path, opts = {}, &block)
139
+ request(:patch, path, opts, &block)
140
+ end
141
+
142
+ # Make a DELETE request with the Access Token
143
+ #
144
+ # @see AccessToken#request
145
+ def delete(path, opts = {}, &block)
146
+ request(:delete, path, opts, &block)
147
+ end
148
+
149
+ # Get the headers hash (includes Authorization token)
150
+ def headers
151
+ { "Authorization" => options[:header_format] % token }
152
+ end
153
+
154
+ private
155
+
156
+ def token=(opts) # rubocop:disable MethodLength
157
+ case options[:mode]
158
+ when :header
159
+ opts[:headers] ||= {}
160
+ opts[:headers].merge!(headers)
161
+ when :query
162
+ opts[:params] ||= {}
163
+ opts[:params][options[:param_name]] = token
164
+ when :body
165
+ opts[:body] ||= {}
166
+ if opts[:body].is_a?(Hash)
167
+ opts[:body][options[:param_name]] = token
168
+ else
169
+ opts[:body] << "&#{options[:param_name]}=#{token}"
170
+ end
171
+ # @todo support for multi-part (file uploads)
172
+ else
173
+ fail("invalid :mode option of #{options[:mode]}")
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,163 @@
1
+ module OAuth2
2
+ # The OAuth2::Client class
3
+ class Client
4
+ attr_reader :id, :secret, :site
5
+ attr_accessor :options
6
+ attr_writer :connection
7
+
8
+ # Instantiate a new OAuth 2.0 client using the
9
+ # Client ID and Client Secret registered to your
10
+ # application.
11
+ #
12
+ # @param [String] client_id the client_id value
13
+ # @param [String] client_secret the client_secret value
14
+ # @param [Hash] opts the options to create the client with
15
+ # @option opts [String] :site the OAuth2 provider site host
16
+ # @option opts [String] :authorize_url ("/oauth/authorize") absolute or relative URL path to the Authorization endpoint
17
+ # @option opts [String] :token_url ("/oauth/token") absolute or relative URL path to the Token endpoint
18
+ # @option opts [Symbol] :token_method (:post) HTTP method to use to request token (:get or :post)
19
+ # @option opts [Hash] :connection_opts ({}) Hash of connection options to pass to initialize Faraday with
20
+ # @option opts [FixNum] :max_redirects (5) maximum number of redirects to follow
21
+ # @option opts [Boolean] :raise_errors (true) whether or not to raise an OAuth2::Error
22
+ # on responses with 400+ status codes
23
+ def initialize(client_id, client_secret, options = {})
24
+ opts = options.dup
25
+ @id = client_id
26
+ @secret = client_secret
27
+ @site = opts.delete(:site)
28
+ ssl = opts.delete(:ssl)
29
+ @options = {
30
+ authorize_url: "/oauth/authorize",
31
+ token_url: "/oauth/token",
32
+ token_method: :post,
33
+ connection_opts: {},
34
+ max_redirects: 5,
35
+ raise_errors: true
36
+ }.merge(opts)
37
+ @options[:connection_opts][:ssl] = ssl if ssl
38
+ end
39
+
40
+ # Set the site host
41
+ #
42
+ # @param [String] the OAuth2 provider site host
43
+ def site=(value)
44
+ @connection = nil
45
+ @site = value
46
+ end
47
+
48
+ # The OAuth2::Connection object
49
+ def connection
50
+ @connection ||= Connection.new(site, options[:connection_opts])
51
+ end
52
+
53
+ # The authorize endpoint URL of the OAuth2 provider
54
+ #
55
+ # @param [Hash] params additional query parameters
56
+ def authorize_url(params = nil)
57
+ connection.build_url(options[:authorize_url], params).to_s
58
+ end
59
+
60
+ # The token endpoint URL of the OAuth2 provider
61
+ #
62
+ # @param [Hash] params additional query parameters
63
+ def token_url(params = nil)
64
+ connection.build_url(options[:token_url], params).to_s
65
+ end
66
+
67
+ # Makes a request relative to the specified site root.
68
+ #
69
+ # @param [Symbol] verb one of :get, :post, :put, :delete
70
+ # @param [String] url URL path of request
71
+ # @param [Hash] opts the options to make the request with
72
+ # @option opts [Hash] :params additional query parameters for the URL of the request
73
+ # @option opts [Hash, String] :body the body of the request
74
+ # @option opts [Hash] :headers http request headers
75
+ # @option opts [Boolean] :raise_errors whether or not to raise an OAuth2::Error on 400+ status
76
+ # code response for this request. Will default to client option
77
+ # @option opts [Symbol] :parse @see Response::initialize
78
+ # @yield [req] The OAuth2::Request
79
+ def request(verb, url, opts = {}) # rubocop:disable CyclomaticComplexity, MethodLength
80
+ # connection.response :logger, ::Logger.new($stdout) if ENV["OAUTH_DEBUG"] == "true"
81
+
82
+ url = connection.build_url(url, opts[:params]).to_s
83
+ response = connection.run_request(verb, url, opts[:body], opts[:headers], opts[:parse])
84
+
85
+ case response.status
86
+ when 301, 302, 303, 307
87
+ opts[:redirect_count] ||= 0
88
+ opts[:redirect_count] += 1
89
+ return response if opts[:redirect_count] > options[:max_redirects]
90
+ if response.status == 303
91
+ verb = :get
92
+ opts.delete(:body)
93
+ end
94
+ request(verb, response.headers["location"], opts)
95
+ when 200..299, 300..399
96
+ # on non-redirecting 3xx statuses, just return the response
97
+ response
98
+ when 400..599
99
+ error = Error.new(response)
100
+ fail(error) if opts.fetch(:raise_errors, options[:raise_errors])
101
+ response.error = error
102
+ response
103
+ else
104
+ error = Error.new(response)
105
+ fail(error, "Unhandled status code value of #{response.status}")
106
+ end
107
+ end
108
+
109
+ # Initializes an AccessToken by making a request to the token endpoint
110
+ #
111
+ # @param [Hash] params a Hash of params for the token endpoint
112
+ # @param [Hash] access token options, to pass to the AccessToken object
113
+ # @param [Class] class of access token for easier subclassing OAuth2::AccessToken
114
+ # @return [AccessToken] the initalized AccessToken
115
+ def get_token(params, access_token_opts = {}, access_token_class = AccessToken)
116
+ opts = { raise_errors: options[:raise_errors], parse: params.delete(:parse) }
117
+ if options[:token_method] == :post
118
+ headers = params.delete(:headers)
119
+ opts[:body] = params
120
+ opts[:headers] = { "Content-Type" => "application/x-www-form-urlencoded" }
121
+ opts[:headers].merge!(headers) if headers
122
+ else
123
+ opts[:params] = params
124
+ end
125
+ response = request(options[:token_method], token_url, opts)
126
+ error = Error.new(response)
127
+ fail(error) if options[:raise_errors] && !(response.parsed.is_a?(Hash) && response.parsed["access_token"])
128
+ access_token_class.from_hash(self, response.parsed.merge(access_token_opts))
129
+ end
130
+
131
+ # The Authorization Code strategy
132
+ #
133
+ # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.1
134
+ def auth_code
135
+ @auth_code ||= OAuth2::Strategy::AuthCode.new(self)
136
+ end
137
+
138
+ # The Implicit strategy
139
+ #
140
+ # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-26#section-4.2
141
+ def implicit
142
+ @implicit ||= OAuth2::Strategy::Implicit.new(self)
143
+ end
144
+
145
+ # The Resource Owner Password Credentials strategy
146
+ #
147
+ # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.3
148
+ def password
149
+ @password ||= OAuth2::Strategy::Password.new(self)
150
+ end
151
+
152
+ # The Client Credentials strategy
153
+ #
154
+ # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.4
155
+ def client_credentials
156
+ @client_credentials ||= OAuth2::Strategy::ClientCredentials.new(self)
157
+ end
158
+
159
+ def assertion
160
+ @assertion ||= OAuth2::Strategy::Assertion.new(self)
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,35 @@
1
+ module OAuth2
2
+ class Connection
3
+ # A Set of allowed HTTP verbs.
4
+ METHODS = [:get, :post, :put, :delete, :head, :patch, :options]
5
+
6
+ # Public: Returns a Hash of URI query unencoded key/value pairs.
7
+ attr_reader :params
8
+
9
+ # Public: Returns a Hash of unencoded HTTP header key/value pairs.
10
+ attr_reader :headers
11
+
12
+ # Public: Returns a URI with the prefix used for all requests from this
13
+ # Connection. This includes a default host name, scheme, port, and path.
14
+ attr_reader :url_prefix
15
+
16
+ # Public: Returns the Faraday::Builder for this Connection.
17
+ attr_reader :builder
18
+
19
+ # Public: Returns a Hash of the request options.
20
+ attr_reader :options
21
+
22
+ # Public: Returns a Hash of the SSL options.
23
+ attr_reader :ssl
24
+
25
+ # Public: Sets the Hash of unencoded HTTP header key/value pairs.
26
+ def headers=(hash)
27
+ @headers.replace(hash)
28
+ end
29
+
30
+ # Public: Sets the Hash of URI query unencoded key/value pairs.
31
+ def params=(hash)
32
+ @params.replace(hash)
33
+ end
34
+ end
35
+ end
@@ -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,74 @@
1
+ module OAuth2
2
+ class MACToken < AccessToken
3
+ # Generates a MACToken from an AccessToken and secret
4
+ #
5
+ # @param [AccessToken] token the OAuth2::Token instance
6
+ # @option [String] secret the secret key value
7
+ # @param [Hash] opts the options to create the Access Token with
8
+ # @see MACToken#initialize
9
+ def self.from_access_token(token, secret, options = {})
10
+ new(token.client, token.token, secret, token.params.merge(
11
+ refresh_token: token.refresh_token,
12
+ expires_in: token.expires_in,
13
+ expires_at: token.expires_at
14
+ ).merge(options))
15
+ end
16
+
17
+ attr_reader :algorithm, :secret
18
+
19
+ # Initalize a MACToken
20
+ #
21
+ # @param [Client] client the OAuth2::Client instance
22
+ # @param [String] token the Access Token value
23
+ # @option [String] secret the secret key value
24
+ # @param [Hash] opts the options to create the Access Token with
25
+ # @option opts [String] :refresh_token (nil) the refresh_token value
26
+ # @option opts [FixNum, String] :expires_in (nil) the number of seconds in which the AccessToken will expire
27
+ # @option opts [FixNum, String] :expires_at (nil) the epoch time in seconds in which AccessToken will expire
28
+ # @option opts [FixNum, String] :algorithm (hmac-sha-256) the algorithm to use for the HMAC digest (one of 'hmac-sha-256', 'hmac-sha-1')
29
+ def initialize(client, token, secret, opts = {})
30
+ @secret = secret
31
+ self.algorithm = opts.delete(:algorithm) || "hmac-sha-256"
32
+
33
+ super(client, token, opts)
34
+ end
35
+
36
+ # Make a request with the MAC Token
37
+ #
38
+ # @param [Symbol] verb the HTTP request method
39
+ # @param [String] path the HTTP URL path of the request
40
+ # @param [Hash] opts the options to make the request with
41
+ # @see Client#request
42
+ def request(verb, path, opts = {}, &block)
43
+ url = client.connection.build_url(path, opts[:params]).to_s
44
+
45
+ opts[:headers] ||= {}
46
+ opts[:headers].merge!("Authorization" => header(verb, url))
47
+
48
+ @client.request(verb, path, opts, &block)
49
+ end
50
+
51
+ # Get the headers hash (always an empty hash)
52
+ def headers
53
+ {}
54
+ end
55
+
56
+ # Generate the MAC header
57
+ #
58
+ # @param [Symbol] verb the HTTP request method
59
+ # @param [String] url the HTTP URL path of the request
60
+ def header(verb, url)
61
+ timestamp = Time.now.utc.to_i
62
+ nonce = generate_nonce
63
+ mac = signature(timestamp, nonce, verb, url)
64
+ "MAC id=\"#{token}\", ts=\"#{timestamp}\", nonce=\"#{nonce}\", mac=\"#{mac}\""
65
+ end
66
+
67
+ private
68
+
69
+ # No-op since we need the verb and path
70
+ # and the MAC always goes in a header
71
+ def token=(_)
72
+ end
73
+ end
74
+ end