I have set up my application on Entra with the appropriate permissions but apparently I might have missed something since the authentication is failing.
I am trying to access the user’s mailbox with IMAP using imaplib after they have logged in with their credentials but I am encountering this error:
127.0.0.1 - - [17/Aug/2024 18:57:30] "GET /callback?code=M.C547_BAY.2.U.<auth code>&state=abcdefghijklmnop HTTP/1.1" 200 -
57:32.87 > b'GKLO1 AUTHENTICATE XOAUTH2'
57:32.90 < b'+ '
57:32.91 write literal size 2064
57:32.97 < b'GKLO1 NO AUTHENTICATE failed.'
57:32.97 NO response: b'AUTHENTICATE failed.'
IMAP Authentication failed: AUTHENTICATE failed.
57:32.97 > b'GKLO2 LOGOUT'
57:33.01 < b'* BYE Microsoft Exchange Server IMAP4 server signing off.'
57:33.01 BYE response: b'Microsoft Exchange Server IMAP4 server signing off.'
This is my code:
import requests
import imaplib
import base64
import ssl
from urllib.parse import urlencode, urlparse, parse_qs
from http.server import BaseHTTPRequestHandler, HTTPServer
import webbrowser
import threading
# Configuration
client_id = '<client id>'
client_secret = '<client secret>'
tenant_id = 'common'
redirect_uri = 'http://localhost:8000/callback'
scope = 'https://outlook.office.com/IMAP.AccessAsUser.All offline_access'
auth_url = f'https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/authorize'
token_url = f'https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token'
# Global variable to store the email address
email_address = None
# Step 1: Generate the authorization URL
def get_auth_url(email):
auth_params = {
'client_id': client_id,
'response_type': 'code',
'redirect_uri': redirect_uri,
'response_mode': 'query',
'scope': scope,
'state': 'abcdefghijklmnop',
'login_hint': email # Add email as login_hint
}
return f"{auth_url}?{urlencode(auth_params)}"
# Step 2: Handle the OAuth2 Redirect Locally
class OAuthHandler(BaseHTTPRequestHandler):
def do_GET(self):
parsed_url = urlparse(self.path)
query_params = parse_qs(parsed_url.query)
self.send_response(200)
self.end_headers()
self.wfile.write(b"Authentication complete. You can close this window.")
auth_code = query_params.get('code', [None])[0]
if auth_code:
threading.Thread(target=exchange_code_for_token, args=(auth_code,)).start()
def run_local_server():
server_address = ('', 8000)
httpd = HTTPServer(server_address, OAuthHandler)
httpd.handle_request()
# Step 3: Open the auth URL in the user's default browser
def authenticate_user(email):
global email_address
email_address = email
auth_url_with_email = get_auth_url(email)
threading.Thread(target=run_local_server).start()
webbrowser.open(auth_url_with_email)
# Step 4: Exchange the authorization code for an access token
def exchange_code_for_token(auth_code):
token_data = {
'grant_type': 'authorization_code',
'client_id': client_id,
'client_secret': client_secret,
'redirect_uri': redirect_uri,
'code': auth_code,
'scope': scope
}
response = requests.post(token_url, data=token_data)
token_response = response.json()
access_token = token_response.get('access_token')
if access_token:
connect_to_imap(access_token)
else:
print("Failed to obtain access token:", token_response)
# Step 5: Connect to IMAP using the access token
def connect_to_imap(access_token):
imap_server = 'outlook.office365.com'
imap_port = 993
context = ssl.create_default_context()
try:
connection = imaplib.IMAP4_SSL(imap_server, imap_port, ssl_context=context)
connection.debug = 4 # Enable debug output
auth_string = f'user={email_address}x01auth=Bearer {access_token}x01x01'
auth_string = base64.b64encode(auth_string.encode('ascii')).decode('ascii')
connection.authenticate('XOAUTH2', lambda x: auth_string)
print("IMAP Authentication successful!")
connection.select("INBOX")
status, messages = connection.search(None, "ALL")
print(f"Number of emails in INBOX: {len(messages[0].split())}")
except imaplib.IMAP4.error as e:
print("IMAP Authentication failed:", str(e))
except Exception as e:
print("An error occurred:", str(e))
finally:
if 'connection' in locals() and connection:
connection.logout()
# Main execution
if __name__ == "__main__":
user_email = "[email protected]"
authenticate_user(user_email)
If I use MSAL to authenticate my email, I get a detailed error log telling me to move my multi-tenant application to an enterprise application, but I believe this might be overkill since I only wish to access mailboxes for the users who have signed in.
{'error': 'invalid_client', 'error_description': 'AADSTS7000229: The client application <client id> is missing service principal in the tenant <insert id here>.
See instructions here: https://go.microsoft.com/fwlink/?linkid=2225119 Trace ID: 5ba88408-1488-40c0-9a65-56d6ffeb8500 Correlation ID: 9cb2684f-df48-4c37-8548-36f7dfe6ab98 Timestamp: 2024-08-17 12:57:03Z',
'error_codes': [7000229], 'timestamp': '2024-08-17 12:57:03Z', 'trace_id': '5ba88408-1488-40c0-9a65-56d6ffeb8500',
'correlation_id': '9cb2684f-df48-4c37-8548-36f7dfe6ab98', 'error_uri': 'https://login.microsoftonline.com/error?code=7000229'}