exchange.rb 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. class ExternalCredential::Exchange
  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: 'exchange')
  9. raise Exceptions::UnprocessableEntity, __('No Exchange 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. # client_tenant may be empty. Set only if key is nonexistant at all
  18. if !credentials.key? :client_tenant
  19. credentials[:client_tenant] = external_credential.credentials['client_tenant']
  20. end
  21. end
  22. raise Exceptions::UnprocessableEntity, __("The required parameter 'client_id' is missing.") if credentials[:client_id].blank?
  23. raise Exceptions::UnprocessableEntity, __("The required parameter 'client_secret' is missing.") if credentials[:client_secret].blank?
  24. authorize_url = generate_authorize_url(credentials)
  25. {
  26. authorize_url: authorize_url,
  27. }
  28. end
  29. def self.link_account(_request_token, params)
  30. # return to admin interface if admin Consent is in process and user clicks on "Back to app"
  31. return "#{Setting.get('http_type')}://#{Setting.get('fqdn')}/#system/integration/exchange/error/AADSTS65004" if params[:error_description].present? && params[:error_description].include?('AADSTS65004')
  32. external_credential = ExternalCredential.find_by(name: 'exchange')
  33. raise Exceptions::UnprocessableEntity, __('No Exchange app configured!') if !external_credential
  34. raise Exceptions::UnprocessableEntity, __("The required parameter 'code' is missing.") if !params[:code]
  35. response = authorize_tokens(external_credential.credentials, params[:code])
  36. %w[refresh_token access_token expires_in scope token_type id_token].each do |key|
  37. raise Exceptions::UnprocessableEntity, "No #{key} for authorization request found!" if response[key.to_sym].blank?
  38. end
  39. user_data = user_info(response[:id_token])
  40. raise Exceptions::UnprocessableEntity, __("The user's 'preferred_username' could not be extracted from 'id_token'.") if user_data[:preferred_username].blank?
  41. config = response.merge(
  42. user: user_data[:preferred_username],
  43. client_id: external_credential.credentials[:client_id],
  44. client_secret: external_credential.credentials[:client_secret],
  45. client_tenant: external_credential.credentials[:client_tenant],
  46. status: 200,
  47. )
  48. Setting.set('exchange_oauth', config)
  49. "#{Setting.get('http_type')}://#{Setting.get('fqdn')}/#system/integration/exchange/success/1"
  50. end
  51. def self.generate_authorize_url(credentials, scope = 'https://outlook.office365.com/EWS.AccessAsUser.All offline_access openid profile email')
  52. params = {
  53. 'client_id' => credentials[:client_id],
  54. 'redirect_uri' => ExternalCredential.callback_url('exchange'),
  55. 'scope' => scope,
  56. 'response_type' => 'code',
  57. 'access_type' => 'offline',
  58. 'prompt' => credentials[:prompt] || 'login',
  59. }
  60. tenant = credentials[:client_tenant].presence || 'common'
  61. uri = URI::HTTPS.build(
  62. host: 'login.microsoftonline.com',
  63. path: "/#{tenant}/oauth2/v2.0/authorize",
  64. query: params.to_query
  65. )
  66. uri.to_s
  67. end
  68. def self.authorize_tokens(credentials, authorization_code)
  69. uri = authorize_tokens_uri(credentials[:client_tenant])
  70. params = authorize_tokens_params(credentials, authorization_code)
  71. response = UserAgent.post(uri.to_s, params)
  72. if response.code != 200 && response.body.blank?
  73. Rails.logger.error "Request failed! (code: #{response.code})"
  74. raise "Request failed! (code: #{response.code})"
  75. end
  76. result = JSON.parse(response.body)
  77. if result['error'] && response.code != 200
  78. Rails.logger.error "Request failed! ERROR: #{result['error']} (#{result['error_description']}, params: #{params.to_json})"
  79. raise "Request failed! ERROR: #{result['error']} (#{result['error_description']})"
  80. end
  81. result[:created_at] = Time.zone.now
  82. result.symbolize_keys
  83. end
  84. def self.authorize_tokens_params(credentials, authorization_code)
  85. {
  86. client_secret: credentials[:client_secret],
  87. code: authorization_code,
  88. grant_type: 'authorization_code',
  89. client_id: credentials[:client_id],
  90. redirect_uri: ExternalCredential.callback_url('exchange'),
  91. }
  92. end
  93. def self.authorize_tokens_uri(tenant)
  94. URI::HTTPS.build(
  95. host: 'login.microsoftonline.com',
  96. path: "/#{tenant.presence || 'common'}/oauth2/v2.0/token",
  97. )
  98. end
  99. def self.refresh_token
  100. return {} if !Setting.get('exchange_integration')
  101. config = Setting.get('exchange_oauth')
  102. return {} if config.blank?
  103. return config if config[:created_at] >= 50.minutes.ago
  104. params = refresh_token_params(config)
  105. uri = refresh_token_uri(config)
  106. response = UserAgent.post(uri.to_s, params)
  107. if response.code != 200 && response.body.blank?
  108. HttpLog.create(
  109. direction: 'out',
  110. facility: 'EWS',
  111. url: uri,
  112. status: response.code,
  113. ip: nil,
  114. request: { content: params },
  115. response: { content: false },
  116. method: 'refresh_token',
  117. created_by_id: 1,
  118. updated_by_id: 1,
  119. )
  120. config_state(response.code)
  121. Rails.logger.error "Exchange refresh token: Request failed! (code: #{response.code})"
  122. raise "Request failed! (code: #{response.code})"
  123. end
  124. result = JSON.parse(response.body)
  125. if result['error'] && response.code != 200
  126. HttpLog.create(
  127. direction: 'out',
  128. facility: 'EWS',
  129. url: uri,
  130. status: response.code,
  131. ip: nil,
  132. request: { content: params },
  133. response: { content: result },
  134. method: 'refresh_token',
  135. created_by_id: 1,
  136. updated_by_id: 1,
  137. )
  138. config_state(response.code)
  139. Rails.logger.error "Exchange refresh token: Request failed! ERROR: #{result['error']} (#{result['error_description']}, params: #{params.to_json})"
  140. raise "Request failed! ERROR: #{result['error']} (#{result['error_description']})"
  141. end
  142. config = config.merge(result.symbolize_keys).merge(
  143. created_at: Time.zone.now,
  144. status: 200,
  145. )
  146. Setting.set('exchange_oauth', config)
  147. config
  148. end
  149. def self.config_state(status)
  150. config = Setting.get('exchange_oauth')
  151. config = config.merge(status: status)
  152. Setting.set('exchange_oauth', config)
  153. end
  154. def self.refresh_token_params(credentials)
  155. {
  156. client_id: credentials[:client_id],
  157. client_secret: credentials[:client_secret],
  158. refresh_token: credentials[:refresh_token],
  159. grant_type: 'refresh_token',
  160. }
  161. end
  162. def self.refresh_token_uri(credentials)
  163. tenant = credentials[:client_tenant].presence || 'common'
  164. URI::HTTPS.build(
  165. host: 'login.microsoftonline.com',
  166. path: "/#{tenant}/oauth2/v2.0/token",
  167. )
  168. end
  169. def self.user_info(id_token)
  170. split = id_token.split('.')[1]
  171. return if split.blank?
  172. JSON.parse(Base64.decode64(split)).symbolize_keys
  173. end
  174. end