SMTP OAuth2: Token Authentication for Email Clients
SMTP OAuth2 replaces a mailbox password in the SMTP AUTH exchange with a short-lived access token. Your application still submits mail over SMTP, usually after TLS is active. The change is how the client proves it may use the submission server.
In production, the base64 string is the easy part. The real work is obtaining the right token, keeping refresh credentials out of logs, refreshing once when authentication fails, and keeping that retry separate from message-delivery retries.
I’ll trace the protocol first, then turn it into a small state machine you can implement without treating every 535 as a bad password.
Start with the SMTP exchange
This hypothetical trace uses reserved .example names. The bearer token is deliberately redacted.
S: 220 smtp.provider.example ESMTP ready
C: EHLO app.sender.example
S: 250-smtp.provider.example
S: 250-STARTTLS
S: 250 AUTH PLAIN XOAUTH2
C: STARTTLS
S: 220 Ready to start TLS
... TLS negotiation and certificate validation ...
C: EHLO app.sender.example
S: 250 AUTH PLAIN XOAUTH2
C: AUTH XOAUTH2 dXNlcj1zZW5kZXJAZXhhbXBsZS5jb20B...
S: 235 2.7.0 Authentication successful
The sequence has four important boundaries.
EHLOdiscovers the authentication mechanisms that this server offers on this connection.STARTTLSprotects the bearer token in transit. After TLS starts, the client sendsEHLOagain because the advertised capabilities can change.AUTH XOAUTH2carries a base64-encoded username and access token.235means this SMTP AUTH exchange succeeded. It does not say that a recipient exists, that the message was accepted, or that delivery will succeed.
RFC 4954 defines the SMTP AUTH extension, including the AUTH capability and the 235 2.7.0 success reply. RFC 7628 defines OAuth SASL mechanisms and requires TLS for OAUTHBEARER because the token is a bearer credential.
That last word matters. Anyone who steals a usable bearer token can present it until it expires or is revoked. OAuth reduces the blast radius of a leaked mailbox password, but it does not make secret storage optional.
What is inside XOAUTH2?
Google and Microsoft both document the same XOAUTH2 client-response shape for SMTP:
base64(
"user=" + email_address + "\x01" +
"auth=Bearer " + access_token + "\x01\x01"
)
\x01 is the Control+A byte. It is not the two visible characters ^A, and it is not a newline. The completed value must be encoded as one base64 string and sent as the SASL response.
Here is a compact JavaScript helper. It creates the client response only. It does not obtain or refresh a token.
function xoauth2Response(emailAddress, accessToken) {
if (!emailAddress || !accessToken) {
throw new Error("emailAddress and accessToken are required");
}
const sasl =
`user=${emailAddress}\x01` +
`auth=Bearer ${accessToken}\x01\x01`;
return Buffer.from(sasl, "utf8").toString("base64");
}
Never print the returned value. Base64 is an encoding, not encryption. Anyone who decodes the string gets the access token.
There are two names you may encounter:
| Mechanism | Where it comes from | What the client should do |
|---|---|---|
OAUTHBEARER |
Standards-track RFC 7628 | Use it when the server advertises it and your library supports the RFC format. |
XOAUTH2 |
Provider-documented SASL mechanism | Use the provider’s documented format, scope, token endpoint, and mailbox rules. |
Do not choose a mechanism from a configuration guess. Read the post-TLS EHLO response and fail closed if the required mechanism is absent. Falling back silently to a password defeats the migration.
The token lifecycle is the implementation
A reliable mail client treats token state separately from SMTP state. I use a state machine like this:
| State | Action | Next state |
|---|---|---|
| No access token | Obtain one using the configured OAuth grant. | Ready, or reauthorization required |
| Token near expiry | Refresh before opening the SMTP session. | Ready |
| Ready | Connect, establish TLS, discover AUTH, then authenticate. | Authenticated, or auth failed |
| Auth failed | Invalidate the cached access token and refresh once. | Retry auth once, or reauthorization required |
| Reauthorization required | Stop automated retries and surface an operator or user action. | No token |
The single-refresh rule prevents a revoked grant, wrong audience, disabled account, or missing permission from turning into a tight retry loop. A fresh token can still fail if it was minted for the wrong resource or if the server policy does not allow SMTP submission.
RFC 9700, the OAuth 2.0 security best current practice, requires refresh-token rotation or sender-constraining for public clients and calls for refresh tokens to be protected in transit and storage. Your authorization server or provider may implement the rotation. Your client still has to persist the newest token atomically so two workers do not race with an invalidated predecessor.
A practical refresh lock can be small:
async function getAccessToken(accountId) {
const cached = await tokenStore.read(accountId);
if (cached && cached.expiresAt > Date.now() + 60_000) {
return cached.accessToken;
}
return refreshLocks.runExclusive(accountId, async () => {
const latest = await tokenStore.read(accountId);
if (latest && latest.expiresAt > Date.now() + 60_000) {
return latest.accessToken;
}
const refreshed = await oauthClient.refresh(latest.refreshToken);
await tokenStore.replaceAtomically(accountId, refreshed);
return refreshed.accessToken;
});
}
This is illustrative code, not a complete OAuth client. Use a maintained OAuth library for authorization-code validation, PKCE, token endpoint authentication, and provider discovery.
Provider scopes and mailbox policy still differ
XOAUTH2 is a wire format, not a universal permission model. As of November 14, 2026, the official provider documentation describes these delegated scopes:
| Provider | Documented SMTP scope | Important boundary |
|---|---|---|
https://mail.google.com/ |
The scope covers IMAP, POP, and SMTP access and is restricted. Google advises using the Gmail API when the full mail scope is unnecessary. | |
| Microsoft | https://outlook.office.com/SMTP.Send |
The delegated token must match the documented resource and the mailbox must be allowed to use SMTP AUTH. |
See Google’s XOAUTH2 protocol documentation and Microsoft’s SMTP OAuth documentation before shipping. Copying a scope or token endpoint from another provider can produce a perfectly valid token that the SMTP server must reject.
For service-to-service sending, do not assume a user refresh token is the only option. Some providers document application permissions or service principals, while others steer server applications toward a mail API. Choose the provider-supported grant for your workload, then keep the SMTP SASL step distinct from the OAuth grant step.
Keep authentication retries away from message retries
An authentication failure happens before MAIL FROM, so the server has not accepted a message in that SMTP transaction. It is normally safe to refresh the access token and retry authentication once on a new or clean session.
That does not make every later retry safe. Once you send DATA, a timeout can leave you unsure whether the submission server accepted the message. Refreshing an OAuth token does nothing to resolve that ambiguity.
| Failure point | What is known | Safe response |
|---|---|---|
Before 235 |
SMTP authentication did not complete. | Refresh once, reconnect, and retry AUTH. |
After 235, before MAIL FROM |
No message transaction has started. | Reconnect and reauthenticate if the session is lost. |
| After final message bytes, before final reply | Acceptance is ambiguous. | Reconcile or apply your duplicate-control policy before resending. |
After final 250 |
The submission server accepted responsibility. | Record the handoff. Do not send the same intent again. |
If your application can send the same business event more than once, pair the SMTP client with an outbox and stable idempotency key. I explain that failure window in How to Prevent Duplicate Transactional Emails. For the wider transport choice, see Email API vs SMTP.
Does OAuth replace TLS or email authentication?
No. This is the strongest fair objection to the whole migration: if SMTP is already protected by TLS, why add OAuth?
TLS protects the connection and the credential while it crosses the network. OAuth changes the credential itself. A short-lived, scoped, revocable token is less reusable than a mailbox password, and the refresh credential can remain in a controlled token store instead of being copied into every SMTP client.
But OAuth does not sign the message, align the visible From: domain, verify a recipient, or prove delivery. SPF, DKIM, and DMARC remain separate. So do mailbox confirmation and address verification. SMTP AUTH vs SPF, DKIM, and DMARC maps those evidence boundaries.
I would call the migration complete only when the client can demonstrate all of these behaviors in staging:
- It validates the SMTP server certificate and never sends a bearer token before TLS is active.
- It reads the post-TLS AUTH capabilities and refuses a silent password fallback.
- It keeps access tokens, refresh tokens, and encoded XOAUTH2 responses out of application logs and error trackers.
- It refreshes once on an authentication failure, then surfaces a durable reauthorization error.
- It serializes refreshes per account and persists rotated credentials atomically.
- It records authentication failures separately from message handoff failures, so a token problem cannot create a duplicate email.
That is the useful mental model: OAuth gets your client through the submission gate. Everything after that gate is still email delivery.