<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[ReAuthWatch]]></title><description><![CDATA[ReAuthWatch]]></description><link>https://reauthwatch.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>ReAuthWatch</title><link>https://reauthwatch.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sun, 20 Sep 2026 19:15:23 GMT</lastBuildDate><atom:link href="https://reauthwatch.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[When OAuth refresh dies with invalid_grant — and you only hear from support]]></title><description><![CDATA[If you ship customer OAuth (Google, Slack, Microsoft, HubSpot, Salesforce Connected Apps…), you already know the failure mode:

Refresh token gets revoked, rotated, or hits idle TTL.
Your background s]]></description><link>https://reauthwatch.hashnode.dev/when-oauth-refresh-dies-with-invalid-grant-and-you-only-hear-from-support</link><guid isPermaLink="true">https://reauthwatch.hashnode.dev/when-oauth-refresh-dies-with-invalid-grant-and-you-only-hear-from-support</guid><category><![CDATA[oauth]]></category><category><![CDATA[Security]]></category><category><![CDATA[SaaS]]></category><category><![CDATA[Devops]]></category><dc:creator><![CDATA[ReAuthWatch]]></dc:creator><pubDate>Thu, 17 Sep 2026 16:32:26 GMT</pubDate><content:encoded><![CDATA[<p>If you ship customer OAuth (Google, Slack, Microsoft, HubSpot, Salesforce Connected Apps…), you already know the failure mode:</p>
<ol>
<li>Refresh token gets revoked, rotated, or hits idle TTL.</li>
<li>Your background sync keeps retrying.</li>
<li>The first <em>real</em> signal is a customer saying “sync stopped working.”</li>
</ol>
<p><code>invalid_grant</code> is usually that moment — and most product metrics never see it.
2. Never hold the refresh token in the canary if your product already owns OAuth
3. Alert on:</p>
<ul>
<li><strong>7 / 3 / 1 days</strong> before known expiry</li>
<li><strong>Spikes</strong> of <code>invalid_grant</code> (or equivalent) by provider</li>
</ul>
<ol>
<li>Attach a <strong>drafted re-auth email</strong> so CS/ops isn’t inventing copy at 5pm Friday</li>
</ol>
<p>That’s enough to turn “support discovered it” into “we pinged Slack yesterday.”</p>
<h2>DIY vs a tiny tool</h2>
<p>A 20-line cron over your connections table is totally fine. The canary only earns its keep if:</p>
<ul>
<li>you have many customer connections</li>
<li>expiry + error classes are scattered across services</li>
<li>you want the drafted re-auth loop, not just a Graphite spike</li>
</ul>
<p>I’m testing that thin loop as <strong>ReAuthWatch</strong> (free concierge pilot while validating): <a href="https://reauthwatch-canary.netlify.app?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=invalid_grant">https://reauthwatch-canary.netlify.app</a></p>
<p>Curious how others catch this today — <code>invalid_grant</code> volume alerts, Connected App usage dashboards, or still CS tickets?</p>
<h2>What actually breaks</h2>
<p>Expiry dates alone are not enough:</p>
<ul>
<li><strong>Revocation / rotation</strong> can kill a refresh token before <code>expires_at</code></li>
<li><strong>Idle TTL / policy changes</strong> (Connected App, workspace admin flips) don’t always show up in your app logs as “auth”</li>
<li><strong>Per-tenant</strong> connections fail independently — one customer can be dark while your global “OAuth health” looks green</li>
</ul>
<p>If you only monitor HTTP 5xx on your API, you will miss this.</p>
<h2>A thin canary pattern (no token custody)</h2>
<p>You don’t need a full unified-API platform. A useful v1 is boring:</p>
<ol>
<li>Store <strong>connection metadata only</strong> (provider, tenant id, <code>refresh_token_expires_at</code>, last successful refresh timestamp, last error class)</li>
</ol>
]]></content:encoded></item></channel></rss>