Skip to content

Authenticate M365 SMTP via OAuth2 certificate instead of password - #45

Merged
jsquyres merged 2 commits into
mainfrom
m365-oauth-cert-smtp
Jul 10, 2026
Merged

jsquyres merged 2 commits into
mainfrom
m365-oauth-cert-smtp

Conversation

@jsquyres

Copy link
Copy Markdown
Contributor

Why

Microsoft 365 has disabled Basic Authentication (username/password) for SMTP AUTH. The scheduled report in src/email_sender.py used smtplib.SMTP.login(sender, password), which M365 now rejects — the nightly email fails to send.

What

Switch to app-only (client-credentials) OAuth2 using a certificate, and send via the SMTP XOAUTH2 mechanism. No mailbox password is used or stored.

  • src/email_sender.py — acquire an app-only access token from Entra ID via MSAL using a certificate credential (private key + SHA-1 thumbprint computed from the PEM), then authenticate with AUTH XOAUTH2 instead of SMTP.login(). The smtp.office365.com:587 STARTTLS connection and domain→server mapping are unchanged. Also loads .env from the project root so cron runs resolve it regardless of working directory.
  • src/dialogueBox.py — the "Email Password" field is replaced by Tenant ID, Client ID, and Certificate Path (with a Browse file-picker), persisted as AZURE_TENANT_ID / AZURE_CLIENT_ID / AZURE_CERT_PATH.
  • requirements.txt / scripts/initial_setup.sh — add msal and cryptography; the cron .venv installs them too.
  • .env.example — EMAIL_PASSWORD → the AZURE_* keys.
  • README.md — new "Microsoft 365 email setup" section: app registration, SMTP.SendAsApp application permission + admin consent, self-signed certificate creation/upload, and Exchange Online mailbox scoping (New-ServicePrincipal / Add-MailboxPermission / Set-CASMailbox).

Admin prerequisites (one-time, per README)

Requires an Entra ID admin to register an app, grant SMTP.SendAsApp (admin consent), upload the public certificate, and scope the service principal to the sender mailbox.

Verification

  • Our computed SHA-1 thumbprint matches openssl x509 -fingerprint -sha1 and MSAL's own internal computation (cert.fingerprint(hashes.SHA1()).hex()).
  • The combined private-key + certificate PEM loads cleanly; MSAL accepts the credential dict.
  • Both edited files compile under the project venv.
  • Not exercised here: the live token exchange + real send, which need the actual tenant and an uploaded certificate. Validate on the target with .venv/bin/python src/email_sender.py — set_debuglevel(1) prints the SMTP conversation, including the 235 auth-success line.

jsquyres added 2 commits July 10, 2026 11:51
Microsoft 365 has disabled Basic Authentication (username/password) for
SMTP AUTH, so email_sender.py could no longer log in with a mailbox
password. Switch to app-only (client-credentials) OAuth2 using a
certificate, and send via the SMTP XOAUTH2 mechanism.

- email_sender.py: acquire an app-only access token from Entra ID via
  MSAL using a certificate credential (private key + SHA-1 thumbprint
  computed from the PEM), then authenticate with AUTH XOAUTH2 instead of
  SMTP.login(). Load .env from the project root so cron runs resolve it.
- dialogueBox.py: replace the "Email Password" field with Tenant ID,
  Client ID, and Certificate Path (with a Browse button); persist as
  AZURE_TENANT_ID / AZURE_CLIENT_ID / AZURE_CERT_PATH.
- requirements.txt / initial_setup.sh: add msal and cryptography (the
  cron venv installs them too).
- .env.example: replace EMAIL_PASSWORD with the AZURE_* keys.
- README.md: add a "Microsoft 365 email setup" section covering the app
  registration, SMTP.SendAsApp admin consent, self-signed certificate
  creation/upload, and Exchange Online mailbox scoping.
@jsquyres
jsquyres merged commit a98f8a5 into main Jul 10, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant