2022-01-01 13:38:12 +00:00
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
2021-06-01 12:20:20 +00:00
2020-10-27 14:05:10 +00:00
class ExternalCredential :: Microsoft365
2020-10-02 12:46:19 +00:00
def self . app_verify ( params )
request_account_to_link ( params , false )
params
end
def self . request_account_to_link ( credentials = { } , app_required = true )
2020-10-27 14:05:10 +00:00
external_credential = ExternalCredential . find_by ( name : 'microsoft365' )
2021-11-15 15:58:19 +00:00
raise Exceptions :: UnprocessableEntity , __ ( 'No Microsoft365 app configured!' ) if ! external_credential && app_required
2020-10-02 12:46:19 +00:00
if external_credential
if credentials [ :client_id ] . blank?
credentials [ :client_id ] = external_credential . credentials [ 'client_id' ]
end
if credentials [ :client_secret ] . blank?
credentials [ :client_secret ] = external_credential . credentials [ 'client_secret' ]
end
2021-03-08 13:13:15 +00:00
# client_tenant may be empty. Set only if key is nonexistant at all
if ! credentials . key? :client_tenant
credentials [ :client_tenant ] = external_credential . credentials [ 'client_tenant' ]
end
2020-10-02 12:46:19 +00:00
end
2021-11-15 15:58:19 +00:00
raise Exceptions :: UnprocessableEntity , __ ( 'No client_id param!' ) if credentials [ :client_id ] . blank?
raise Exceptions :: UnprocessableEntity , __ ( 'No client_secret param!' ) if credentials [ :client_secret ] . blank?
2020-10-02 12:46:19 +00:00
2021-03-08 13:13:15 +00:00
authorize_url = generate_authorize_url ( credentials )
2020-10-02 12:46:19 +00:00
{
authorize_url : authorize_url ,
}
end
def self . link_account ( _request_token , params )
2020-10-27 14:05:10 +00:00
external_credential = ExternalCredential . find_by ( name : 'microsoft365' )
2021-11-15 15:58:19 +00:00
raise Exceptions :: UnprocessableEntity , __ ( 'No Microsoft365 app configured!' ) if ! external_credential
raise Exceptions :: UnprocessableEntity , __ ( 'No code for session found!' ) if ! params [ :code ]
2020-10-02 12:46:19 +00:00
2021-03-08 13:13:15 +00:00
response = authorize_tokens ( external_credential . credentials , params [ :code ] )
2020-10-02 12:46:19 +00:00
%w[ refresh_token access_token expires_in scope token_type id_token ] . each do | key |
raise Exceptions :: UnprocessableEntity , " No #{ key } for authorization request found! " if response [ key . to_sym ] . blank?
end
user_data = user_info ( response [ :id_token ] )
2022-01-19 13:59:52 +00:00
raise Exceptions :: UnprocessableEntity , __ ( " The user's 'preferred_username' could not be extracted from 'id_token'. " ) if user_data [ :preferred_username ] . blank?
2020-10-02 12:46:19 +00:00
channel_options = {
inbound : {
adapter : 'imap' ,
options : {
auth_type : 'XOAUTH2' ,
host : 'outlook.office365.com' ,
ssl : true ,
user : user_data [ :preferred_username ] ,
} ,
} ,
outbound : {
adapter : 'smtp' ,
options : {
host : 'smtp.office365.com' ,
port : 587 ,
user : user_data [ :preferred_username ] ,
authentication : 'xoauth2' ,
} ,
} ,
auth : response . merge (
2020-10-27 14:05:10 +00:00
provider : 'microsoft365' ,
2020-10-02 12:46:19 +00:00
type : 'XOAUTH2' ,
client_id : external_credential . credentials [ :client_id ] ,
client_secret : external_credential . credentials [ :client_secret ] ,
2021-03-08 13:13:15 +00:00
client_tenant : external_credential . credentials [ :client_tenant ] ,
2020-10-02 12:46:19 +00:00
) ,
}
2020-11-20 13:58:57 +00:00
if params [ :channel_id ]
existing_channel = Channel . where ( area : 'Microsoft365::Account' ) . find ( params [ :channel_id ] )
existing_channel . update! (
options : channel_options ,
)
existing_channel . refresh_xoauth2!
return existing_channel
end
migrate_channel = nil
Channel . where ( area : 'Email::Account' ) . find_each do | channel |
2022-02-10 11:38:31 +00:00
next if channel . options . dig ( :inbound , :options , :user ) & . downcase != user_data [ :email ] . downcase
next if channel . options . dig ( :inbound , :options , :host ) & . downcase != 'outlook.office365.com'
next if channel . options . dig ( :outbound , :options , :user ) & . downcase != user_data [ :email ] . downcase
next if channel . options . dig ( :outbound , :options , :host ) & . downcase != 'smtp.office365.com'
2020-11-20 13:58:57 +00:00
migrate_channel = channel
break
end
2020-10-02 12:46:19 +00:00
if migrate_channel
channel_options [ :inbound ] [ :options ] [ :folder ] = migrate_channel . options [ :inbound ] [ :options ] [ :folder ]
channel_options [ :inbound ] [ :options ] [ :keep_on_server ] = migrate_channel . options [ :inbound ] [ :options ] [ :keep_on_server ]
backup = {
attributes : {
area : migrate_channel . area ,
options : migrate_channel . options ,
last_log_in : migrate_channel . last_log_in ,
last_log_out : migrate_channel . last_log_out ,
status_in : migrate_channel . status_in ,
status_out : migrate_channel . status_out ,
} ,
migrated_at : Time . zone . now ,
}
migrate_channel . update (
2020-10-27 14:05:10 +00:00
area : 'Microsoft365::Account' ,
2020-10-02 12:46:19 +00:00
options : channel_options . merge ( backup_imap_classic : backup ) ,
last_log_in : nil ,
last_log_out : nil ,
)
return migrate_channel
end
2020-10-07 13:21:21 +00:00
email_addresses = [
{
realname : " #{ Setting . get ( 'product_name' ) } Support " ,
email : user_data [ :preferred_username ] ,
} ,
]
2020-10-02 12:46:19 +00:00
email_addresses . each do | email |
2020-10-07 13:21:21 +00:00
next if ! EmailAddress . exists? ( email : email [ :email ] )
2020-10-02 12:46:19 +00:00
2020-10-07 13:21:21 +00:00
raise Exceptions :: UnprocessableEntity , " Duplicate email address or email alias #{ email [ :email ] } found! "
2020-10-02 12:46:19 +00:00
end
# create channel
channel = Channel . create! (
2020-10-27 14:05:10 +00:00
area : 'Microsoft365::Account' ,
2020-10-02 12:46:19 +00:00
group_id : Group . first . id ,
options : channel_options ,
active : false ,
created_by_id : 1 ,
updated_by_id : 1 ,
)
email_addresses . each do | user_alias |
EmailAddress . create! (
channel_id : channel . id ,
realname : user_alias [ :realname ] ,
email : user_alias [ :email ] ,
active : true ,
created_by_id : 1 ,
updated_by_id : 1 ,
)
end
channel
end
2021-03-08 13:13:15 +00:00
def self . generate_authorize_url ( credentials , scope = 'https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send offline_access openid profile email' )
2020-10-02 12:46:19 +00:00
params = {
2021-03-08 13:13:15 +00:00
'client_id' = > credentials [ :client_id ] ,
2020-10-27 14:05:10 +00:00
'redirect_uri' = > ExternalCredential . callback_url ( 'microsoft365' ) ,
2020-10-02 12:46:19 +00:00
'scope' = > scope ,
'response_type' = > 'code' ,
'access_type' = > 'offline' ,
'prompt' = > 'consent' ,
}
2021-03-08 13:13:15 +00:00
tenant = credentials [ :client_tenant ] . presence || 'common'
2020-10-02 12:46:19 +00:00
uri = URI :: HTTPS . build (
host : 'login.microsoftonline.com' ,
2021-03-08 13:13:15 +00:00
path : " / #{ tenant } /oauth2/v2.0/authorize " ,
2020-10-02 12:46:19 +00:00
query : params . to_query
)
uri . to_s
end
2021-03-08 13:13:15 +00:00
def self . authorize_tokens ( credentials , authorization_code )
uri = authorize_tokens_uri ( credentials [ :client_tenant ] )
params = authorize_tokens_params ( credentials , authorization_code )
2020-10-02 12:46:19 +00:00
response = Net :: HTTP . post_form ( uri , params )
if response . code != 200 && response . body . blank?
Rails . logger . error " Request failed! (code: #{ response . code } ) "
raise " Request failed! (code: #{ response . code } ) "
end
result = JSON . parse ( response . body )
if result [ 'error' ] && response . code != 200
Rails . logger . error " Request failed! ERROR: #{ result [ 'error' ] } ( #{ result [ 'error_description' ] } , params: #{ params . to_json } ) "
raise " Request failed! ERROR: #{ result [ 'error' ] } ( #{ result [ 'error_description' ] } ) "
end
result [ :created_at ] = Time . zone . now
result . symbolize_keys
end
2021-03-08 13:13:15 +00:00
def self . authorize_tokens_params ( credentials , authorization_code )
{
2021-05-18 11:27:02 +00:00
client_secret : credentials [ :client_secret ] ,
code : authorization_code ,
grant_type : 'authorization_code' ,
client_id : credentials [ :client_id ] ,
redirect_uri : ExternalCredential . callback_url ( 'microsoft365' ) ,
2020-10-02 12:46:19 +00:00
}
2021-03-08 13:13:15 +00:00
end
def self . authorize_tokens_uri ( tenant )
URI :: HTTPS . build (
2020-10-02 12:46:19 +00:00
host : 'login.microsoftonline.com' ,
2021-03-08 13:13:15 +00:00
path : " / #{ tenant . presence || 'common' } /oauth2/v2.0/token " ,
2020-10-02 12:46:19 +00:00
)
2021-03-08 13:13:15 +00:00
end
def self . refresh_token ( token )
2022-01-03 09:47:32 +00:00
return token if token [ :created_at ] > = 50 . minutes . ago
2021-03-08 13:13:15 +00:00
params = refresh_token_params ( token )
uri = refresh_token_uri ( token )
2020-10-02 12:46:19 +00:00
response = Net :: HTTP . post_form ( uri , params )
if response . code != 200 && response . body . blank?
Rails . logger . error " Request failed! (code: #{ response . code } ) "
raise " Request failed! (code: #{ response . code } ) "
end
result = JSON . parse ( response . body )
if result [ 'error' ] && response . code != 200
Rails . logger . error " Request failed! ERROR: #{ result [ 'error' ] } ( #{ result [ 'error_description' ] } , params: #{ params . to_json } ) "
raise " Request failed! ERROR: #{ result [ 'error' ] } ( #{ result [ 'error_description' ] } ) "
end
2021-02-25 17:46:39 +00:00
token . merge ( result . symbolize_keys ) . merge (
created_at : Time . zone . now ,
)
2020-10-02 12:46:19 +00:00
end
2021-03-08 13:13:15 +00:00
def self . refresh_token_params ( credentials )
{
2021-05-18 11:27:02 +00:00
client_id : credentials [ :client_id ] ,
client_secret : credentials [ :client_secret ] ,
refresh_token : credentials [ :refresh_token ] ,
grant_type : 'refresh_token' ,
2021-03-08 13:13:15 +00:00
}
end
def self . refresh_token_uri ( credentials )
tenant = credentials [ :client_tenant ] . presence || 'common'
URI :: HTTPS . build (
host : 'login.microsoftonline.com' ,
path : " / #{ tenant } /oauth2/v2.0/token " ,
)
end
2020-10-02 12:46:19 +00:00
def self . user_info ( id_token )
split = id_token . split ( '.' ) [ 1 ]
return if split . blank?
JSON . parse ( Base64 . decode64 ( split ) ) . symbolize_keys
end
end