<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: George Benjamin</title>
    <description>The latest articles on DEV Community by George Benjamin (@georgejnrs).</description>
    <link>https://dev.to/georgejnrs</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F945556%2F9199fd57-9a51-45bd-b5f7-2ac9903a73d1.jpeg</url>
      <title>DEV Community: George Benjamin</title>
      <link>https://dev.to/georgejnrs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/georgejnrs"/>
    <language>en</language>
    <item>
      <title>The Login Loop of Doom.</title>
      <dc:creator>George Benjamin</dc:creator>
      <pubDate>Wed, 19 Aug 2026 09:47:29 +0000</pubDate>
      <link>https://dev.to/georgejnrs/the-login-loop-of-doom-how-a-naive-datetime-held-our-users-hostage-4cmm</link>
      <guid>https://dev.to/georgejnrs/the-login-loop-of-doom-how-a-naive-datetime-held-our-users-hostage-4cmm</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;. Code snippets are recreated and anonymized for illustrative purposes.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Symptom: A Revolving Door Instead of a Login Page
&lt;/h2&gt;

&lt;p&gt;It started innocently enough: I was clicking through our app and hit "Log in." Auth0's Universal Login page appeared, I entered my credentials, got redirected back to the app... and landed on the Auth0 login page again. And again. And again.&lt;/p&gt;

&lt;p&gt;No error message. No failed login attempt. Auth0 was happily authenticating me every single time — and our app was just as happily bouncing me right back, like a bouncer who checks your ID, nods, and then immediately forgets he checked it.&lt;/p&gt;

&lt;p&gt;The login loop. Every developer's favorite horror movie, now starring me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Red Herring #1: "It's the Frontend's Fault"
&lt;/h2&gt;

&lt;p&gt;My first suspect was the obvious one: the frontend callback handler. A Node.js/Express app sits in front of our Django API, handling the Auth0 redirect dance. A login loop &lt;em&gt;screams&lt;/em&gt; "broken callback" or "state/nonce mismatch," so I spent a solid hour there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ State parameter matched&lt;/li&gt;
&lt;li&gt;✅ Nonce validated&lt;/li&gt;
&lt;li&gt;✅ Callback URL whitelisted in the Auth0 dashboard&lt;/li&gt;
&lt;li&gt;✅ ID token and access token both present in the response&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything the frontend touched was perfect. The tokens were real, signed by Auth0, freshly issued seconds ago. And yet the moment the frontend sent the access token to our Django API, the API answered with a flat &lt;code&gt;401 Unauthorized&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Fine. New suspect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Red Herring #2: "Auth0 Must Be Misconfigured"
&lt;/h2&gt;

&lt;p&gt;Next stop: the Auth0 dashboard. Maybe the token lifetime was set to something absurd, like 5 seconds? Maybe the audience claim was wrong?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Token lifetime: 3600 seconds. Normal.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aud&lt;/code&gt; claim: matched our API identifier exactly.&lt;/li&gt;
&lt;li&gt;Signature: verified against the JWKS. Valid.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So Auth0 was issuing perfectly good tokens, the frontend was delivering them intact, and Django was spitting them out. The bug had to be in the validation logic itself. Time to actually read the code we trusted blindly every day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Root Cause: Two Clocks, One Lie
&lt;/h2&gt;

&lt;p&gt;Buried in our custom JWT validation middleware, I found this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The offending code (recreated)
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# ...
&lt;/span&gt;    &lt;span class="n"&gt;exp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exp&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InvalidTokenError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Missing exp claim&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;TokenExpiredError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Token has expired&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks harmless, right? That's exactly why it survived code review.&lt;/p&gt;

&lt;p&gt;Here's the problem: &lt;strong&gt;JWT &lt;code&gt;exp&lt;/code&gt; is a Unix timestamp — seconds since epoch, UTC.&lt;/strong&gt; &lt;code&gt;datetime.now()&lt;/code&gt; &lt;strong&gt;returns &lt;em&gt;local server time&lt;/em&gt;.&lt;/strong&gt; On a machine configured for UTC, the comparison works by pure luck. But our server wasn't on UTC. It was several hours ahead.&lt;/p&gt;

&lt;p&gt;So the moment Auth0 issued a token, our server looked at its own clock — hours in the future — and declared the token already expired. Every token. Instantly. Forever.&lt;/p&gt;

&lt;p&gt;The sequence of doom:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User logs in via Auth0 → valid token issued with &lt;code&gt;exp = now_utc + 3600&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Frontend calls Django API with the token&lt;/li&gt;
&lt;li&gt;Django compares &lt;code&gt;exp&lt;/code&gt; against &lt;strong&gt;local time&lt;/strong&gt;, sees it as "expired" → &lt;code&gt;401&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Frontend's interceptor sees &lt;code&gt;401&lt;/code&gt; → "session must be dead" → redirects to Auth0&lt;/li&gt;
&lt;li&gt;Auth0 session cookie is still valid → silently issues a &lt;strong&gt;brand new token&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Go to step 2. Repeat until the user gives up.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A timezone bug, wearing an authentication bug's clothes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: One Line and a Lesson
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The fix (recreated)
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;exp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exp&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;InvalidTokenError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Missing exp claim&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;TokenExpiredError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Token has expired&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Actually, the honest fix was two lines, because we also hardened the failure mode on the frontend: the interceptor now distinguishes "token rejected" from "session expired" instead of blindly redirecting to login on every &lt;code&gt;401&lt;/code&gt;. A 401 from token validation should never automatically mean "start the whole login dance over" — that assumption is what turned a quiet bug into a user-facing infinite loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Verification: Become Your Own Attackers
&lt;/h2&gt;

&lt;p&gt;Debugging this took a few hours; verifying the fix took a different mindset. I didn't just log in as myself and call it a day — I ran the flow with &lt;strong&gt;multiple different accounts through different login paths&lt;/strong&gt;: social login vs. email/password, fresh accounts vs. long-lived ones, incognito sessions vs. sessions with existing Auth0 cookies. Every combination, every timezone simulation (I also changed the server TZ deliberately to break it again on purpose — highly recommended; a fix you can't re-break is a fix you don't understand).&lt;/p&gt;

&lt;p&gt;All green. The revolving door became a door again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Took Away
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;datetime.now()&lt;/code&gt; &lt;strong&gt;without a timezone is a loaded gun.&lt;/strong&gt; In Python, always use &lt;code&gt;datetime.now(timezone.utc)&lt;/code&gt; for anything that touches timestamps, tokens, or comparisons across systems. Better yet, lint for it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The scariest bugs live at system boundaries.&lt;/strong&gt; Auth0 was fine. The frontend was fine. Django was "fine." The bug existed only in the &lt;em&gt;assumption&lt;/em&gt; that two machines share a clock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A 401 is not a synonym for "please log in again."&lt;/strong&gt; Distinguish your failure modes, or your error handling will amplify small bugs into infinite loops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test like an attacker, not like yourself.&lt;/strong&gt; Different accounts, different login methods, different session states. The bug hid from the happy path and lived everywhere else.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The best part? This class of bug is everywhere. If your stack validates JWTs anywhere by hand, go grep for naive &lt;code&gt;now()&lt;/code&gt; calls right now. I'll wait. 🐛🔨&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
    </item>
    <item>
      <title>Grandma Rose's Recipe 🍲</title>
      <dc:creator>George Benjamin</dc:creator>
      <pubDate>Mon, 10 Aug 2026 08:30:41 +0000</pubDate>
      <link>https://dev.to/georgejnrs/grandma-roses-recipe-1eeb</link>
      <guid>https://dev.to/georgejnrs/grandma-roses-recipe-1eeb</guid>
      <description>&lt;p&gt;Comfort food isn't just about the food — it's about the memories, the people, and the feeling that comes with it.&lt;/p&gt;

&lt;p&gt;For this challenge, I built Grandma Rose's Recipe, a warm and welcoming comfort-food landing page inspired by the feeling of discovering a recipe that has been passed down through generations.&lt;/p&gt;

&lt;p&gt;💡 The idea&lt;/p&gt;

&lt;p&gt;I wanted the website to feel less like a typical restaurant page and more like opening an old family recipe book — something familiar, personal, and comforting.&lt;/p&gt;

&lt;p&gt;🛠️ Built with&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTML&lt;/li&gt;
&lt;li&gt;CSS&lt;/li&gt;
&lt;li&gt;JavaScript&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✨ What I focused on&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A warm, food-focused visual design&lt;/li&gt;
&lt;li&gt;Responsive layout for different screen sizes&lt;/li&gt;
&lt;li&gt;Interactive elements&lt;/li&gt;
&lt;li&gt;Clear and simple navigation&lt;/li&gt;
&lt;li&gt;Creating an experience that feels nostalgic while still being modern&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Demo&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/editor/Juniors001/embed/019fea7f-13d1-78d8-81bb-a8a8189c7637?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;I'd love to hear your thoughts and feedback!&lt;/p&gt;

&lt;h1&gt;
  
  
  frontendchallenge #webdev #frontend #html #css #javascript
&lt;/h1&gt;

</description>
      <category>devchallenge</category>
      <category>frontendchallenge</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
