📌 Change Summary
Since September 25, 2025, Microsoft has permanently disabled Basic Authentication (username/password) in Exchange Online (Microsoft 365).
Only OAuth 2.0 (Modern Authentication) — using temporary access tokens — is now supported for all apps, services, and devices. For more insights and to explore my other repositories or access this post in Portuguese, be sure to visit my GitHub profile at my GitHub.
Common error after the change:
550 5.7.30 Basic authentication is not supported for Client Submission
🔄 What Changed
- ❌ Basic Authentication (
user/password) is no longer supported - ❌ App Passwords have been discontinued
-
✅ OAuth 2.0 (Modern Auth) is now mandatory
- Tokens valid for about 1 hour
- Issued by Microsoft Entra ID (Azure AD)
- Supports MFA and Conditional Access
⚙️ Configuration — Microsoft Graph (OAuth 2.0 + Token)
1️⃣ Prerequisites
- Access to Entra ID: https://entra.microsoft.com/
-
Valid mailbox (e.g.,
sender@yourdomain.com) - Verified domain in your tenant
- Tools: cURL/Postman or Go
2️⃣ Register the App
- Go to App registrations → New registration
-
Name:
Email Relay OAuth - Supported account types: Single tenant
- Redirect URI: not required
- Click Register
Copy: **Directory (tenant) ID** → `TENANT_ID` **Application (client) ID** → `CLIENT_ID`
- Go to Certificates & secrets → New client secret
- Define an expiration and copy the *Value* (will not be shown again)
- API permissions → Add a permission → Microsoft Graph → Application permissions
- Select
Mail.Send→ Click Grant admin consent
3️⃣ Generate OAuth 2.0 Token
Client Credentials Flow:
curl -X POST https://login.microsoftonline.com/<TENANT_ID>/oauth2/v2.0/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=<CLIENT_ID>" \
-d "client_secret=<CLIENT_SECRET>" \
-d "scope=https://graph.microsoft.com/.default" \
-d "grant_type=client_credentials"
Expected Response:
{
"token_type": "Bearer",
"expires_in": 3599,
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGci..."
}
Use access_token in your header: Authorization: Bearer <token>.
#️⃣ Send Email Example (Go)
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
)
type tokenResp struct{ AccessToken string `json:"access_token"` }
func getToken(tenantID, clientID, clientSecret string) (string, error) {
endpoint := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", tenantID)
form := url.Values{
"client_id": {clientID},
"client_secret": {clientSecret},
"scope": {"https://graph.microsoft.com/.default"},
"grant_type": {"client_credentials"},
}
req, _ := http.NewRequest("POST", endpoint, bytes.NewBufferString(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := http.DefaultClient.Do(req)
if err != nil { return "", err }
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("token fail: %s - %s", resp.Status, string(b)) }
var tr tokenResp
json.Unmarshal(b, &tr)
return tr.AccessToken, nil
}
func sendMail(sender, token string) error {
body := map[string]any{
"message": map[string]any{
"subject": "Test – Microsoft Graph API",
"body": map[string]string{"contentType": "Text", "content": "Automatic email sent via OAuth 2.0 (Go)."},
"toRecipients": []map[string]any{
{"emailAddress": map[string]string{"address": "recipient@example.com"}},
},
},
"saveToSentItems": false,
}
b, _ := json.Marshal(body)
req, _ := http.NewRequest("POST", fmt.Sprintf("https://graph.microsoft.com/v1.0/users/%s/sendMail", url.PathEscape(sender)), bytes.NewReader(b))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { return err }
defer resp.Body.Close()
if resp.StatusCode != http.StatusAccepted { rb, _ := io.ReadAll(resp.Body); return fmt.Errorf("sendMail fail: %s - %s", resp.Status, string(rb)) }
return nil
}
✅ Successful response:
HTTP 202 Accepted
📨 SMTP Relay (Exchange Online)
1️⃣ Prerequisites
-
Fixed public IP (e.g.,
191.209.58.228) - Verified domain in Microsoft 365
- TCP Port 25 open (outbound/inbound)
- PTR/rDNS configured to point to your domain hostname
2️⃣ Create Connector
Access: https://admin.cloud.microsoft/exchange?#/connectors
- From: Your organization’s email server
- To: Microsoft 365
- Name:
SMTP Relay – On‑prem - Identify by IP → add your public IP
⚠️ Mail must originate from the authorized IP; otherwise, relay will fail.
3️⃣ Configure SPF Record
Add/edit TXT (only one SPF per domain):
v=spf1 ip4:191.209.58.228 include:spf.protection.outlook.com -all
Merge if needed; multiple SPF records will cause failure.
4️⃣ Find Smart Host (MX)
nslookup -type=mx yourdomain.com
Use the result as the smart host (port 25), e.g.:
yourdomain-com.mail.protection.outlook.com
#️⃣ Send Email Example (Go)
package main
import (
"fmt"
"log"
"net/smtp"
"os"
)
// Relay configuration
const (
smtpHost = "yourdomain-com.mail.protection.outlook.com" // Smart host for your domain (MX)
smtpPort = "25" // Default relay port
)
func main() {
// Sender address (must belong to the verified domain/tenant)
from := "sender@yourdomain.com"
to := []string{"recipient@example.com"}
// Message body
msg := []byte("To: recipient@example.com\r\n" +
"Subject: SMTP Relay Test (Go)\r\n" +
"Content-Type: text/plain; charset=UTF-8\r\n\r\n" +
"Hello!\nThis is a test message sent through SMTP Relay (IP-authenticated) in Microsoft 365.\n" +
"Sent using Go and net/smtp.\r\n")
// Server address
addr := fmt.Sprintf("%s:%s", smtpHost, smtpPort)
log.Printf("🔄 Connecting to %s to send email...", addr)
// Send message without authentication (relay authorized by IP)
err := smtp.SendMail(addr, nil, from, to, msg)
if err != nil {
log.Fatalf("❌ Failed to send email: %v", err)
}
fmt.Println("✅ Email successfully sent via SMTP Relay.")
}
Top comments (0)