OAuth2 Token Refresh for aerc
Problem
Email accounts failing in aerc with:
Failed to refresh token: exit status 1
Or for Outlook specifically:
[Outlook].source: failed to read password: Failed to refresh token
Root cause: Refresh tokens expired or revoked.
Common triggers:
-
Outlook: Microsoft tokens expire after 90 days of inactivity
-
Gmail: Google Cloud app in "Testing" mode (tokens expire after 7 days)
Permanent Fix (Gmail Only)
Publish your Google Cloud app to production mode:
-
Change status from Testing β In production
-
Tokens will no longer expire after 7 days
Alternative: Gmail App Passwords (No OAuth)
|
Why consider this? Most Unix greybeards (Linus, etc.) avoid OAuth entirely. They either self-host mail or use providers that support app passwords. OAuth token refresh is a Microsoft/Google invention that CLI users tolerate, not embrace. Outlook.com (personal): No app password option since 2024 - OAuth is mandatory. Gmail: Still supports app passwords if you have 2FA enabled. |
Prerequisites
-
2-Factor Authentication enabled on your Google account
-
Go to: myaccount.google.com/security and verify "2-Step Verification" is ON
Generate App Password
-
Select app: Mail
-
Select device: Other (Custom name) β enter "aerc linux"
-
Click Generate
-
Copy the 16-character password (spaces don’t matter)
Store App Password
Encrypt and store the app password:
# Create password file (replace YOUR_APP_PASSWORD)
echo "YOUR_APP_PASSWORD" | age -e -R ~/.secrets/.metadata/keys/master.age.pub \
-o ~/.secrets/email/gmail-primary-apppass.age
chmod 600 ~/.secrets/email/gmail-primary-apppass.age
Update accounts.conf
Change from OAuth2 to plain password auth:
[Gmail-Primary]
# OLD (OAuth2):
# source = imaps+xoauth2://evanrosado100%40gmail.com@imap.gmail.com:993
# source-cred-cmd = ~/atelier/_projects/personal/email-config/scripts/oauth2-token.sh gmail-primary
# NEW (App Password):
source = imaps://evanrosado100%40gmail.com@imap.gmail.com:993
source-cred-cmd = age -d -i ~/.secrets/.metadata/keys/master.age.key ~/.secrets/email/gmail-primary-apppass.age
outgoing = smtps://evanrosado100%40gmail.com@smtp.gmail.com:465
outgoing-cred-cmd = age -d -i ~/.secrets/.metadata/keys/master.age.key ~/.secrets/email/gmail-primary-apppass.age
|
Note the protocol changes:
App passwords use standard TLS, not OAuth2 bearer tokens. |
Verify
# Test decryption
age -d -i ~/.secrets/.metadata/keys/master.age.key ~/.secrets/email/gmail-primary-apppass.age
# Restart aerc
pkill aerc && aerc
Trade-offs
| OAuth2 | App Passwords |
|---|---|
Token expires, needs refresh |
Password never expires (until revoked) |
More "secure" (Google’s view) |
Simpler, Unix-friendly |
Requires custom scripts |
Native aerc support |
Complex debugging |
Easy to troubleshoot |
Microsoft forces this |
Google still allows choice |
Check Token Status
Before re-authenticating, check which tokens are expired:
AGE_KEY=~/.secrets/.metadata/keys/master.age.key
# Check all accounts at once
for acct in outlook gmail-primary gmail-secondary; do
echo "=== $acct ==="
age -d -i $AGE_KEY ~/.secrets/email/${acct}-tokens.json.age 2>/dev/null | \
jq -r '"Expires: \(.expires_at | todate)"' || echo "No token file"
done
Check a single account with more detail:
age -d -i $AGE_KEY ~/.secrets/email/outlook-tokens.json.age | \
jq '{account: "outlook", expires_at: .expires_at, expires_human: (.expires_at | todate), client_id: .client_id}'
Outlook Re-authentication
1. Get Client ID
Outlook uses a public client (no secret required for personal accounts):
AGE_KEY=~/.secrets/.metadata/keys/master.age.key
age -d -i $AGE_KEY ~/.secrets/email/outlook-tokens.json.age | jq -r '.client_id'
2. Run Setup Script
~/atelier/_projects/personal/email-config/scripts/setup-oauth2.sh outlook
3. Complete Authorization
-
Paste Client ID when prompted
-
Leave Client Secret blank (press Enter) - Outlook public client doesn’t need it
-
Open the provided URL in browser
-
Sign in with
evan.rosado@outlook.com -
Copy the code from redirect URL (value after
code=until&or end of URL)-
Microsoft codes start with
M.C…
-
-
Paste code when prompted
Gmail Re-authentication
1. Get Credentials
Gmail requires both Client ID and Client Secret:
AGE_KEY=~/.secrets/.metadata/keys/master.age.key
age -d -i $AGE_KEY ~/.secrets/email/gmail-primary-tokens.json.age | \
jq -r '"Client ID: \(.client_id)\nClient Secret: \(.client_secret)"'
Same credentials work for all Gmail accounts.
2. Run Setup Script
# Primary account (evanrosado100@gmail.com)
~/atelier/_projects/personal/email-config/scripts/setup-oauth2.sh gmail-primary
# Secondary account (erjunior1983@gmail.com)
~/atelier/_projects/personal/email-config/scripts/setup-oauth2.sh gmail-secondary
3. Complete Authorization
-
Paste Client ID when prompted
-
Paste Client Secret when prompted
-
Open the provided URL in browser
-
CRITICAL: Authorize with the correct Gmail account
-
gmail-primaryβevanrosado100@gmail.com -
gmail-secondaryβerjunior1983@gmail.com
-
-
Copy the code from redirect URL (value after
code=until&scope=)-
Google codes start with
4/0A…
-
-
Paste code when prompted
|
If you authorize with the wrong Gmail account, the token won’t work. The token must match the email in |
Verify and Restart
Verify Tokens Work
# Outlook (returns long token string)
~/atelier/_projects/personal/email-config/scripts/oauth2-token.sh outlook
# Gmail (returns token starting with ya29.)
~/atelier/_projects/personal/email-config/scripts/oauth2-token.sh gmail-primary
~/atelier/_projects/personal/email-config/scripts/oauth2-token.sh gmail-secondary
Restart aerc
pkill aerc && aerc
Files Involved
| File | Purpose |
|---|---|
|
Account configuration (email, IMAP, SMTP settings) |
|
Encrypted OAuth tokens for Outlook |
|
Encrypted OAuth tokens for Gmail primary |
|
Encrypted OAuth tokens for Gmail secondary |
|
Initial OAuth authorization flow |
|
Token refresh script (called by aerc) |
Troubleshooting
Token works but aerc rejects it
Cause: You authorized the wrong Gmail account in the browser.
Fix: Re-run setup-oauth2.sh and ensure you’re logged into the correct account before authorizing.
"invalid_grant" persists after re-auth
-
Check Google Cloud Console for app status
-
Revoke all tokens: myaccount.google.com/permissions
-
Re-authorize from scratch
Script can’t find token file
Check the token file exists and is readable:
ls -la ~/.secrets/email/*.age
If you renamed accounts, ensure token files match:
- gmail-primary-tokens.json.age
- gmail-secondary-tokens.json.age
Quick Reference
AGE_KEY=~/.secrets/.metadata/keys/master.age.key
# Check all token expiry dates
for acct in outlook gmail-primary gmail-secondary; do
echo "=== $acct ==="
age -d -i $AGE_KEY ~/.secrets/email/${acct}-tokens.json.age 2>/dev/null | \
jq -r '"Expires: \(.expires_at | todate)"' || echo "No token file"
done
# View full token JSON for an account
age -d -i $AGE_KEY ~/.secrets/email/outlook-tokens.json.age | jq .
# Extract credentials (Gmail - needs both)
age -d -i $AGE_KEY ~/.secrets/email/gmail-primary-tokens.json.age | \
jq -r '"Client ID: \(.client_id)\nClient Secret: \(.client_secret)"'
# Extract client ID only (Outlook - no secret needed)
age -d -i $AGE_KEY ~/.secrets/email/outlook-tokens.json.age | jq -r '.client_id'
# Test token refresh
~/atelier/_projects/personal/email-config/scripts/oauth2-token.sh outlook
~/atelier/_projects/personal/email-config/scripts/oauth2-token.sh gmail-primary
# Full re-auth
~/atelier/_projects/personal/email-config/scripts/setup-oauth2.sh outlook
~/atelier/_projects/personal/email-config/scripts/setup-oauth2.sh gmail-primary
# Restart aerc
pkill aerc && aerc
jq Patterns Used
| Pattern | Purpose |
|---|---|
|
Extract single field as raw string |
|
String interpolation with field value |
|
Convert Unix timestamp to ISO date |
|
Build new JSON object from fields |