google.rb 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. # Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
  2. class ExternalCredential::Google
  3. def self.app_verify(params)
  4. request_account_to_link(params, false)
  5. params
  6. end
  7. def self.request_account_to_link(credentials = {}, app_required = true)
  8. external_credential = ExternalCredential.find_by(name: 'google')
  9. raise Exceptions::UnprocessableEntity, __('No Google app configured!') if !external_credential && app_required
  10. if external_credential
  11. if credentials[:client_id].blank?
  12. credentials[:client_id] = external_credential.credentials['client_id']
  13. end
  14. if credentials[:client_secret].blank?
  15. credentials[:client_secret] = external_credential.credentials['client_secret']
  16. end
  17. end
  18. raise Exceptions::UnprocessableEntity, __("The required parameter 'client_id' is missing.") if credentials[:client_id].blank?
  19. raise Exceptions::UnprocessableEntity, __("The required parameter 'client_secret' is missing.") if credentials[:client_secret].blank?
  20. authorize_url = generate_authorize_url(credentials[:client_id])
  21. {
  22. authorize_url: authorize_url,
  23. }
  24. end
  25. def self.link_account(_request_token, params)
  26. external_credential = ExternalCredential.find_by(name: 'google')
  27. raise Exceptions::UnprocessableEntity, __('No Google app configured!') if !external_credential
  28. raise Exceptions::UnprocessableEntity, __('No code for session found!') if !params[:code]
  29. response = authorize_tokens(external_credential.credentials[:client_id], external_credential.credentials[:client_secret], params[:code])
  30. %w[refresh_token access_token expires_in scope token_type id_token].each do |key|
  31. raise Exceptions::UnprocessableEntity, "No #{key} for authorization request found!" if response[key.to_sym].blank?
  32. end
  33. user_data = user_info(response[:id_token])
  34. raise Exceptions::UnprocessableEntity, __("User email could not be extracted from 'id_token'.") if user_data[:email].blank?
  35. channel_options = {
  36. inbound: {
  37. adapter: 'imap',
  38. options: {
  39. auth_type: 'XOAUTH2',
  40. host: 'imap.gmail.com',
  41. ssl: 'ssl',
  42. user: user_data[:email],
  43. },
  44. },
  45. outbound: {
  46. adapter: 'smtp',
  47. options: {
  48. host: 'smtp.gmail.com',
  49. port: 465,
  50. ssl: true,
  51. user: user_data[:email],
  52. authentication: 'xoauth2',
  53. },
  54. },
  55. auth: response.merge(
  56. provider: 'google',
  57. type: 'XOAUTH2',
  58. client_id: external_credential.credentials[:client_id],
  59. client_secret: external_credential.credentials[:client_secret],
  60. ),
  61. }
  62. if params[:channel_id]
  63. existing_channel = Channel.where(area: 'Google::Account').find(params[:channel_id])
  64. existing_channel.update!(
  65. options: channel_options,
  66. )
  67. existing_channel.refresh_xoauth2!
  68. return existing_channel
  69. end
  70. migrate_channel = nil
  71. Channel.where(area: 'Email::Account').find_each do |channel|
  72. next if channel.options.dig(:inbound, :options, :host)&.downcase != 'imap.gmail.com'
  73. next if channel.options.dig(:outbound, :options, :host)&.downcase != 'smtp.gmail.com'
  74. next if channel.options.dig(:outbound, :options, :user)&.downcase != user_data[:email].downcase && channel.options.dig(:outbound, :email)&.downcase != user_data[:email].downcase
  75. migrate_channel = channel
  76. break
  77. end
  78. if migrate_channel
  79. channel_options[:inbound][:options][:folder] = migrate_channel.options[:inbound][:options][:folder]
  80. channel_options[:inbound][:options][:keep_on_server] = migrate_channel.options[:inbound][:options][:keep_on_server]
  81. backup = {
  82. attributes: {
  83. area: migrate_channel.area,
  84. options: migrate_channel.options,
  85. last_log_in: migrate_channel.last_log_in,
  86. last_log_out: migrate_channel.last_log_out,
  87. status_in: migrate_channel.status_in,
  88. status_out: migrate_channel.status_out,
  89. },
  90. migrated_at: Time.zone.now,
  91. }
  92. migrate_channel.update(
  93. area: 'Google::Account',
  94. options: channel_options.merge(backup_imap_classic: backup),
  95. last_log_in: nil,
  96. last_log_out: nil,
  97. )
  98. return migrate_channel
  99. end
  100. email_addresses = user_aliases(response)
  101. email_addresses.unshift({
  102. realname: "#{Setting.get('product_name')} Support",
  103. email: user_data[:email],
  104. })
  105. email_addresses.each do |email|
  106. next if !EmailAddress.exists?(email: email[:email])
  107. raise Exceptions::UnprocessableEntity, "Duplicate email address or email alias #{email[:email]} found!"
  108. end
  109. # create channel
  110. channel = Channel.create!(
  111. area: 'Google::Account',
  112. group_id: Group.first.id,
  113. options: channel_options,
  114. active: false,
  115. created_by_id: 1,
  116. updated_by_id: 1,
  117. )
  118. email_addresses.each do |user_alias|
  119. EmailAddress.create!(
  120. channel_id: channel.id,
  121. realname: user_alias[:realname],
  122. email: user_alias[:email],
  123. active: true,
  124. created_by_id: 1,
  125. updated_by_id: 1,
  126. )
  127. end
  128. channel
  129. end
  130. def self.generate_authorize_url(client_id, scope = 'openid email profile https://mail.google.com/')
  131. params = {
  132. 'client_id' => client_id,
  133. 'redirect_uri' => ExternalCredential.callback_url('google'),
  134. 'scope' => scope,
  135. 'response_type' => 'code',
  136. 'access_type' => 'offline',
  137. 'prompt' => 'consent',
  138. }
  139. uri = URI::HTTPS.build(
  140. host: 'accounts.google.com',
  141. path: '/o/oauth2/auth',
  142. query: params.to_query
  143. )
  144. uri.to_s
  145. end
  146. def self.authorize_tokens(client_id, client_secret, authorization_code)
  147. params = {
  148. 'client_secret' => client_secret,
  149. 'code' => authorization_code,
  150. 'grant_type' => 'authorization_code',
  151. 'client_id' => client_id,
  152. 'redirect_uri' => ExternalCredential.callback_url('google'),
  153. }
  154. uri = URI::HTTPS.build(
  155. host: 'accounts.google.com',
  156. path: '/o/oauth2/token',
  157. )
  158. response = Net::HTTP.post_form(uri, params)
  159. if response.code != 200 && response.body.blank?
  160. Rails.logger.error "Request failed! (code: #{response.code})"
  161. raise "Request failed! (code: #{response.code})"
  162. end
  163. result = JSON.parse(response.body)
  164. if result['error'] && response.code != 200
  165. Rails.logger.error "Request failed! ERROR: #{result['error']} (#{result['error_description']}, params: #{params.to_json})"
  166. raise "Request failed! ERROR: #{result['error']} (#{result['error_description']})"
  167. end
  168. result[:created_at] = Time.zone.now
  169. result.symbolize_keys
  170. end
  171. def self.refresh_token(token)
  172. return token if token[:created_at] >= 50.minutes.ago
  173. params = {
  174. 'client_id' => token[:client_id],
  175. 'client_secret' => token[:client_secret],
  176. 'refresh_token' => token[:refresh_token],
  177. 'grant_type' => 'refresh_token',
  178. }
  179. uri = URI::HTTPS.build(
  180. host: 'accounts.google.com',
  181. path: '/o/oauth2/token',
  182. )
  183. response = Net::HTTP.post_form(uri, params)
  184. if response.code != 200 && response.body.blank?
  185. Rails.logger.error "Request failed! (code: #{response.code})"
  186. raise "Request failed! (code: #{response.code})"
  187. end
  188. result = JSON.parse(response.body)
  189. if result['error'] && response.code != 200
  190. Rails.logger.error "Request failed! ERROR: #{result['error']} (#{result['error_description']}, params: #{params.to_json})"
  191. raise "Request failed! ERROR: #{result['error']} (#{result['error_description']})"
  192. end
  193. token.merge(
  194. created_at: Time.zone.now,
  195. access_token: result['access_token'],
  196. ).symbolize_keys
  197. end
  198. def self.user_aliases(token)
  199. uri = URI.parse('https://www.googleapis.com/gmail/v1/users/me/settings/sendAs')
  200. http = Net::HTTP.new(uri.host, uri.port)
  201. http.use_ssl = true
  202. response = http.get(uri.request_uri, { 'Authorization' => "#{token[:token_type]} #{token[:access_token]}" })
  203. if response.code != 200 && response.body.blank?
  204. Rails.logger.error "Request failed! (code: #{response.code})"
  205. raise "Request failed! (code: #{response.code})"
  206. end
  207. result = JSON.parse(response.body)
  208. if result['error'] && response.code != 200
  209. Rails.logger.error "Request failed! ERROR: #{result['error']['message']}"
  210. raise "Request failed! ERROR: #{result['error']['message']}"
  211. end
  212. aliases = []
  213. result['sendAs'].each do |row|
  214. next if row['isPrimary']
  215. next if !row['verificationStatus']
  216. next if row['verificationStatus'] != 'accepted'
  217. aliases.push({
  218. realname: row['displayName'],
  219. email: row['sendAsEmail'],
  220. })
  221. end
  222. aliases
  223. end
  224. def self.user_info(id_token)
  225. split = id_token.split('.')[1]
  226. return if split.blank?
  227. JSON.parse(Base64.decode64(split)).symbolize_keys
  228. end
  229. end