diff --git a/app/controllers/microsoft/callbacks_controller.rb b/app/controllers/microsoft/callbacks_controller.rb index 2f07505fc..045789f75 100644 --- a/app/controllers/microsoft/callbacks_controller.rb +++ b/app/controllers/microsoft/callbacks_controller.rb @@ -14,4 +14,11 @@ class Microsoft::CallbacksController < OauthCallbackController def imap_address 'outlook.office365.com' end + + # Exchange Online's SMTP AUTH (XOAUTH2) rejects proxy addresses in the SASL `user=` field; + # it must match the token's UPN. `preferred_username` is the documented v2.0 claim; + # `upn` is the v1.0 fallback. + def imap_login_identity + users_data['preferred_username'] || users_data['upn'] || super + end end diff --git a/app/controllers/oauth_callback_controller.rb b/app/controllers/oauth_callback_controller.rb index be0fa5008..8929db987 100644 --- a/app/controllers/oauth_callback_controller.rb +++ b/app/controllers/oauth_callback_controller.rb @@ -44,7 +44,7 @@ class OauthCallbackController < ApplicationController def update_channel(channel_email) channel_email.update!({ - imap_login: users_data['email'], imap_address: imap_address, + imap_login: imap_login_identity, imap_address: imap_address, imap_port: '993', imap_enabled: true, provider: provider_name, provider_config: { @@ -55,6 +55,13 @@ class OauthCallbackController < ApplicationController }) end + # Identity used as the IMAP/SMTP login (SASL XOAUTH2 `user=` field). Defaults to the + # id_token's email claim; providers override when their server requires a different + # claim (e.g. Microsoft SMTP requires UPN). + def imap_login_identity + users_data['email'] + end + def provider_name raise NotImplementedError end diff --git a/spec/controllers/microsoft/callbacks_controller_spec.rb b/spec/controllers/microsoft/callbacks_controller_spec.rb index 6bd9a0583..4a7661f88 100644 --- a/spec/controllers/microsoft/callbacks_controller_spec.rb +++ b/spec/controllers/microsoft/callbacks_controller_spec.rb @@ -34,6 +34,25 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do expect(inbox.channel.imap_address).to eq 'outlook.office365.com' end + it 'sets imap_login from preferred_username when the id_token carries a UPN that differs from email' do + upn = 'livetestaccount@capeunionmart.co.za' + mailbox = 'LiveTestAccount@cumi.co.za' + response_body = { + id_token: JWT.encode({ email: mailbox, preferred_username: upn, name: 'test' }, false), + access_token: SecureRandom.hex(10), token_type: 'Bearer', refresh_token: SecureRandom.hex(10) + } + stub_request(:post, 'https://login.microsoftonline.com/common/oauth2/v2.0/token') + .with(body: { 'code' => code, 'grant_type' => 'authorization_code', + 'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback" }) + .to_return(status: 200, body: response_body.to_json, headers: { 'Content-Type' => 'application/json' }) + + get microsoft_callback_url, params: { code: code, state: state } + + channel = account.inboxes.last.channel + expect(channel.imap_login).to eq upn + expect(channel.email).to eq mailbox + end + it 'creates updates inbox channel config if inbox exists and authentication is successful' do inbox = create(:channel_email, account: account, email: email)&.inbox expect(inbox.channel.provider_config).to eq({})