motionauth-oauth2 1.0.0

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