/ads_common/lib/ads_common/auth/oauth2_handler.rb

http://github.com/google/google-api-ads-ruby · Ruby · 250 lines · 145 code · 22 blank · 83 comment · 19 complexity · b8addb7b1e83ee7397e1e16a2cccb868 MD5 · raw file

  1. # Encoding: utf-8
  2. #
  3. # Copyright:: Copyright 2012, Google Inc. All Rights Reserved.
  4. #
  5. # License:: Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
  14. # implied.
  15. # See the License for the specific language governing permissions and
  16. # limitations under the License.
  17. #
  18. # This module manages OAuth2 authentication.
  19. require 'time'
  20. require 'faraday'
  21. require 'signet/oauth_2/client'
  22. require 'ads_common/auth/base_handler'
  23. require 'ads_common/errors'
  24. module AdsCommon
  25. module Auth
  26. # Credentials class to handle OAuth2 authentication.
  27. class OAuth2Handler < AdsCommon::Auth::BaseHandler
  28. OAUTH2_CONFIG = {
  29. :authorization_uri =>
  30. 'https://accounts.google.com/o/oauth2/auth',
  31. :token_credential_uri =>
  32. 'https://accounts.google.com/o/oauth2/token'
  33. }
  34. DEFAULT_CALLBACK = 'urn:ietf:wg:oauth:2.0:oob'
  35. # Initializes the OAuthHandler2 with all the necessary details.
  36. #
  37. # Args:
  38. # - config: Config object with library configuration
  39. # - scope: OAuth authorization scope
  40. #
  41. def initialize(config, scope)
  42. super(config)
  43. @scopes = []
  44. @scopes << scope unless scope.nil?
  45. additional_scopes = @config.read('authentication.oauth2_extra_scopes')
  46. @scopes += additional_scopes if additional_scopes.is_a?(Array)
  47. @client = nil
  48. end
  49. # Invalidates the stored token if the required credential has changed.
  50. def property_changed(prop, value)
  51. oauth2_keys = [:oauth2_client_id, :oauth2_client_secret, :oauth2_token]
  52. if oauth2_keys.include?(prop)
  53. @token, @client = nil, nil
  54. end
  55. end
  56. def handle_error(error)
  57. # TODO: Add support.
  58. get_logger().error(error)
  59. raise error
  60. end
  61. # Generates auth string for OAuth2 method of authentication.
  62. #
  63. # Args:
  64. # - credentials: credentials set for authorization
  65. #
  66. # Returns:
  67. # - Authentication string
  68. #
  69. def auth_string(credentials)
  70. token = get_token(credentials)
  71. if token.nil?
  72. raise AdsCommon::Errors::AuthError.new(
  73. 'Could not get auth token. Are you missing a refresh token?')
  74. end
  75. return ::Signet::OAuth2.generate_bearer_authorization_header(
  76. token[:access_token])
  77. end
  78. # Overrides base get_token method to account for the token expiration.
  79. def get_token(credentials = nil, force_refresh = false)
  80. token = super(credentials)
  81. token = refresh_token! if !@client.nil? &&
  82. (force_refresh || @client.expired?)
  83. return token
  84. end
  85. # Refreshes access token from refresh token.
  86. def refresh_token!()
  87. return nil if @token.nil? or @token[:refresh_token].nil?
  88. begin
  89. if @client.issued_at.is_a?(String)
  90. @client.issued_at = Time.parse(@client.issued_at)
  91. end
  92. @client.refresh!
  93. rescue Signet::AuthorizationError => e
  94. raise AdsCommon::Errors::AuthError.new("OAuth2 token refresh failed",
  95. e, (e.response.nil?) ? nil : e.response.body)
  96. end
  97. @token = token_from_client(@client)
  98. return @token
  99. end
  100. private
  101. # Auxiliary method to validate the credentials for token generation.
  102. #
  103. # Args:
  104. # - credentials: a hash with the credentials for the account being
  105. # accessed
  106. #
  107. # Raises:
  108. # - AdsCommon::Errors::AuthError if validation fails
  109. #
  110. def validate_credentials(credentials)
  111. if @scopes.empty?
  112. raise AdsCommon::Errors::AuthError, 'Scope is not specified.'
  113. end
  114. if credentials.nil?
  115. raise AdsCommon::Errors::AuthError, 'No credentials supplied.'
  116. end
  117. if credentials[:oauth2_client_id].nil?
  118. raise AdsCommon::Errors::AuthError,
  119. 'Client id is not included in the credentials.'
  120. end
  121. if credentials[:oauth2_client_secret].nil?
  122. raise AdsCommon::Errors::AuthError,
  123. 'Client secret is not included in the credentials.'
  124. end
  125. if credentials[:oauth2_token] &&
  126. !credentials[:oauth2_token].kind_of?(Hash)
  127. raise AdsCommon::Errors::AuthError,
  128. 'OAuth2 token provided must be a Hash'
  129. end
  130. end
  131. # Auxiliary method to generate an authentication token for logging via
  132. # the OAuth2 API.
  133. #
  134. # Args:
  135. # - credentials: a hash with the credentials for the account being
  136. # accessed
  137. #
  138. # Returns:
  139. # - The auth token for the account (as an AccessToken)
  140. #
  141. # Raises:
  142. # - AdsCommon::Errors::AuthError if authentication fails
  143. # - AdsCommon::Errors::OAuthVerificationRequired if OAuth verification
  144. # code required
  145. #
  146. def create_token(credentials)
  147. validate_credentials(credentials)
  148. @client ||= create_client(credentials)
  149. return create_token_from_credentials(credentials, @client) ||
  150. generate_access_token(credentials, @client)
  151. end
  152. def create_client(credentials)
  153. oauth_options = OAUTH2_CONFIG.merge({
  154. :client_id => credentials[:oauth2_client_id],
  155. :client_secret => credentials[:oauth2_client_secret],
  156. :scope => @scopes.join(' '),
  157. :redirect_uri => credentials[:oauth2_callback] || DEFAULT_CALLBACK,
  158. :state => credentials[:oauth2_state]
  159. }).reject {|k, v| v.nil?}
  160. return Signet::OAuth2::Client.new(oauth_options)
  161. end
  162. # Creates access token based on data from credentials.
  163. #
  164. # Args:
  165. # - credentials: a hash with the credentials for the account being
  166. # accessed. Has to include :oauth2_token key with a hash value that
  167. # represents a stored OAuth2 access token
  168. # - client: OAuth2 client for the current configuration
  169. #
  170. # Returns:
  171. # - The auth token for the account (as an AccessToken)
  172. #
  173. def create_token_from_credentials(credentials, client)
  174. oauth2_token_hash = credentials[:oauth2_token]
  175. if !oauth2_token_hash.nil? && oauth2_token_hash.kind_of?(Hash)
  176. token_data = AdsCommon::Utils.hash_keys_to_str(oauth2_token_hash)
  177. client.update_token!(token_data)
  178. end
  179. return token_from_client(client)
  180. end
  181. # Generates new request tokens and authorizes it to get access token.
  182. #
  183. # Args:
  184. # - credentials: a hash with the credentials for the account being
  185. # accessed
  186. # - client: OAuth2 client for the current configuration
  187. #
  188. # Returns:
  189. # - The auth token for the account (as Hash)
  190. #
  191. def generate_access_token(credentials, client)
  192. token = nil
  193. begin
  194. verification_code = credentials[:oauth2_verification_code]
  195. if verification_code.nil? || verification_code.empty?
  196. uri_options = {
  197. :access_type => credentials[:oauth2_access_type],
  198. :prompt => credentials[:oauth2_prompt]
  199. }.reject {|k, v| v.nil?}
  200. oauth_url = client.authorization_uri(uri_options)
  201. raise AdsCommon::Errors::OAuth2VerificationRequired.new(oauth_url)
  202. else
  203. client.code = verification_code
  204. proxy = @config.read('connection.proxy')
  205. connection = (proxy.nil?) ? nil : Faraday.new(:proxy => proxy)
  206. client.fetch_access_token!(:connection => connection)
  207. token = token_from_client(client)
  208. end
  209. rescue Signet::AuthorizationError => e
  210. raise AdsCommon::Errors::AuthError,
  211. 'Authorization error occured: %s' % e
  212. end
  213. return token
  214. end
  215. # Create a token Hash from a client.
  216. def token_from_client(client)
  217. return nil if client.refresh_token.nil? && client.access_token.nil?
  218. return {
  219. :access_token => client.access_token,
  220. :refresh_token => client.refresh_token,
  221. :issued_at => client.issued_at,
  222. :expires_in => client.expires_in,
  223. :id_token => client.id_token
  224. }
  225. end
  226. end
  227. end
  228. end