fix(microsoft-shared): drop CGI.escape on UPN; add scope+status logging on Graph access check

Two changes to debug the 'You don't have access' rejection in the
OAuth callback flow:

- Use literal '@' in the /users/{upn} path. Microsoft Graph accepts both
  forms in most cases but the URL-encoded %40 form has been seen to fail
  with 404 on some endpoints. Literal @ is more reliable.
- Log the actual HTTP status, response body (first 500 chars), and the
  granted scopes from the access token's 'scp' claim. Makes future
  debugging of permission/scope issues immediate from production.log
  instead of a binary 'access denied' message.

Token decoding uses JWT.decode with verify=false — this is our own
token so verifying signature isn't useful here, we just inspect the scp
claim to confirm what scopes Microsoft actually granted.
This commit is contained in:
netlas
2026-04-27 14:44:48 +03:00
parent 9912342e61
commit 2317324950

View File

@@ -35,13 +35,31 @@ class MicrosoftShared::CallbacksController < ApplicationController
end
def mailbox_accessible?
response = Faraday.new(url: GRAPH_BASE_URL).get("/users/#{CGI.escape(@upn)}/mailFolders/inbox") do |req|
response = Faraday.new(url: GRAPH_BASE_URL).get("/users/#{@upn}/mailFolders/inbox") do |req|
req.headers['Authorization'] = "Bearer #{access_token}"
end
response.status == 200
rescue StandardError => e
Rails.logger.warn("Microsoft Shared mailbox access check failed: #{e.message}")
if response.status == 200
Rails.logger.info("Microsoft Shared mailbox access verified: upn=#{@upn} granted_scopes=#{token_scopes.inspect}")
return true
end
Rails.logger.warn(
"Microsoft Shared mailbox access denied: upn=#{@upn} status=#{response.status} " \
"granted_scopes=#{token_scopes.inspect} body=#{response.body.to_s[0, 500]}"
)
false
rescue StandardError => e
Rails.logger.warn("Microsoft Shared mailbox access check raised: upn=#{@upn} #{e.class}: #{e.message}")
false
end
# Decode the JWT payload (without verifying signature — just inspecting our own token's scopes
# for debugging purposes). Microsoft Graph access tokens are JWTs with scope info in `scp`.
def token_scopes
JWT.decode(access_token, nil, false).first['scp']
rescue StandardError
'unparseable'
end
def create_channel_and_inbox