From 4338a8ffd8367d215e4109d70e66d098ae99e57b Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Tue, 23 Jun 2026 20:02:21 +0530 Subject: [PATCH] fix: create email inbox when Microsoft id_token has no email claim --- .../microsoft/callbacks_controller.rb | 7 +++++++ app/controllers/oauth_callback_controller.rb | 13 +++++++++--- .../microsoft/callbacks_controller_spec.rb | 20 +++++++++++++++++++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/app/controllers/microsoft/callbacks_controller.rb b/app/controllers/microsoft/callbacks_controller.rb index 045789f75..3e2ea1bf6 100644 --- a/app/controllers/microsoft/callbacks_controller.rb +++ b/app/controllers/microsoft/callbacks_controller.rb @@ -21,4 +21,11 @@ class Microsoft::CallbacksController < OauthCallbackController def imap_login_identity users_data['preferred_username'] || users_data['upn'] || super end + + # Prefer the actual `email` claim. Microsoft work/school accounts without a mailbox + # omit it, returning only `preferred_username`/`upn`; fall back to those so inbox + # creation does not fail on a null email. + def email_address + super || users_data['preferred_username'] || users_data['upn'] + end end diff --git a/app/controllers/oauth_callback_controller.rb b/app/controllers/oauth_callback_controller.rb index 4a3d049a6..4bd017d9f 100644 --- a/app/controllers/oauth_callback_controller.rb +++ b/app/controllers/oauth_callback_controller.rb @@ -41,7 +41,7 @@ class OauthCallbackController < ApplicationController end def find_channel_by_email - Channel::Email.find_by(email: users_data['email'], account: account) + Channel::Email.find_by(email: email_address, account: account) end def update_channel(channel_email) @@ -64,6 +64,13 @@ class OauthCallbackController < ApplicationController users_data['email'] end + # Email address stored on the Channel::Email record and used to match an existing + # channel. Defaults to the id_token's `email` claim; providers override when that + # claim can be absent and a different one carries the address. + def email_address + users_data['email'] + end + def provider_name raise NotImplementedError end @@ -74,7 +81,7 @@ class OauthCallbackController < ApplicationController def create_channel_with_inbox ActiveRecord::Base.transaction do - channel_email = Channel::Email.create!(email: users_data['email'], account: account) + channel_email = Channel::Email.create!(email: email_address, account: account) account.inboxes.create!( account: account, @@ -119,7 +126,7 @@ class OauthCallbackController < ApplicationController # Fallback name, for when name field is missing from users_data def fallback_name - users_data['email'].split('@').first.parameterize.titleize + email_address.to_s.split('@').first.parameterize.titleize end def oauth_code diff --git a/spec/controllers/microsoft/callbacks_controller_spec.rb b/spec/controllers/microsoft/callbacks_controller_spec.rb index be13d5c84..8bded6a38 100644 --- a/spec/controllers/microsoft/callbacks_controller_spec.rb +++ b/spec/controllers/microsoft/callbacks_controller_spec.rb @@ -53,6 +53,26 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do expect(channel.email).to eq mailbox end + it 'falls back to preferred_username for the channel email when the id_token has no email claim' do + upn = 'testaccount@primary-domain.example' + response_body = { + id_token: JWT.encode({ preferred_username: upn, name: 'test' }, nil, 'none'), + 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 } + + expect(response).to redirect_to app_email_inbox_agents_url(account_id: account.id, inbox_id: account.inboxes.last.id) + expect(account.inboxes.count).to be 1 + channel = account.inboxes.last.channel + expect(channel.email).to eq upn + expect(channel.imap_login).to eq upn + 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({})