Compare commits

...
Author SHA1 Message Date
Shivam Mishra 56d9cc7e0b fix: use UPN for imap_login on Microsoft OAuth callback 2026-05-21 17:41:39 +05:30
Shivam Mishra 092f9ef500 feat: remove prompt param 2026-05-19 15:40:30 +05:30
5 changed files with 36 additions and 3 deletions
@@ -6,8 +6,7 @@ class Api::V1::Accounts::Microsoft::AuthorizationsController < Api::V1::Accounts
{ {
redirect_uri: "#{base_url}/microsoft/callback", redirect_uri: "#{base_url}/microsoft/callback",
scope: scope, scope: scope,
state: state, state: state
prompt: 'consent'
} }
) )
if redirect_url if redirect_url
@@ -14,4 +14,11 @@ class Microsoft::CallbacksController < OauthCallbackController
def imap_address def imap_address
'outlook.office365.com' 'outlook.office365.com'
end 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 end
+8 -1
View File
@@ -44,7 +44,7 @@ class OauthCallbackController < ApplicationController
def update_channel(channel_email) def update_channel(channel_email)
channel_email.update!({ 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, imap_port: '993', imap_enabled: true,
provider: provider_name, provider: provider_name,
provider_config: { provider_config: {
@@ -55,6 +55,13 @@ class OauthCallbackController < ApplicationController
}) })
end 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 def provider_name
raise NotImplementedError raise NotImplementedError
end end
@@ -43,6 +43,7 @@ RSpec.describe 'Microsoft Authorization API', type: :request do
] ]
expect(params['scope']).to eq(expected_scope) expect(params['scope']).to eq(expected_scope)
expect(params['redirect_uri']).to eq(["#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback"]) expect(params['redirect_uri']).to eq(["#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback"])
expect(url).not_to match(/(?:\?|&)prompt=/)
# Validate state parameter exists and can be decoded back to the account # Validate state parameter exists and can be decoded back to the account
expect(params['state']).to be_present expect(params['state']).to be_present
@@ -34,6 +34,25 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do
expect(inbox.channel.imap_address).to eq 'outlook.office365.com' expect(inbox.channel.imap_address).to eq 'outlook.office365.com'
end 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 it 'creates updates inbox channel config if inbox exists and authentication is successful' do
inbox = create(:channel_email, account: account, email: email)&.inbox inbox = create(:channel_email, account: account, email: email)&.inbox
expect(inbox.channel.provider_config).to eq({}) expect(inbox.channel.provider_config).to eq({})