<?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: ahmed oubadi</title>
    <description>The latest articles on DEV Community by ahmed oubadi (@ahmedoubadi).</description>
    <link>https://dev.to/ahmedoubadi</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%2F1069023%2F8a3c061c-3100-4aa7-a9b6-e95965349e36.jpeg</url>
      <title>DEV Community: ahmed oubadi</title>
      <link>https://dev.to/ahmedoubadi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ahmedoubadi"/>
    <language>en</language>
    <item>
      <title>10 Authentication Module Decisions That Took This Codebase From "It Compiles" to Production-Ready</title>
      <dc:creator>ahmed oubadi</dc:creator>
      <pubDate>Sun, 20 Sep 2026 13:38:17 +0000</pubDate>
      <link>https://dev.to/ahmedoubadi/10-authentication-module-decisions-that-took-this-codebase-from-it-compiles-to-production-ready-5a25</link>
      <guid>https://dev.to/ahmedoubadi/10-authentication-module-decisions-that-took-this-codebase-from-it-compiles-to-production-ready-5a25</guid>
      <description>&lt;p&gt;📢 If you're building your own auth instead of bolting on a third-party provider but&lt;br&gt;
↳ Every tutorial stops at "hash the password and you're done"&lt;br&gt;
↳ You're not sure which shortcuts will actually get you breached&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj0xt3fkkw1zbdjcu3zke.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj0xt3fkkw1zbdjcu3zke.jpeg" alt=" " width="800" height="322"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then here are the 10 decisions baked into a real Express + TypeScript auth module; register, login, Google sign-in, email verification, password reset, that separate "it works on my machine" from something you'd trust with real accounts.&lt;/p&gt;

&lt;p&gt;Most password reuse and account-takeover incidents don't come from exotic exploits. They come from small, boring omissions: an error message that confirms an email exists, a session token stored in plaintext, a reset link that never expires. Auth isn't hard because the crypto is hard. It's hard because it's the one module where every shortcut becomes someone else's incident report.&lt;/p&gt;

&lt;p&gt;Here are the 10 decisions I'd copy 👇&lt;/p&gt;
&lt;h2&gt;
  
  
  1️⃣ Hash passwords with Argon2id, not bcrypt defaults
&lt;/h2&gt;

&lt;p&gt;Everyone knows "don't store plaintext passwords." Fewer people tune the hashing cost for their actual hardware.&lt;/p&gt;

&lt;p&gt;↳ Use &lt;code&gt;argon2.argon2id&lt;/code&gt; specifically, it resists both GPU cracking and side-channel attacks, which plain Argon2i or Argon2d don't do alone.&lt;br&gt;
↳ Set &lt;code&gt;memoryCost&lt;/code&gt;, &lt;code&gt;timeCost&lt;/code&gt;, and &lt;code&gt;parallelism&lt;/code&gt; explicitly instead of trusting defaults.&lt;br&gt;
↳ Wrap verification in a try/catch that fails closed, a malformed hash should mean "reject," never "throw and crash the request."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;argon2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;argon2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argon2id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;memoryCost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timeCost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;parallelism&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The memory cost is the part people skip. It's what makes cracking expensive on custom hardware, not just slow on a CPU.&lt;/p&gt;

&lt;h2&gt;
  
  
  2️⃣ Give login and register the exact same failure shape
&lt;/h2&gt;

&lt;p&gt;This is the auth equivalent of tactic #2 in most SEO threads: don't tell the attacker anything they didn't already know.&lt;/p&gt;

&lt;p&gt;↳ Registering with an email that already exists? Same generic error as any other validation failure.&lt;br&gt;
↳ Logging in with a wrong email? Same error as a wrong password.&lt;br&gt;
↳ Logging in with a right email but wrong password? Same error again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AppError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid email or password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// wrong password later in the function throws the identical message&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three different failure states, one message. If your error responses let someone enumerate which emails have accounts, you've built a mailing list for credential-stuffing attempts.&lt;/p&gt;

&lt;h2&gt;
  
  
  3️⃣ Never store the session token you hand out
&lt;/h2&gt;

&lt;p&gt;The cookie value and the database value should never be the same string.&lt;/p&gt;

&lt;p&gt;↳ Generate a random 64-byte token with &lt;code&gt;crypto.randomBytes&lt;/code&gt;.&lt;br&gt;
↳ Hash it with SHA-256 before it touches the database.&lt;br&gt;
↳ Store the hash, send the raw token to the browser.&lt;br&gt;
↳ On every request, hash the incoming cookie and compare hashes.&lt;/p&gt;

&lt;p&gt;If your session table gets dumped, an attacker holding hashes can't reconstruct working cookies from them. This is the same logic as never storing a password, a session token is just a very short-lived password.&lt;/p&gt;
&lt;h2&gt;
  
  
  4️⃣ Lock the cookie down on every axis, not just &lt;code&gt;httpOnly&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;httpOnly&lt;/code&gt; alone stops casual XSS token theft. It doesn't stop CSRF, and it doesn't stop the cookie leaking over plain HTTP.&lt;/p&gt;

&lt;p&gt;↳ &lt;code&gt;httpOnly: true&lt;/code&gt;, JavaScript on the page can't read it.&lt;br&gt;
↳ &lt;code&gt;secure&lt;/code&gt; tied to environment, true in production, so it never rides over HTTP.&lt;br&gt;
↳ &lt;code&gt;sameSite: 'lax'&lt;/code&gt;, blocks the cookie from being sent on cross-site POSTs.&lt;br&gt;
↳ An explicit &lt;code&gt;expires&lt;/code&gt; matching your actual session lifetime, not "whenever the browser feels like clearing it."&lt;/p&gt;

&lt;p&gt;Each flag closes a different door. Ship all four or you've only closed one.&lt;/p&gt;
&lt;h2&gt;
  
  
  5️⃣ Verify the Google ID token server-side, never trust the decoded JWT
&lt;/h2&gt;

&lt;p&gt;This is the mistake that looks fine in a demo and falls apart the moment someone forges a payload.&lt;/p&gt;

&lt;p&gt;↳ Send the token to Google's &lt;code&gt;tokeninfo&lt;/code&gt; endpoint instead of just base64-decoding it.&lt;br&gt;
↳ Check &lt;code&gt;email_verified&lt;/code&gt;, an unverified Google email is not proof of ownership.&lt;br&gt;
↳ Check &lt;code&gt;aud&lt;/code&gt; against your own client ID, or anyone's token from any Google app authenticates into yours.&lt;br&gt;
↳ Check &lt;code&gt;iss&lt;/code&gt; matches &lt;code&gt;accounts.google.com&lt;/code&gt;.&lt;br&gt;
↳ Check &lt;code&gt;exp&lt;/code&gt; hasn't already passed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expectedClientId&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aud&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;expectedClientId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AppError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid Google token audience&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decoding a JWT tells you what it &lt;em&gt;claims&lt;/em&gt;. Verifying it tells you what's &lt;em&gt;true&lt;/em&gt;. Those are not the same operation, and treating them as interchangeable is how you end up letting anyone log in as anyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  6️⃣ Don't silently merge accounts across providers
&lt;/h2&gt;

&lt;p&gt;Someone signs up with a password using &lt;code&gt;jane@company.com&lt;/code&gt;. Later, someone else authenticates with Google using the same email address. Are they the same person?&lt;/p&gt;

&lt;p&gt;You don't know. Don't guess.&lt;/p&gt;

&lt;p&gt;↳ If a Google email matches an existing password account, reject the sign-in.&lt;br&gt;
↳ Return a clear message pointing to an explicit linking flow instead.&lt;br&gt;
↳ Only ever create the link when the &lt;em&gt;already-authenticated&lt;/em&gt; user requests it from their own settings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existingUser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AppError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;An account with this email already exists. Sign in with your password and link Google from your account settings.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="mi"&gt;409&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Auto-linking on email match is exactly the kind of implicit trust that turns "forgot my Google password" into "someone else now owns my account."&lt;/p&gt;

&lt;h2&gt;
  
  
  7️⃣ Make verification and reset tokens single-use and short-lived
&lt;/h2&gt;

&lt;p&gt;A link that works forever isn't a link, it's a standing vulnerability with a nice subject line.&lt;/p&gt;

&lt;p&gt;↳ Generate a random token, hash it the same way session tokens are hashed.&lt;br&gt;
↳ Store it with a 30-minute expiry.&lt;br&gt;
↳ Delete any previous unused tokens for that user before creating a new one, so old links stop working the moment a new one is requested.&lt;br&gt;
↳ Delete the token itself the instant it's redeemed, a reset link should not be replayable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleteUserVerificationTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`email:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createVerification&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="p"&gt;...,&lt;/span&gt;
    &lt;span class="na"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&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="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;VERIFICATION_MINUTES&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The identifier prefix (&lt;code&gt;email:&lt;/code&gt; vs &lt;code&gt;reset:&lt;/code&gt;) matters too, it keeps a leaked verification token from being usable to reset a password, and vice versa.&lt;/p&gt;

&lt;h2&gt;
  
  
  8️⃣ Revoke every session after a password reset or change
&lt;/h2&gt;

&lt;p&gt;If a password reset means an account was compromised, an attacker's still-active session shouldn't survive the fix.&lt;/p&gt;

&lt;p&gt;↳ On successful password reset: delete every session for that user, clear the cookie.&lt;br&gt;
↳ On successful password change: same thing, then immediately issue one fresh session for the user who just proved they know both passwords.&lt;br&gt;
↳ Treat "user changed their credentials" as a full trust reset, not an incremental update.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleteAllUserSessions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;clearSessionCookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the step that's easiest to skip because nothing breaks in testing if you forget it. It only matters on the one day someone's account was actually taken over.&lt;/p&gt;

&lt;h2&gt;
  
  
  9️⃣ Validate at the edge, before any handler logic runs
&lt;/h2&gt;

&lt;p&gt;Every DTO in this module has a matching schema, and the schema does more than check "is this a string."&lt;/p&gt;

&lt;p&gt;↳ Email: normalized to lowercase, capped at 255 characters, format-checked without requiring a TLD allowlist that goes stale.&lt;br&gt;
↳ Password: minimum 12 characters, long enough to push people toward passphrases instead of &lt;code&gt;Password1!&lt;/code&gt;.&lt;br&gt;
↳ Tokens: fixed hex length matching the exact hash output, so a malformed token 400s before it ever reaches a database query.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Joi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Validation isn't just UX. A schema that rejects garbage before your handler runs is one less code path an attacker can use to find an edge case.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔟 Rate-limit by intent, not by one global limiter
&lt;/h2&gt;

&lt;p&gt;Login attempts, password resets, and email verifications get abused in different ways and need different ceilings.&lt;/p&gt;

&lt;p&gt;↳ &lt;code&gt;authLimiter&lt;/code&gt; on register/login/Google, throttles brute-force credential guessing.&lt;br&gt;
↳ &lt;code&gt;passwordResetLimiter&lt;/code&gt;, throttles someone hammering forgot-password to spam a target's inbox.&lt;br&gt;
↳ &lt;code&gt;emailVerificationLimiter&lt;/code&gt;, throttles resend abuse separately from reset abuse.&lt;/p&gt;

&lt;p&gt;One shared limiter either lets credential stuffing through or blocks legitimate users doing something unrelated. Splitting them means each limit can actually be tuned to the attack it's stopping.&lt;/p&gt;




&lt;p&gt;None of these ten decisions are visible in a demo. They only show up the day someone tries to abuse the system, and by then it's too late to add them. Build them in from the first commit, the module doesn't get simpler later, only busier.&lt;/p&gt;

</description>
      <category>auth0challenge</category>
      <category>programming</category>
      <category>softwareengineering</category>
      <category>systemdesign</category>
    </item>
  </channel>
</rss>
