<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged authentication</title>
<link>https://coderlegion.com/tag/authentication</link>
<description>Powered by Question2Answer</description>
<item>
<title>Lock Your Inbox: Email 2FA, Passkeys and App Passwords</title>
<link>https://coderlegion.com/32844/lock-your-inbox-email-2fa-passkeys-and-app-passwords</link>
<description>&lt;p&gt;&lt;em&gt;This article was originally published on the &lt;a rel=&quot;nofollow&quot; href=&quot;https://fuck.it/blog/email-2fa-passkeys-app-passwords&quot;&gt;[@fuck.it] blog&lt;/a&gt; and is republished here by the author.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;There is a quiet assumption sitting behind almost every account you own: that if you ever lose your way in, your email will let you back. Click &quot;forgot password,&quot; wait a moment, and something arrives in your inbox that puts you back in charge.&lt;/p&gt;
&lt;p&gt;It is a reasonable way to build things. It also means your mailbox is not really one account among many. It is the drawer where the spare keys to the others are kept, which makes it the one account worth being fussy about.&lt;/p&gt;
&lt;p&gt;So here is the fussy version. What the options actually do, which of them hold up against a convincing fake login page, the one feature that quietly undoes the rest and how to switch all of it on.&lt;/p&gt;
&lt;h3 id=&quot;tl-dr&quot;&gt;TL;DR&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Use a passkey or a hardware security key if your provider offers one.&lt;/strong&gt; They are phishing-resistant in a way that SMS codes and authenticator codes are not.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use an authenticator app if it doesn't.&lt;/strong&gt; Enormous improvement over nothing. Still phishable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SMS codes are the weakest real option.&lt;/strong&gt; NIST's current guidelines classify exactly one authenticator as restricted, and this is it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Save your recovery codes somewhere that is not the inbox they unlock.&lt;/strong&gt; Locking yourself out is a very real way to lose an email account.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;App passwords bypass your second factor completely.&lt;/strong&gt; That is their entire job. Create one only when a client demands it, keep it to one per device and revoke it when you are done.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your recovery address and recovery phone are keys too.&lt;/strong&gt; A locked front door and an open window is not security.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;why-the-inbox-is-the-wrong-thing-to-secure-casually&quot;&gt;Why the Inbox Is the Wrong Thing to Secure Casually&lt;/h3&gt;
&lt;p&gt;Almost every account you own has a button that says it will email you a way back in. That is sensible design. It also means that for a great many of your accounts, the real lock on the door is your mailbox, however strong the password on each individual one happens to be.&lt;/p&gt;
&lt;p&gt;Attackers worked this out long before users did. Taking over an inbox is not usually the goal. It is the tool. Once somebody is reading your mail they can trigger resets, collect the codes, approve the changes and delete the warning messages on the way out, without ever having to attack the accounts they actually want.&lt;/p&gt;
&lt;p&gt;So if you are going to be fussy about exactly one account, make it this one. Everything in this article is worth doing on your email before you do it anywhere else.&lt;/p&gt;
&lt;h3 id=&quot;what-people-mean-when-they-talk-about-2fa&quot;&gt;What People Mean When They Talk About 2FA&lt;/h3&gt;
&lt;p&gt;There are four of them in common use, and they are not equivalent. The gap between the best and the worst is wider than most people assume.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A code by SMS.&lt;/strong&gt; The provider texts you six digits. Better than nothing, and worse than every other option here. The weakness is not the code, it is the phone number. Numbers get moved to a new SIM by somebody who has talked a carrier employee into it, and your codes move with them. The problem is real enough that the FCC adopted rules in November 2023 requiring carriers to authenticate customers properly before a SIM change or a port-out, and to offer you a free account lock against both. NIST's 2025 authentication guidelines go further: use of the phone network for out-of-band verification is classified as &lt;em&gt;restricted&lt;/em&gt;, and at publication it was the only restricted authenticator in the document. Services may still offer it, but they have to provide something better alongside it and tell you about the risk.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A code from an authenticator app.&lt;/strong&gt; A six-digit number that changes every thirty seconds, generated on your device from a shared secret. No phone number, no carrier, no text message to intercept. This is a very large upgrade, and for most people it is the realistic default.&lt;/p&gt;
&lt;p&gt;It has one specific weakness worth understanding, because it is the weakness that catches people who thought they were safe. Typing a code into a web page does not tie that code to the real site. NIST puts it plainly: manual entry does not bind the output to the session, so an impostor verifier could relay it. In practice, a fake login page that looks right can collect your password and your code, pass both to the real provider inside the thirty seconds they are valid, and let itself in. Ready-made tooling for exactly this has existed for years. An authenticator app defeats password reuse, credential stuffing and opportunistic attacks. It does not defeat somebody who has got you onto a convincing fake page.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A push notification you approve.&lt;/strong&gt; Your phone asks &quot;was this you?&quot; and you tap yes. Convenient, and vulnerable to the oldest trick in the catalog: send the prompt over and over at eleven at night until the human taps approve to make it stop. The better implementations use &lt;em&gt;number matching&lt;/em&gt;, where the login screen shows you two digits that you have to type into the app. That kills the half-asleep approval, and it is worth turning on wherever you are offered it. Against a live fake-page attack it is in the same boat as an authenticator app.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A passkey or a hardware security key.&lt;/strong&gt; This one differs in kind rather than in degree, so it gets its own section.&lt;/p&gt;
&lt;h3 id=&quot;what-a-passkey-actually-is&quot;&gt;What a Passkey Actually Is&lt;/h3&gt;
&lt;p&gt;A passkey is a pair of cryptographic keys. The private half stays on your device or in an encrypted credential manager and never travels to the site you are signing into. The site keeps only the public half, which is useless to a thief. When you sign in, your device proves it holds the private key without ever revealing it.&lt;/p&gt;
&lt;p&gt;The part that matters for phishing is less obvious and more important. When a passkey is created, it is bound to the exact domain that created it. A passkey you made for your bank will not answer a request from your-bank-secure-login.example, no matter how perfect the copy of the page is, because your browser will not even offer it. NIST calls this verifier name binding, and names WebAuthn, the standard passkeys are built on, as the example of how to do it.&lt;/p&gt;
&lt;p&gt;That is the whole argument, so it is worth reading twice. With an authenticator app, your safety depends on you noticing the fake site. With a passkey, the fake site gets nothing even when you do not notice. The decision has moved from your judgment at eight in the morning to a check your browser performs every single time without ever getting tired or distracted.&lt;/p&gt;
&lt;p&gt;Two honest drawbacks, both smaller than they were a year ago and neither of them gone.&lt;/p&gt;
&lt;p&gt;First, a synced passkey puts part of your security in the hands of a credential manager and its account recovery. Moving them used to be close to impossible. That has improved: Apple's iOS 26, iPadOS 26 and macOS 26 releases and recent Android versions support transferring credentials directly between credential manager apps on the same device, and Apple Passwords, 1Password, Bitwarden, Dashlane and others have adopted it. The underlying work is only half finished, though. The exchange &lt;em&gt;format&lt;/em&gt; reached proposed-standard status in 2025, while the &lt;em&gt;protocol&lt;/em&gt; for actually moving credentials between providers is still a working draft that explicitly says it is not a basis for implementations. Same-device switching between participating credential managers works today on both Apple and Android. What does not exist yet is a finished universal protocol for moving credentials freely between providers across different devices and platforms.&lt;/p&gt;
&lt;p&gt;Second, support is uneven. Microsoft began making new consumer accounts passwordless by default in 2025 and most large providers accept passkeys somewhere, but plenty of smaller services still do not offer them at all. A physical security key sidesteps the ecosystem question completely, at the cost of being an object you can lose. Credentials cannot be exported from a certified hardware key by design, so there is no migration path and the answer is to buy two and register both.&lt;/p&gt;
&lt;p&gt;Where a passkey is on offer for your email, take it.&lt;/p&gt;
&lt;h3 id=&quot;app-passwords-the-back-door-you-opened-yourself&quot;&gt;App Passwords: The Back Door You Opened Yourself&lt;/h3&gt;
&lt;p&gt;Now the part that is mostly missing from articles about securing email, and that can undo everything above.&lt;/p&gt;
&lt;p&gt;Turn on two-factor authentication, then try to set up your mailbox in an older desktop mail client or on an office printer. It fails, and the reason is structural. When an older mail client connects over IMAP or SMTP using traditional password authentication, it hands over a username and a password, and there is no interactive moment in that exchange where the server can stop and ask you for a code from your phone.&lt;/p&gt;
&lt;p&gt;The modern fix is to stop using a password at all. RFC 7628 defines OAuth-based authentication over these protocols for exactly this situation: the client sends you through a browser login where two-factor happens normally, then receives a token it uses from then on. This is what &quot;modern authentication&quot; means in Gmail and Outlook, and it is why those accounts work properly in current mail apps.&lt;/p&gt;
&lt;p&gt;The older fix, still available at many providers, is an app password. Your provider generates a long random string that works in place of your real password and &lt;strong&gt;skips your second factor entirely.&lt;/strong&gt; Google's is sixteen characters, shown to you exactly once. You can only create one if two-step verification is already switched on, which is the quiet irony of the feature: it exists only for accounts that took security seriously, and what it produces is a credential that ignores the protection they switched on.&lt;/p&gt;
&lt;p&gt;Google itself says app passwords are not recommended and are unnecessary in most cases. That has not stopped them being widely used, and it has not stopped them being targeted.&lt;/p&gt;
&lt;p&gt;In June 2025 the Citizen Lab published a report on a campaign that did precisely that. Over at least ten email exchanges, an attacker posing as a US State Department official cultivated a relationship with Keir Giles, a Chatham House specialist on Russian information operations. Four apparently fabricated state.gov addresses were copied into the thread for credibility, which worked partly because the real State Department mail server appears to accept messages to addresses that do not exist without bouncing them. Eventually a professionally formatted PDF arrived with instructions for joining a government platform. The instructions walked him through creating an app-specific password on his Google account, labeling it with a meaningless string and sending it back.&lt;/p&gt;
&lt;p&gt;The PDF contained no malware. There was no exploit anywhere in the operation. The entire attack was a set of polite instructions that persuaded a careful, security-aware target to generate a credential that bypassed his own two-factor authentication, and then to hand it over. Google's threat intelligence group attributed the operator to UNC6293, linked with low confidence to APT29.&lt;/p&gt;
&lt;p&gt;The rules that follow are short:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create an app password only when a device truly cannot do anything else, and look for an OAuth option first.&lt;/li&gt;
&lt;li&gt;One per device, named for that device, so you can revoke precisely.&lt;/li&gt;
&lt;li&gt;Revoke it the moment you stop using that client, sell the phone or replace the printer.&lt;/li&gt;
&lt;li&gt;Never send one to anybody. There is no legitimate situation in which a person, a company or a support agent needs your app password. None.&lt;/li&gt;
&lt;li&gt;Google revokes its app passwords when you change your account password. Do not assume every provider does. After any scare, change the password and then go and audit the app password list separately.&lt;/li&gt;
&lt;li&gt;Audit that list occasionally anyway. Old app passwords have a habit of surviving long after the device they were created for is gone.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;recovery-codes-are-the-part-everyone-skips&quot;&gt;Recovery Codes Are the Part Everyone Skips&lt;/h3&gt;
&lt;p&gt;When you switch on two-factor authentication you are handed a list of one-time codes and a button to download them. Do it. NIST calls these look-up secrets and treats them as a legitimate authenticator in their own right, with the caveat that they are not phishing-resistant either.&lt;/p&gt;
&lt;p&gt;The reason to care is not heroic. A very ordinary way to lose access to an email account is a phone that went into a canal carrying the only copy of the authenticator secret. Recovery codes are the answer to that, and they are the step people reliably skip, because the account works perfectly well without them right up until it does not.&lt;/p&gt;
&lt;p&gt;Where to put them: printed on paper somewhere a burglar would not think to look is perfectly reasonable. A password manager is good too, with one trap worth thinking through. If your password manager's own account recovery runs through the email address these codes protect, and the codes exist only inside that password manager, you have built a circle with no way in. Keep at least one copy outside the loop.&lt;/p&gt;
&lt;p&gt;And do not store them as a draft in the inbox they unlock, which is a thing people do.&lt;/p&gt;
&lt;h3 id=&quot;the-windows-you-left-open&quot;&gt;The Windows You Left Open&lt;/h3&gt;
&lt;p&gt;Two more ways into your mailbox that are not the front door.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Your recovery address and recovery phone.&lt;/strong&gt; These exist to let you back in, which means they can let somebody else in. An abandoned address at a provider you no longer check, or worse, one that has lapsed and could be registered by anybody who notices, is a live key to your current mailbox. Go and look at what is actually listed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Forwarding rules and filters.&lt;/strong&gt; This is a standard move after a mailbox compromise and one that is easy to overlook. A rule that quietly copies everything to an outside address, or files all mail from your bank into a folder you never open, survives the password change, the new second factor and the warm feeling of having dealt with it. If you ever suspect something happened, read your rules before you relax.&lt;/p&gt;
&lt;h3 id=&quot;turning-it-on&quot;&gt;Turning It On&lt;/h3&gt;
&lt;p&gt;The wording differs between providers, but the shape of it almost never does.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Sign in and open your account settings, then look for a section called Security, Sign-in or Password.&lt;/li&gt;
&lt;li&gt;Switch on two-factor authentication, two-step verification or multi-factor authentication. Same thing, three names.&lt;/li&gt;
&lt;li&gt;Scan the QR code with any authenticator app. Anything that does standard TOTP works, so you are not tied to a particular vendor.&lt;/li&gt;
&lt;li&gt;Type the six-digit code once to confirm. If you skip this step, it is not actually on.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Save the recovery codes.&lt;/strong&gt; This is the step that matters most in a year's time.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;One thing worth understanding while you are in there. A mail client using traditional password authentication over IMAP, POP3 or SMTP has no interactive moment where the server can stop and ask you for a code. Providers deal with that in different ways: some disable password authentication once a second factor is enabled, some issue separate application passwords and modern services increasingly use OAuth so that the second factor can happen during a browser login instead.&lt;/p&gt;
&lt;p&gt;The practical conclusion matters more than the mechanism, so find out what credential your mail apps are actually using. If your normal mailbox password still works over IMAP, POP3 or SMTP after you switch on two-factor authentication, then that password is still a way into the mailbox with no second factor standing in front of it. Make it long, make it unique to this account and never reuse it anywhere else.&lt;/p&gt;
&lt;p&gt;Worth checking separately: a few services keep more than one login. The account you use for billing and the mailbox itself can be two different doors with two different settings, and switching one on does not switch on the other.&lt;/p&gt;
&lt;h3 id=&quot;the-twenty-minute-version&quot;&gt;The Twenty-Minute Version&lt;/h3&gt;
&lt;p&gt;In order, because the order matters:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Email first.&lt;/strong&gt; It is the recovery channel for a disturbing number of your other accounts, so securing them while the mailbox is open is largely decorative.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Turn on the strongest second factor on offer.&lt;/strong&gt; Passkey or security key, then authenticator app, then SMS. If SMS is the only option, take it and ask your provider why.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Save the recovery codes.&lt;/strong&gt; Somewhere physical, outside the loop described above.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Review your app passwords and connected apps.&lt;/strong&gt; Revoke anything you do not recognize or no longer use.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check your forwarding rules and filters.&lt;/strong&gt; Takes a minute, almost never done.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fix your recovery address and phone.&lt;/strong&gt; Make sure both are things you still control.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Then do steps two and three for your password manager and your phone's cloud account.&lt;/strong&gt; Those are the other two sets of spare keys.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That is the lot. One short session on the sofa, once, for the account that holds the spare keys to so many of the others.&lt;/p&gt;
&lt;h3 id=&quot;one-number-before-you-go&quot;&gt;One Number Before You Go&lt;/h3&gt;
&lt;p&gt;We run a small paid email service, which puts us in a position to look something up that most people writing about this subject cannot: how many of our own customers actually switch this on.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fewer than one in five.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Sit with that for a second. Our customers are, by definition, people who went looking for a private email service, found an invite-only one and paid money for it. If adoption is that low among people who are actively paying for privacy, it says something uncomfortable about how badly an optional security feature loses to inertia, no matter how motivated the person is.&lt;/p&gt;
&lt;p&gt;Which is the honest note to end on. An optional second factor cannot protect you if you never switch it on.&lt;/p&gt;
&lt;p&gt;It takes about four minutes. Go on.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Sources &amp;amp; further reading:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://csrc.nist.gov/pubs/sp/800/63/b/4/final&quot;&gt;NIST SP 800-63B-4: Authentication and Authenticator Management&lt;/a&gt; · &lt;a rel=&quot;nofollow&quot; href=&quot;https://pages.nist.gov/800-63-4/sp800-63b/authenticators/&quot;&gt;NIST: Authenticators and phishing resistance&lt;/a&gt; · &lt;a rel=&quot;nofollow&quot; href=&quot;https://pages.nist.gov/800-63-4/sp800-63b/syncable&quot;&gt;NIST: Syncable authenticators&lt;/a&gt; · &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.w3.org/TR/webauthn/&quot;&gt;W3C Web Authentication (WebAuthn)&lt;/a&gt; · &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc7628&quot;&gt;RFC 7628: SASL Mechanisms for OAuth&lt;/a&gt; · &lt;a rel=&quot;nofollow&quot; href=&quot;https://citizenlab.ca/research/russian-government-linked-social-engineering-targets-app-specific-passwords/&quot;&gt;Citizen Lab: Same Sea, New Phish&lt;/a&gt; · &lt;a rel=&quot;nofollow&quot; href=&quot;https://support.google.com/accounts/answer/185833&quot;&gt;Google: Sign in with app passwords&lt;/a&gt; · &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.govinfo.gov/content/pkg/FR-2023-12-08/html/2023-26338.htm&quot;&gt;FCC: SIM swap and port-out fraud rules&lt;/a&gt; · &lt;a rel=&quot;nofollow&quot; href=&quot;https://documentation.open-xchange.com/appsuite/security/application_passwords.html&quot;&gt;Open-Xchange: Application passwords&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Disclosure: We run &lt;a rel=&quot;nofollow&quot; href=&quot;https://fuck.it&quot;&gt;[@fuck.it]&lt;/a&gt;, a privacy-focused, subscription-funded email service by Paradise Circus, Inc. — no ads, no tracking. Facts and product details are accurate as of October 28, 2026; check the original for the latest version.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32844/lock-your-inbox-email-2fa-passkeys-and-app-passwords</guid>
<pubDate>Wed, 28 Oct 2026 13:51:22 +0000</pubDate>
</item>
<item>
<title>Building Verifiable Bounty Payments and Revocable Account Sessions at MyZubster Over the latest MyZu</title>
<link>https://coderlegion.com/28935/building-verifiable-bounty-payments-revocable-account-sessions-myzubster-over-latest</link>
<description>&lt;h2 id=&quot;engineering-myzubster-verifiable-crypto-settlements-refresh-token-rotation-revocable-sessions-and-cross-mongoose-runtime-hardening&quot;&gt;Engineering MyZubster: Verifiable Crypto Settlements, Refresh-Token Rotation, Revocable Sessions, and Cross-Mongoose Runtime Hardening&lt;/h2&gt;
&lt;p&gt;MyZubster recently completed a substantial infrastructure iteration spanning two systems that initially appear unrelated: contributor bounty settlement and authentication/session security.&lt;/p&gt;
&lt;p&gt;They share an important engineering requirement:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;external or client-side state must not be treated as authoritative without server-side verification and persistence.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For bounty payments, a blockchain transaction needs to become a verified, replay-safe treasury record.&lt;/p&gt;
&lt;p&gt;For authentication, possession of a cryptographically valid JWT should not permanently imply that a session is still authorized.&lt;/p&gt;
&lt;p&gt;This development cycle introduced infrastructure addressing both problems.&lt;/p&gt;
&lt;p&gt;This article goes through the implementation, security properties, failure modes we discovered during integration testing, and the validation performed before pushing the final changes.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-the-systems-we-changed&quot;&gt;1. The Systems We Changed&lt;/h3&gt;
&lt;p&gt;The work covered two major areas.&lt;/p&gt;
&lt;h4 id=&quot;treasury-and-bounty-settlement&quot;&gt;Treasury and bounty settlement&lt;/h4&gt;
&lt;p&gt;We implemented infrastructure for recording verified cryptocurrency conversions associated with bounty settlement.&lt;/p&gt;
&lt;p&gt;The flow now supports:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;external transaction
        │
        ▼
blockchain reference
        │
        ▼
conversion verification
        │
        ▼
normalized treasury object
        │
        ▼
dry-run validation
        │
        ▼
idempotency check
        │
        ▼
persistent settlement record
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;authentication-and-account-security&quot;&gt;Authentication and account security&lt;/h4&gt;
&lt;p&gt;MYZ-71 introduced a more substantial authentication lifecycle:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;authentication
      │
      ▼
persistent server session
      │
      ├── short-lived access token
      │
      └── rotating opaque refresh token
                    │
                    ▼
             replay detection
                    │
                    ▼
              session revocation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The frontend then gained automatic renewal, account/device controls, terminal-expiry recovery, and CSRF protection for cookie-authenticated mutations.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-i-verifiable-cryptocurrency-bounty-settlement&quot;&gt;Part I — Verifiable Cryptocurrency Bounty Settlement&lt;/h2&gt;
&lt;h3 id=&quot;2-moving-beyond-a-transaction-was-sent-&quot;&gt;2. Moving Beyond “A Transaction Was Sent”&lt;/h3&gt;
&lt;p&gt;Sending cryptocurrency is only one part of a bounty system.&lt;/p&gt;
&lt;p&gt;From an engineering perspective, the application needs to answer several additional questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Why was the transaction made?&lt;/li&gt;
&lt;li&gt;Which asset left the treasury?&lt;/li&gt;
&lt;li&gt;Which asset arrived?&lt;/li&gt;
&lt;li&gt;Which network was used?&lt;/li&gt;
&lt;li&gt;What external provider participated?&lt;/li&gt;
&lt;li&gt;Which blockchain transaction proves the operation?&lt;/li&gt;
&lt;li&gt;Has the conversion been independently verified?&lt;/li&gt;
&lt;li&gt;Has this exact operation already been recorded?&lt;/li&gt;
&lt;li&gt;Can the dashboard consume the result without directly coupling itself to persistence?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the settlement flow implemented during this cycle, the normalized record contains information such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;purpose&quot;: &quot;BOUNTY_SETTLEMENT&quot;,
  &quot;provider&quot;: &quot;SimpleSwap&quot;,
  &quot;source&quot;: {
    &quot;asset&quot;: &quot;BTC&quot;,
    &quot;amount&quot;: &quot;0.000464&quot;,
    &quot;txId&quot;: &quot;2ea0bc209a2d5bcfbf520f830f1a39dee0691e47c461afc316e5aeb2e83cdd2d&quot;
  },
  &quot;target&quot;: {
    &quot;asset&quot;: &quot;ETH&quot;,
    &quot;network&quot;: &quot;ethereum-mainnet&quot;,
    &quot;amount&quot;: &quot;0.014355678390291923&quot;,
    &quot;txId&quot;: &quot;0x62b58e69931ef6046d8f68df39c118fe0b1fc7234b88c780d26a8b53fdcddcc6&quot;
  },
  &quot;state&quot;: &quot;CONVERSION_COMPLETED&quot;,
  &quot;verification&quot;: {
    &quot;status&quot;: &quot;VERIFIED&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This represents the BTC → ETH treasury conversion as application state rather than leaving the operation as an external exchange receipt or manually interpreted transaction history.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-separating-verification-from-persistence&quot;&gt;3. Separating Verification From Persistence&lt;/h3&gt;
&lt;p&gt;A financial script should not immediately mutate production data simply because it successfully parsed its input.&lt;/p&gt;
&lt;p&gt;We therefore implemented a dry-run path.&lt;/p&gt;
&lt;p&gt;The sequence becomes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;construct record
     │
     ▼
validate input
     │
     ▼
verify external reference
     │
     ▼
print normalized result
     │
     ▼
STOP
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The operator sees:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;DRY-RUN OK — no database write performed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only explicit commit mode proceeds to persistence.&lt;/p&gt;
&lt;p&gt;This creates a useful operational boundary:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;verification != mutation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That distinction matters for treasury operations because malformed metadata can be caught before it becomes part of the persistent accounting history.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-idempotent-conversion-recording&quot;&gt;4. Idempotent Conversion Recording&lt;/h3&gt;
&lt;p&gt;Financial scripts are frequently rerun.&lt;/p&gt;
&lt;p&gt;A terminal disconnect, uncertain command result, deployment retry, or operator mistake should not produce a second logical settlement record.&lt;/p&gt;
&lt;p&gt;The first committed execution recorded the conversion:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;RECORDED
{
  &quot;id&quot;: &quot;6ab96bcb4c201256912e837a&quot;,
  &quot;purpose&quot;: &quot;BOUNTY_SETTLEMENT&quot;,
  &quot;state&quot;: &quot;CONVERSION_COMPLETED&quot;,
  &quot;verificationStatus&quot;: &quot;VERIFIED&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Repeating the operation returned:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;IDEMPOTENT REPLAY — conversion already recorded
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;instead of creating a duplicate.&lt;/p&gt;
&lt;p&gt;The desired invariant is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;same external conversion
        +
same transaction identity
        =
same internal logical record
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is much safer than relying on the operator to remember whether a previous command completed.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-provider-boundary-for-settlement-observability&quot;&gt;5. Provider Boundary for Settlement Observability&lt;/h3&gt;
&lt;p&gt;The settlement dashboard should not need to understand database internals.&lt;/p&gt;
&lt;p&gt;We introduced a conversion-provider boundary so the dashboard can receive preloaded conversion information.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;buildDashboard({
  conversionProvider: () =&amp;gt; conversion
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This provides two useful properties.&lt;/p&gt;
&lt;p&gt;First, the dashboard can consume treasury state without performing hidden database I/O.&lt;/p&gt;
&lt;p&gt;Second, provider failure does not need to crash the entire dashboard.&lt;/p&gt;
&lt;p&gt;If the repository fails, the conversion layer can instead return:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;configured: false
items: []
reason: conversion repository unavailable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and emit an explicit warning.&lt;/p&gt;
&lt;p&gt;That is deliberate degraded behavior.&lt;/p&gt;
&lt;p&gt;For operational dashboards, an unavailable subsystem should be visible as unavailable — not silently represented as empty valid state and not allowed to crash unrelated observability.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-ii-revocable-authentication-sessions&quot;&gt;Part II — Revocable Authentication Sessions&lt;/h2&gt;
&lt;h3 id=&quot;6-why-jwt-validation-alone-was-not-enough&quot;&gt;6. Why JWT Validation Alone Was Not Enough&lt;/h3&gt;
&lt;p&gt;A signed JWT answers an important question:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Was this token issued by a trusted signer and has its cryptographic validity expired?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It does not necessarily answer:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Does the server still want this browser session to exist?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Those are different questions.&lt;/p&gt;
&lt;p&gt;A user may need to revoke a stolen device.&lt;/p&gt;
&lt;p&gt;A refresh token may have been replayed.&lt;/p&gt;
&lt;p&gt;An administrator or security flow may invalidate a session.&lt;/p&gt;
&lt;p&gt;A user may explicitly log out.&lt;/p&gt;
&lt;p&gt;For these cases, authentication needs authoritative server-side state.&lt;/p&gt;
&lt;p&gt;We introduced persistent &lt;code&gt;AuthSession&lt;/code&gt; records.&lt;/p&gt;
&lt;p&gt;The resulting model is conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;User
 │
 ├── AuthSession A
 │      ├── sessionId
 │      ├── expiry
 │      ├── device metadata
 │      ├── lastSeenAt
 │      ├── refreshTokenHash
 │      └── revocation state
 │
 ├── AuthSession B
 │
 └── AuthSession C
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Access JWTs contain a session identifier.&lt;/p&gt;
&lt;p&gt;JWT verification can therefore be followed by server-session validation.&lt;/p&gt;
&lt;p&gt;A valid signature no longer necessarily means an active session.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-refresh-tokens-became-opaque-persistent-and-rotating&quot;&gt;7. Refresh Tokens Became Opaque, Persistent, and Rotating&lt;/h3&gt;
&lt;p&gt;The new refresh credential is an opaque token.&lt;/p&gt;
&lt;p&gt;Its raw value is not persisted.&lt;/p&gt;
&lt;p&gt;Instead, the server stores an HMAC-SHA256 representation.&lt;/p&gt;
&lt;p&gt;The lifecycle looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;refresh token R1
      │
      ▼
hash / identify
      │
      ▼
validate session
      │
      ▼
atomically consume R1
      │
      ▼
issue R2
      │
      ▼
store hash(R2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;R1 is now consumed.&lt;/p&gt;
&lt;p&gt;The server maintains bounded history for previously consumed refresh-token hashes.&lt;/p&gt;
&lt;p&gt;The bounded part matters: security metadata must not create an indefinitely growing session document.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;8-atomic-rotation&quot;&gt;8. Atomic Rotation&lt;/h3&gt;
&lt;p&gt;Refresh rotation introduces a concurrency problem.&lt;/p&gt;
&lt;p&gt;Imagine two requests attempting to use R1 at nearly the same time:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Request A ── R1 ──┐
                  ├── session
Request B ── R1 ──┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A naive implementation could allow both requests to observe R1 as valid before either update becomes visible.&lt;/p&gt;
&lt;p&gt;Rotation therefore needs an atomic state transition.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;UPDATE AuthSession
WHERE
    session = S
AND currentRefreshHash = hash(R1)

SET
    currentRefreshHash = hash(R2)
    consumed += hash(R1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only the request that successfully matches the expected current state should rotate the token.&lt;/p&gt;
&lt;p&gt;This is effectively compare-and-swap behavior applied to authentication state.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;9-refresh-replay-detection&quot;&gt;9. Refresh Replay Detection&lt;/h3&gt;
&lt;p&gt;Single-use refresh tokens provide another security signal.&lt;/p&gt;
&lt;p&gt;If R1 was already consumed and later appears again, something unusual happened.&lt;/p&gt;
&lt;p&gt;Possibilities include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;credential theft;&lt;/li&gt;
&lt;li&gt;duplicated stale browser state;&lt;/li&gt;
&lt;li&gt;client bugs;&lt;/li&gt;
&lt;li&gt;copied authentication material;&lt;/li&gt;
&lt;li&gt;an actual replay attempt.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Our implementation treats reuse as a security event.&lt;/p&gt;
&lt;p&gt;The tested behavior is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;consumed refresh token reused
            │
            ▼
AUTH_REFRESH_REPLAY
            │
            ▼
revoke complete session
            │
            ▼
revokedReason = refresh-token-replay
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The response is therefore stronger than simply rejecting the stale token.&lt;/p&gt;
&lt;p&gt;The associated session itself becomes invalid.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;10-frontend-refresh-deduplication&quot;&gt;10. Frontend Refresh Deduplication&lt;/h3&gt;
&lt;p&gt;Server-side single-use credentials require corresponding client-side concurrency control.&lt;/p&gt;
&lt;p&gt;Suppose a page launches five API requests and the access token expires.&lt;/p&gt;
&lt;p&gt;All five may receive &lt;code&gt;401&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Without coordination:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;401 A → refresh
401 B → refresh
401 C → refresh
401 D → refresh
401 E → refresh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only one of those refresh attempts can legitimately consume the current refresh token.&lt;/p&gt;
&lt;p&gt;So the browser authentication client shares an in-flight refresh promise:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;401 A ─┐
401 B ─┤
401 C ─┼──► one refresh operation
401 D ─┤
401 E ─┘
              │
              ▼
          new access state
              │
              ▼
         retry requests
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This aligns frontend concurrency semantics with backend single-use token semantics.&lt;/p&gt;
&lt;p&gt;Requests are retried only once.&lt;/p&gt;
&lt;p&gt;The client does not enter an infinite &lt;code&gt;401 → refresh → retry&lt;/code&gt; loop.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-iii-account-security&quot;&gt;Part III — Account Security&lt;/h2&gt;
&lt;h3 id=&quot;11-session-management-api&quot;&gt;11. Session Management API&lt;/h3&gt;
&lt;p&gt;The session architecture is exposed through account-security endpoints:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;POST   /api/auth/refresh
POST   /api/auth/logout
GET    /api/auth/me
GET    /api/auth/me/sessions
DELETE /api/auth/me/sessions/:sessionId
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These endpoints provide the foundation for user-controlled session management.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;12-active-device-management&quot;&gt;12. Active Device Management&lt;/h3&gt;
&lt;p&gt;The frontend now contains an account-security surface.&lt;/p&gt;
&lt;p&gt;Users can inspect session information including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;whether the session is the current one;&lt;/li&gt;
&lt;li&gt;creation time;&lt;/li&gt;
&lt;li&gt;last activity;&lt;/li&gt;
&lt;li&gt;expiry;&lt;/li&gt;
&lt;li&gt;available revocation actions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A remote session can be revoked without destroying every other login.&lt;/p&gt;
&lt;p&gt;The current session can also be terminated.&lt;/p&gt;
&lt;p&gt;When that happens, server-side state is revoked and browser authentication state is cleared.&lt;/p&gt;
&lt;p&gt;This turns logout from a mostly client-side token deletion into an actual server-side authorization transition.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;13-session-metadata-and-privacy&quot;&gt;13. Session Metadata and Privacy&lt;/h3&gt;
&lt;p&gt;Session observability is useful, but it should not require retaining unnecessary sensitive network data.&lt;/p&gt;
&lt;p&gt;The session implementation therefore does not store raw client IP addresses as plain values.&lt;/p&gt;
&lt;p&gt;Instead, IP information can be represented through an HMAC-SHA256 digest.&lt;/p&gt;
&lt;p&gt;This allows certain correlation/security use cases without turning the session collection into a direct historical database of raw IP addresses.&lt;/p&gt;
&lt;p&gt;Session lifetime is also bounded.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-iv-authentication-recovery-in-the-metaverse&quot;&gt;Part IV — Authentication Recovery in the Metaverse&lt;/h2&gt;
&lt;h3 id=&quot;14-a-renewable-authenticated-fetch-layer&quot;&gt;14. A Renewable Authenticated Fetch Layer&lt;/h3&gt;
&lt;p&gt;The Metaverse API was moved onto a reusable authenticated request layer.&lt;/p&gt;
&lt;p&gt;The high-level flow is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;API request
    │
    ▼
access accepted?
 ┌──┴──┐
yes    no
 │      │
return  ▼
     renewable auth failure?
          │
       ┌──┴──┐
       no    yes
       │      │
      fail    ▼
           refresh
              │
              ▼
          retry once
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Not every &lt;code&gt;401&lt;/code&gt; is renewable.&lt;/p&gt;
&lt;p&gt;An explicitly revoked session, for example, should not cause endless attempts to resurrect authentication.&lt;/p&gt;
&lt;p&gt;The client distinguishes terminal authentication state from renewable access-token failure.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;15-terminal-expiry-events&quot;&gt;15. Terminal Expiry Events&lt;/h3&gt;
&lt;p&gt;If refresh fails terminally, the browser:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;clears migration authentication state;&lt;/li&gt;
&lt;li&gt;emits an authentication-expired event;&lt;/li&gt;
&lt;li&gt;redirects through the login flow.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This behavior was connected to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Neon Plaza;&lt;/li&gt;
&lt;li&gt;Metaverse room pages;&lt;/li&gt;
&lt;li&gt;room creation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Multiple concurrent failures are deduplicated so the application does not perform several competing redirects.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;16-preserving-the-intended-destination&quot;&gt;16. Preserving the Intended Destination&lt;/h3&gt;
&lt;p&gt;Authentication expiry should not destroy user navigation context.&lt;/p&gt;
&lt;p&gt;The system preserves a local return path so a user can authenticate and return to the intended Metaverse location.&lt;/p&gt;
&lt;p&gt;That can include:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/metaverse/rooms/example?invite=...#section
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But return paths are also security-sensitive.&lt;/p&gt;
&lt;p&gt;Protocol-relative destinations are rejected.&lt;/p&gt;
&lt;p&gt;The application falls back to a known local Metaverse path when the return target is unsafe.&lt;/p&gt;
&lt;p&gt;This prevents the convenience mechanism from becoming an open-redirect primitive.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-v-csrf-protection&quot;&gt;Part V — CSRF Protection&lt;/h2&gt;
&lt;h3 id=&quot;17-cookies-changed-the-threat-model&quot;&gt;17. Cookies Changed the Threat Model&lt;/h3&gt;
&lt;p&gt;Once authentication relies on HttpOnly cookies, the browser automatically attaches those credentials to matching requests.&lt;/p&gt;
&lt;p&gt;That is useful for protecting credentials from direct JavaScript access, but it also means CSRF must be addressed explicitly.&lt;/p&gt;
&lt;p&gt;The server now distinguishes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Authorization: Bearer ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;from ambient cookie authentication.&lt;/p&gt;
&lt;p&gt;Unsafe cookie-authenticated browser mutations require trusted request-origin information.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;18-origin-referer-and-fetch-metadata&quot;&gt;18. Origin, Referer, and Fetch Metadata&lt;/h3&gt;
&lt;p&gt;For applicable unsafe methods, the server evaluates browser request context.&lt;/p&gt;
&lt;p&gt;Cross-site Fetch Metadata can be rejected before session lookup:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Sec-Fetch-Site: cross-site
        │
        ▼
      reject
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Trusted &lt;code&gt;Origin&lt;/code&gt; or &lt;code&gt;Referer&lt;/code&gt; information is checked for cookie-authenticated mutations.&lt;/p&gt;
&lt;p&gt;Rejected requests return a structured error:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;AUTH_CSRF_REJECTED
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Trusted production origins can be configured rather than hardcoded.&lt;/p&gt;
&lt;p&gt;Bearer clients remain supported during the migration because explicit bearer credentials have different ambient-credential behavior from browser cookies.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-vi-structured-authentication-errors&quot;&gt;Part VI — Structured Authentication Errors&lt;/h2&gt;
&lt;h3 id=&quot;19-error-codes-instead-of-raw-cryptographic-errors&quot;&gt;19. Error Codes Instead of Raw Cryptographic Errors&lt;/h3&gt;
&lt;p&gt;Raw JWT/library exceptions are not a good public API.&lt;/p&gt;
&lt;p&gt;The authentication layer now uses structured application errors and request IDs.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;error&quot;: {
    &quot;code&quot;: &quot;AUTH_...&quot;,
    &quot;request_id&quot;: &quot;...&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates a stable contract between server and frontend.&lt;/p&gt;
&lt;p&gt;The UI can distinguish:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;access expiry;&lt;/li&gt;
&lt;li&gt;revoked sessions;&lt;/li&gt;
&lt;li&gt;refresh replay;&lt;/li&gt;
&lt;li&gt;terminal refresh failure;&lt;/li&gt;
&lt;li&gt;CSRF rejection;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;without parsing arbitrary exception strings.&lt;/p&gt;
&lt;p&gt;Request IDs also improve operational debugging.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-vii-the-mongoose-bson-failure-we-found-during-testing&quot;&gt;Part VII — The Mongoose/BSON Failure We Found During Testing&lt;/h2&gt;
&lt;h3 id=&quot;20-the-initial-symptom&quot;&gt;20. The Initial Symptom&lt;/h3&gt;
&lt;p&gt;One of the most useful results of the final integration test was not a passing test.&lt;/p&gt;
&lt;p&gt;It was a hang.&lt;/p&gt;
&lt;p&gt;The social-identity suite stopped at:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;RUNS tests/socialIdentityService.test.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Eventually it failed with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;MongooseError:
Operation `metaversecharacters.findOne()` buffering timed out after 10000ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same thing happened during test cleanup:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;metaversecharacters.deleteMany()
buffering timed out
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At first glance, this looked like a MongoMemoryServer or asynchronous-test problem.&lt;/p&gt;
&lt;p&gt;It wasn't.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;21-two-mongoose-runtimes-in-one-process&quot;&gt;21. Two Mongoose Runtimes in One Process&lt;/h3&gt;
&lt;p&gt;The root application used:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Mongoose 7.8.12
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;while the nested backend used:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Mongoose 8.24.3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;User&lt;/code&gt; and &lt;code&gt;AuthSession&lt;/code&gt; came from the root runtime.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MetaverseCharacter&lt;/code&gt; came from:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;backend/src/models/MetaverseCharacter
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and that model resolved its own:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;require('mongoose')
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;against the backend installation.&lt;/p&gt;
&lt;p&gt;So the test had effectively created:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;MongoMemoryServer
       │
       ▼
root Mongoose 7
       │
       ├── User
       └── AuthSession


backend Mongoose 8
       │
       └── MetaverseCharacter
            X
       no connected test DB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The timeout suddenly made sense.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;22-connecting-both-runtimes-revealed-the-deeper-problem&quot;&gt;22. Connecting Both Runtimes Revealed the Deeper Problem&lt;/h3&gt;
&lt;p&gt;As a diagnostic step, both Mongoose instances were connected to the same in-memory MongoDB.&lt;/p&gt;
&lt;p&gt;The timeout disappeared.&lt;/p&gt;
&lt;p&gt;But the next error was more informative:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;BSONVersionError:
Unsupported BSON version,
bson types must be from bson 6.x.x
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An ObjectId produced by one Mongoose/BSON ecosystem was being passed into a model using another BSON implementation.&lt;/p&gt;
&lt;p&gt;So simply connecting both instances was not a sound fix.&lt;/p&gt;
&lt;p&gt;It exposed the deeper boundary problem.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;23-model-factory-as-the-boundary-repair&quot;&gt;23. Model Factory as the Boundary Repair&lt;/h3&gt;
&lt;p&gt;We did not solve this by increasing Jest timeouts.&lt;/p&gt;
&lt;p&gt;We did not keep &lt;code&gt;--forceExit&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We did not normalize every ObjectId manually and hope no other BSON type crossed the boundary.&lt;/p&gt;
&lt;p&gt;Instead, &lt;code&gt;MetaverseCharacter&lt;/code&gt; was changed to expose a model factory.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function createMetaverseCharacterModel(mongoose) {
  const schema = new mongoose.Schema({
    // ...
  });

  return mongoose.models.MetaverseCharacter ||
    mongoose.model('MetaverseCharacter', schema);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Backend consumers can continue using the backend Mongoose runtime.&lt;/p&gt;
&lt;p&gt;Root consumers can instantiate the same schema using the root runtime.&lt;/p&gt;
&lt;p&gt;That changes the topology to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Root application
       │
       ▼
root Mongoose
       │
       ├── User
       ├── AuthSession
       └── MetaverseCharacter
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Backend application
       │
       ▼
backend Mongoose
       │
       └── MetaverseCharacter
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The schema definition can be shared without forcing two incompatible Mongoose runtime objects into the same database operation.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;24-why-this-matters-beyond-tests&quot;&gt;24. Why This Matters Beyond Tests&lt;/h3&gt;
&lt;p&gt;It would have been easy to classify the failure as “just a Jest problem.”&lt;/p&gt;
&lt;p&gt;That would have been incorrect.&lt;/p&gt;
&lt;p&gt;The test exposed an actual architectural boundary:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;shared model source
≠
shared Mongoose runtime
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Node's module resolution can load different versions of the same dependency from different package trees.&lt;/p&gt;
&lt;p&gt;ODM objects such as ObjectIds, models, connections, and schemas are not automatically interchangeable simply because they ultimately represent MongoDB concepts.&lt;/p&gt;
&lt;p&gt;The lesson is broader:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;When a Node.js process contains multiple ODM/library runtimes, pass primitives or explicitly bind shared schemas to the owning runtime. Do not assume runtime-specific objects are portable across package boundaries.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-viii-repairing-source-corruption&quot;&gt;Part VIII — Repairing Source Corruption&lt;/h2&gt;
&lt;h3 id=&quot;25-utf-8-and-control-character-validation&quot;&gt;25. UTF-8 and Control-Character Validation&lt;/h3&gt;
&lt;p&gt;During the MYZ-71 repair we also discovered corrupted frontend source content.&lt;/p&gt;
&lt;p&gt;The affected surfaces included authentication/security and Metaverse pages.&lt;/p&gt;
&lt;p&gt;We explicitly checked for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Unicode replacement characters;&lt;/li&gt;
&lt;li&gt;unexpected control characters;&lt;/li&gt;
&lt;li&gt;malformed JSX;&lt;/li&gt;
&lt;li&gt;corrupted localized strings.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The affected source was repaired and then checked again before running the frontend suite.&lt;/p&gt;
&lt;p&gt;This was important because some apparent React/parser failures were not behavioral regressions at all — they were malformed source bytes or broken string literals.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;26-javascript-string-parsing-failure&quot;&gt;26. JavaScript String Parsing Failure&lt;/h3&gt;
&lt;p&gt;Another backend gate exposed a malformed JavaScript string containing an apostrophe:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Autorizzazione GitHub non associata all'account ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The single quote terminated the surrounding single-quoted JavaScript string.&lt;/p&gt;
&lt;p&gt;This prevented Babel/Jest from parsing the controller.&lt;/p&gt;
&lt;p&gt;The string was repaired and syntax validation was performed before rerunning the backend suites.&lt;/p&gt;
&lt;p&gt;This is a simple bug, but it reinforces why syntax/build gates belong in the final validation pipeline even after behavioral tests have been added.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-ix-validation&quot;&gt;Part IX — Validation&lt;/h2&gt;
&lt;h3 id=&quot;27-backend-final-gate&quot;&gt;27. Backend Final Gate&lt;/h3&gt;
&lt;p&gt;After repairing the Mongoose boundary and JavaScript syntax, the targeted backend gate completed successfully:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Test Suites: 3 passed, 3 total
Tests:       18 passed, 18 total
Time:        4.478 s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The suites covered the relevant session controller, social authentication callback, and social identity behavior.&lt;/p&gt;
&lt;p&gt;The previously hanging social identity tests now completed normally.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;28-frontend-final-gate&quot;&gt;28. Frontend Final Gate&lt;/h3&gt;
&lt;p&gt;The targeted frontend gate also completed successfully:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Test Suites: 4 passed, 4 total
Tests:       13 passed, 13 total
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Coverage included the authentication-expiry redirect, account-security page, auth-session API, and Metaverse API behavior.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;29-production-build&quot;&gt;29. Production Build&lt;/h3&gt;
&lt;p&gt;We then generated an optimized React production build:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Creating an optimized production build...
Compiled with warnings.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The build completed successfully.&lt;/p&gt;
&lt;p&gt;The remaining ESLint warnings were non-blocking and outside the scope of the repair.&lt;/p&gt;
&lt;p&gt;They were deliberately not mixed into the security fix simply to produce an artificially warning-free diff.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;30-git-integrity-gate&quot;&gt;30. Git Integrity Gate&lt;/h3&gt;
&lt;p&gt;Before committing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git diff --cached --check
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;completed cleanly.&lt;/p&gt;
&lt;p&gt;Generated build artifacts were removed from the commit.&lt;/p&gt;
&lt;p&gt;An unrelated untracked directory was also deliberately kept outside the MYZ-71 commit.&lt;/p&gt;
&lt;p&gt;The final repair was committed as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1878e02a
fix: harden revocable account sessions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and pushed to the MYZ-71 branch.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-x-what-is-operational-now&quot;&gt;Part X — What Is Operational Now&lt;/h2&gt;
&lt;h3 id=&quot;31-treasury&quot;&gt;31. Treasury&lt;/h3&gt;
&lt;p&gt;The implemented treasury infrastructure now supports:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;bounty settlement purpose
        │
        ▼
crypto source transaction
        │
        ▼
asset conversion
        │
        ▼
target-chain transaction
        │
        ▼
verification
        │
        ▼
dry-run
        │
        ▼
idempotent persistence
        │
        ▼
dashboard provider
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The recorded BTC → ETH operation reached:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;CONVERSION_COMPLETED
VERIFIED
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and replaying the recorder did not create a duplicate conversion.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;32-authentication&quot;&gt;32. Authentication&lt;/h3&gt;
&lt;p&gt;The implemented authentication/session infrastructure now provides:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;login
 │
 ▼
persistent AuthSession
 │
 ├── access JWT bound to session
 │
 └── opaque rotating refresh credential
          │
          ├── atomic rotation
          ├── bounded consumed-token history
          └── replay detection
                   │
                   ▼
              session revocation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The frontend adds:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;authenticated request
       │
       ▼
renew if allowed
       │
       ▼
retry once
       │
       ▼
terminal failure?
       │
       ▼
clear state + auth-expired event
       │
       ▼
login
       │
       ▼
restore safe local destination
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-xi-security-properties-achieved&quot;&gt;Part XI — Security Properties Achieved&lt;/h2&gt;
&lt;p&gt;The resulting implementation has several concrete security properties.&lt;/p&gt;
&lt;h4 id=&quot;server-authority-over-sessions&quot;&gt;Server authority over sessions&lt;/h4&gt;
&lt;p&gt;A cryptographically valid JWT can still be rejected when its server session has been revoked or removed.&lt;/p&gt;
&lt;h4 id=&quot;single-use-refresh-credentials&quot;&gt;Single-use refresh credentials&lt;/h4&gt;
&lt;p&gt;Refresh credentials rotate instead of remaining indefinitely reusable.&lt;/p&gt;
&lt;h4 id=&quot;replay-detection&quot;&gt;Replay detection&lt;/h4&gt;
&lt;p&gt;Reuse of consumed refresh state causes session revocation.&lt;/p&gt;
&lt;h4 id=&quot;no-raw-refresh-token-persistence&quot;&gt;No raw refresh-token persistence&lt;/h4&gt;
&lt;p&gt;Refresh credentials are represented server-side through cryptographic hashes.&lt;/p&gt;
&lt;h4 id=&quot;client-server-concurrency-alignment&quot;&gt;Client/server concurrency alignment&lt;/h4&gt;
&lt;p&gt;The frontend shares concurrent refresh work while the backend performs atomic token rotation.&lt;/p&gt;
&lt;h4 id=&quot;remote-session-revocation&quot;&gt;Remote session revocation&lt;/h4&gt;
&lt;p&gt;A user can invalidate another authenticated device without necessarily destroying every active session.&lt;/p&gt;
&lt;h4 id=&quot;cookie-mutation-protection&quot;&gt;Cookie mutation protection&lt;/h4&gt;
&lt;p&gt;Unsafe cookie-authenticated browser operations are protected by trusted-origin/Fetch Metadata checks.&lt;/p&gt;
&lt;h4 id=&quot;structured-auth-failures&quot;&gt;Structured auth failures&lt;/h4&gt;
&lt;p&gt;Clients receive stable authentication semantics rather than raw JWT/library errors.&lt;/p&gt;
&lt;h4 id=&quot;safe-return-navigation&quot;&gt;Safe return navigation&lt;/h4&gt;
&lt;p&gt;Authentication recovery preserves useful local navigation without accepting arbitrary protocol-relative redirect destinations.&lt;/p&gt;
&lt;h4 id=&quot;explicit-odm-runtime-ownership&quot;&gt;Explicit ODM runtime ownership&lt;/h4&gt;
&lt;p&gt;The same Metaverse schema can be bound to the Mongoose runtime that actually owns the surrounding connection and models.&lt;/p&gt;
&lt;h4 id=&quot;replay-safe-treasury-persistence&quot;&gt;Replay-safe treasury persistence&lt;/h4&gt;
&lt;p&gt;Reexecuting the same verified conversion recorder does not produce duplicate settlement state.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;part-xii-what-we-deliberately-did-not-claim&quot;&gt;Part XII — What We Deliberately Did Not Claim&lt;/h2&gt;
&lt;p&gt;Engineering reports are more useful when they clearly distinguish completed infrastructure from future work.&lt;/p&gt;
&lt;p&gt;This iteration does &lt;strong&gt;not&lt;/strong&gt; mean the entire MYZ-71 roadmap is finished.&lt;/p&gt;
&lt;p&gt;Remaining work includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;passkeys and/or magic-link authentication;&lt;/li&gt;
&lt;li&gt;step-up authentication for sensitive operations;&lt;/li&gt;
&lt;li&gt;extending automatic renewal to authenticated clients outside the current Metaverse integration;&lt;/li&gt;
&lt;li&gt;completing migration away from legacy localStorage/bearer compatibility;&lt;/li&gt;
&lt;li&gt;strict production server-session enforcement after the migration window;&lt;/li&gt;
&lt;li&gt;production trusted-origin verification;&lt;/li&gt;
&lt;li&gt;broader OAuth production verification;&lt;/li&gt;
&lt;li&gt;production MongoDB stability validation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On the treasury side, a verified conversion record is also not the final form of a contributor payment platform.&lt;/p&gt;
&lt;p&gt;Future settlement work can connect:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;bounty
  ↓
contributor
  ↓
approved amount
  ↓
payment instruction
  ↓
source transaction
  ↓
optional conversion
  ↓
destination transaction
  ↓
verification
  ↓
notification
  ↓
settled bounty state
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;into one auditable lifecycle.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The most valuable part of this development cycle was not the number of endpoints or React components added.&lt;/p&gt;
&lt;p&gt;It was the strengthening of system boundaries.&lt;/p&gt;
&lt;p&gt;For payments:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;blockchain activity
      ↓
verified application state
      ↓
idempotent treasury record
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For authentication:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;client credential
      ↓
server-authoritative session
      ↓
rotating credentials
      ↓
replay detection
      ↓
revocation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And for the Node/MongoDB architecture:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;shared schema
      ↓
explicit runtime ownership
      ↓
consistent Mongoose connection
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The final repair was validated with 18/18 targeted backend tests, 13/13 targeted frontend tests, a successful optimized frontend build, and a clean Git diff check.&lt;/p&gt;
&lt;p&gt;More importantly, integration testing exposed a real Mongoose 7/Mongoose 8 BSON boundary that would have been easy to hide with larger timeouts. Fixing the ownership boundary instead of suppressing the symptom left the architecture in a better state.&lt;/p&gt;
&lt;p&gt;MyZubster now has stronger primitives for two areas where correctness matters disproportionately: &lt;strong&gt;moving value and maintaining identity&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That gives us a much safer base for the next layer of automation.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28935/building-verifiable-bounty-payments-revocable-account-sessions-myzubster-over-latest</guid>
<pubDate>Mon, 28 Sep 2026 14:14:58 +0000</pubDate>
</item>
<item>
<title>How We Built Revocable Sessions and Refresh-Token Replay Protection for the MyZubster Metaverse</title>
<link>https://coderlegion.com/28847/how-built-revocable-sessions-and-refresh-token-replay-protection-for-myzubster-metaverse</link>
<description>&lt;p&gt;Ecco la versione in inglese pronta per CoderLegion.&lt;br&gt;
How We Built Revocable Sessions and Refresh-Token Replay Protection for the MyZubster Metaverse&lt;br&gt;
Authentication in a Metaverse platform is more than a login form.&lt;br&gt;
An authenticated identity may control characters, social relationships, private spaces, Marketplace activity and, eventually, payment-related operations. For MyZubster, relying only on long-lived JSON Web Tokens was no longer a sufficient security model.&lt;br&gt;
Under MYZ-71 — Authentication, Sessions &amp;amp; Account Security, we started replacing stateless browser authentication with persistent, revocable and observable server-side sessions.&lt;br&gt;
This article describes what we implemented, how refresh-token rotation works, how we detect token replay, what we tested and what still needs to be completed before production deployment.&lt;br&gt;
The implementation is currently in a Draft pull request. It has not been merged or declared production-ready.&lt;/p&gt;
&lt;p&gt;Why stateless JWT authentication was not enough&lt;br&gt;
JWTs are useful because a server can validate them without loading a database record for every request.&lt;br&gt;
However, a completely stateless model creates important limitations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;logout cannot reliably invalidate an existing token;&lt;/li&gt;
&lt;li&gt;users cannot see which devices are authenticated;&lt;/li&gt;
&lt;li&gt;administrators cannot revoke one compromised device;&lt;/li&gt;
&lt;li&gt;a stolen token remains valid until expiration;&lt;/li&gt;
&lt;li&gt;password changes do not automatically terminate sessions;&lt;/li&gt;
&lt;li&gt;long-lived tokens increase the potential impact of credential theft;&lt;/li&gt;
&lt;li&gt;tokens stored in localStorage can be accessed by malicious JavaScript after an XSS compromise.&lt;br&gt;
We therefore introduced a hybrid architecture:&lt;br&gt;
Authentication&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Persistent server-side session&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Short-lived JWT access token&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rotating opaque refresh token&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replay detection and session revocation&lt;br&gt;
JWTs are still used for request authentication, but the server-side session is now the final source of authority.&lt;br&gt;
Persistent server-side sessions&lt;br&gt;
We added a MongoDB AuthSession model.&lt;br&gt;
A separate session is created for each authenticated device. The record contains:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;a unique session identifier;&lt;/li&gt;
&lt;li&gt;the user who owns the session;&lt;/li&gt;
&lt;li&gt;creation and expiration timestamps;&lt;/li&gt;
&lt;li&gt;the last activity timestamp;&lt;/li&gt;
&lt;li&gt;revocation state and reason;&lt;/li&gt;
&lt;li&gt;limited device metadata;&lt;/li&gt;
&lt;li&gt;a privacy-preserving client-IP digest;&lt;/li&gt;
&lt;li&gt;the current refresh-token hash;&lt;/li&gt;
&lt;li&gt;a bounded history of consumed refresh-token hashes;&lt;/li&gt;
&lt;li&gt;refresh-token rotation metadata.&lt;br&gt;
MongoDB TTL indexing is used to remove expired session records automatically.&lt;br&gt;
This enables real session lifecycle management. A valid JWT is rejected whenever its associated session is expired, missing or revoked.&lt;br&gt;
Session-bound access tokens&lt;br&gt;
New JWT access tokens include a server-generated sid claim.&lt;br&gt;
A simplified payload looks like this:&lt;br&gt;
{&lt;br&gt;
&quot;sub&quot;: &quot;user-id&quot;,&lt;br&gt;
&quot;sid&quot;: &quot;session-uuid&quot;,&lt;br&gt;
&quot;jti&quot;: &quot;access-token-uuid&quot;,&lt;br&gt;
&quot;iat&quot;: 1790540000,&lt;br&gt;
&quot;exp&quot;: 1790540900&lt;br&gt;
}&lt;br&gt;
Authentication now involves two checks:&lt;ol&gt;
&lt;li&gt;Verify the JWT signature and expiration&lt;/li&gt;
&lt;li&gt;Validate the persistent session referenced by sid&lt;br&gt;
This architecture preserves the advantages of JWTs while restoring the ability to revoke access immediately.&lt;br&gt;
If the user removes a device from the account-security page, the session becomes invalid even if its most recent JWT has not expired.&lt;br&gt;
Opaque refresh tokens&lt;br&gt;
The refresh token is not a JWT and does not contain trusted user data.&lt;br&gt;
Its conceptual format is:&lt;br&gt;
myzr.&amp;lt;session-id&amp;gt;.&amp;lt;cryptographically-random-secret&amp;gt;&lt;br&gt;
The random section is produced using a cryptographically secure random-number generator.&lt;br&gt;
The complete refresh token is sent to the authenticated browser, but it is never stored directly in MongoDB.&lt;br&gt;
Instead, the backend calculates:&lt;br&gt;
HMAC-SHA256(server secret, raw refresh token)&lt;br&gt;
Only the resulting digest is saved.&lt;br&gt;
This prevents a database leak from immediately exposing usable refresh credentials.&lt;br&gt;
Protected authentication cookies&lt;br&gt;
Refresh tokens are stored in cookies configured with:&lt;br&gt;
HttpOnly&lt;br&gt;
SameSite=Lax&lt;br&gt;
Secure in production&lt;br&gt;
Path=/api/auth&lt;br&gt;
HttpOnly prevents frontend JavaScript from reading the credential.&lt;br&gt;
The restricted path reduces the number of requests carrying the refresh cookie.&lt;br&gt;
Access tokens can also be received through an HttpOnly cookie. Bearer-token authentication remains temporarily supported to avoid breaking clients during the migration.&lt;br&gt;
Atomic refresh-token rotation&lt;br&gt;
A refresh token is single-use.&lt;br&gt;
When the browser calls:&lt;br&gt;
POST /api/auth/refresh&lt;br&gt;
the backend:&lt;/li&gt;
&lt;li&gt;reads the refresh cookie;&lt;/li&gt;
&lt;li&gt;extracts the session identifier;&lt;/li&gt;
&lt;li&gt;calculates the HMAC digest of the submitted token;&lt;/li&gt;
&lt;li&gt;generates a new opaque token;&lt;/li&gt;
&lt;li&gt;atomically compares the submitted hash with the current stored hash;&lt;/li&gt;
&lt;li&gt;replaces the stored hash;&lt;/li&gt;
&lt;li&gt;records the consumed hash;&lt;/li&gt;
&lt;li&gt;issues a new access token;&lt;/li&gt;
&lt;li&gt;sends new access and refresh cookies.&lt;br&gt;
The database update only succeeds if:&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;the session exists;&lt;/li&gt;
&lt;li&gt;the session has not expired;&lt;/li&gt;
&lt;li&gt;the session has not been revoked;&lt;/li&gt;
&lt;li&gt;the submitted hash is still the current refresh-token hash.&lt;br&gt;
This is effectively a compare-and-swap operation.&lt;br&gt;
If two requests try to use the same refresh token simultaneously, only one can rotate it successfully.&lt;br&gt;
Refresh-token replay detection&lt;br&gt;
Rotation prevents normal reuse, but the system must also detect when an old token appears again.&lt;br&gt;
After every successful rotation, the consumed token hash is placed in a bounded history.&lt;br&gt;
If a token from that history is submitted again, the server treats it as a possible credential compromise.&lt;br&gt;
Previously consumed token submitted&lt;pre&gt;&lt;code&gt;          ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replay detected&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;          ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Entire server-side session revoked&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;          ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Authentication cookies cleared&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;          ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A new login is required&lt;br&gt;
The session receives the revocation reason:&lt;br&gt;
refresh-token-replay&lt;br&gt;
The history is limited to the latest 50 hashes so the MongoDB document cannot grow indefinitely.&lt;br&gt;
Replay does not necessarily prove malicious activity. It could also indicate an outdated client or an unusual concurrency condition. Revoking the session is nevertheless the safest response.&lt;br&gt;
New authentication endpoints&lt;br&gt;
The current implementation provides the following endpoints.&lt;br&gt;
Refresh the session&lt;br&gt;
POST /api/auth/refresh&lt;br&gt;
This endpoint rotates the refresh token and issues a new access token.&lt;br&gt;
The raw refresh token is never included in the JSON response.&lt;br&gt;
Log out&lt;br&gt;
POST /api/auth/logout&lt;br&gt;
Logout is idempotent.&lt;br&gt;
Authentication cookies are cleared even when the access token is missing, malformed, expired or already revoked.&lt;br&gt;
When possible, the refresh token is used to find and revoke the persistent session.&lt;br&gt;
Retrieve the authenticated account&lt;br&gt;
GET /api/auth/me&lt;br&gt;
Returns the authenticated user and safe session metadata.&lt;br&gt;
List active devices&lt;br&gt;
GET /api/auth/me/sessions&lt;br&gt;
Returns the user’s active sessions and indicates the current device.&lt;br&gt;
Revoke a device&lt;br&gt;
DELETE /api/auth/me/sessions/:sessionId&lt;br&gt;
Allows users to terminate another session remotely.&lt;br&gt;
Revoking the current session also clears local authentication state and returns the user to the login flow.&lt;br&gt;
Password and social authentication&lt;br&gt;
Password registration and login now create persistent sessions and issue both access and refresh cookies.&lt;br&gt;
Verified social-login flows use the same architecture.&lt;br&gt;
The raw refresh token is not placed inside:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;redirect URLs;&lt;/li&gt;
&lt;li&gt;OAuth query parameters;&lt;/li&gt;
&lt;li&gt;signed callback tickets;&lt;/li&gt;
&lt;li&gt;public API responses.&lt;br&gt;
This reduces the risk of credentials leaking through browser history, analytics systems, proxy logs or referrer headers.&lt;br&gt;
Account-security interface&lt;br&gt;
The React frontend now includes:&lt;br&gt;
/account/security&lt;br&gt;
The page displays:&lt;/li&gt;
&lt;li&gt;active sessions;&lt;/li&gt;
&lt;li&gt;the current device;&lt;/li&gt;
&lt;li&gt;session creation time;&lt;/li&gt;
&lt;li&gt;last activity;&lt;/li&gt;
&lt;li&gt;expiration time;&lt;/li&gt;
&lt;li&gt;limited device information.&lt;br&gt;
Users can remotely revoke another device or terminate the current session.&lt;br&gt;
The page is linked from the authenticated account panel and Neon Plaza.&lt;br&gt;
The deployment configuration also supports the route as a single-page application path and applies:&lt;br&gt;
X-Robots-Tag: noindex, nofollow&lt;br&gt;
Account-security pages should not be indexed by search engines.&lt;br&gt;
Privacy-preserving session metadata&lt;br&gt;
The system does not store raw client IP addresses.&lt;br&gt;
Instead, it saves an HMAC-derived digest. This provides a limited correlation signal for security purposes without retaining the original IP address as normal application data.&lt;br&gt;
Device metadata is also intentionally limited.&lt;br&gt;
Session security should help users recognize their devices without becoming an invasive tracking mechanism.&lt;br&gt;
Legacy migration support&lt;br&gt;
The existing frontend still contains API clients that expect a token in localStorage or use:&lt;br&gt;
Authorization: Bearer &lt;br&gt;
Removing this immediately would break existing routes and clients.&lt;br&gt;
The migration is therefore divided into stages:&lt;br&gt;
Persistent sessions and cookie support&lt;pre&gt;&lt;code&gt;          ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rotating refresh tokens&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;          ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Centralized access-token renewal&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;          ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CSRF protection&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;          ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cookie-only authentication&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;          ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expiration of legacy browser tokens&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;          ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Strict server-session enforcement&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;          ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Removal of localStorage and bearer migration&lt;br&gt;
An environment option is available for strict enforcement:&lt;br&gt;
REQUIRE_SERVER_SESSION=true&lt;br&gt;
It should only be activated after existing clients and legacy credentials have been migrated.&lt;br&gt;
Testing the implementation&lt;br&gt;
The focused authentication implementation currently passes:&lt;br&gt;
Backend:  30/30 tests&lt;br&gt;
Frontend: 12/12 tests&lt;br&gt;
The React production build also completes successfully.&lt;br&gt;
Real MongoDB integration coverage verifies the complete replay lifecycle:&lt;br&gt;
Create persistent session&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use current refresh token&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rotate successfully&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Submit the old token again&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reject the replay&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Revoke the server-side session&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ↓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Persist refresh-token-replay as the reason&lt;br&gt;
The following authentication suites also pass in GitHub Actions:&lt;br&gt;
tests/authSessionService.test.js&lt;br&gt;
tests/authSessionController.test.js&lt;br&gt;
tests/socialIdentityService.test.js&lt;br&gt;
Repository-wide CI status&lt;br&gt;
The complete repository pipeline is not yet green.&lt;br&gt;
The latest full backend execution reported:&lt;br&gt;
141 test suites passed&lt;br&gt;
42 test suites failed&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;738 tests passed&lt;br&gt;
44 tests failed&lt;br&gt;
The failing tests mainly concern pre-existing behavior in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Zorgax payments and payment intents;&lt;/li&gt;
&lt;li&gt;Marketplace flows;&lt;/li&gt;
&lt;li&gt;seller-membership policies;&lt;/li&gt;
&lt;li&gt;older Metaverse authentication expectations;&lt;/li&gt;
&lt;li&gt;Metaverse observability;&lt;/li&gt;
&lt;li&gt;legacy source-code assertion tests.&lt;br&gt;
The Security Audit and MYZ-164 policy workflows also remain red because of previously identified repository-wide conditions.&lt;br&gt;
The new MYZ-71 session and refresh-token suites are green, but the pull request must remain a Draft until the wider baseline and the remaining authentication work are addressed.&lt;br&gt;
What still needs to be implemented&lt;br&gt;
Centralized access-token renewal&lt;br&gt;
The frontend needs a shared API layer that can:&lt;ol&gt;
&lt;li&gt;identify an expired access token;&lt;/li&gt;
&lt;li&gt;call /api/auth/refresh;&lt;/li&gt;
&lt;li&gt;retry the original request;&lt;/li&gt;
&lt;li&gt;prevent simultaneous refresh calls;&lt;/li&gt;
&lt;li&gt;redirect to login when renewal fails.&lt;br&gt;
Without a centralized implementation, individual pages may respond differently when the access token expires.&lt;br&gt;
CSRF protection&lt;br&gt;
Cookie-based authentication requires explicit protection for state-changing operations.&lt;br&gt;
The final design should combine appropriate controls such as:&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;origin validation;&lt;/li&gt;
&lt;li&gt;CSRF tokens;&lt;/li&gt;
&lt;li&gt;double-submit cookies;&lt;/li&gt;
&lt;li&gt;carefully selected SameSite policies;&lt;/li&gt;
&lt;li&gt;rejection of unsafe cross-origin requests.&lt;br&gt;
SameSite=Lax should not be considered the only defence.&lt;br&gt;
Removal of browser-stored tokens&lt;br&gt;
The current frontend still updates the legacy localStorage token as a temporary compatibility bridge.&lt;br&gt;
The final architecture should not expose reusable authentication credentials to frontend JavaScript.&lt;br&gt;
Before removing localStorage, we must migrate:&lt;/li&gt;
&lt;li&gt;every authenticated API client;&lt;/li&gt;
&lt;li&gt;protected React routes;&lt;/li&gt;
&lt;li&gt;realtime and websocket authentication;&lt;/li&gt;
&lt;li&gt;mobile or embedded clients;&lt;/li&gt;
&lt;li&gt;old sessions and legacy JWTs.&lt;br&gt;
Passkeys and magic links&lt;br&gt;
The MYZ-71 roadmap includes passwordless authentication based on flows such as:&lt;br&gt;
/auth/start&lt;br&gt;
/auth/verify&lt;br&gt;
Passkey support will require correct WebAuthn management for:&lt;/li&gt;
&lt;li&gt;relying-party configuration;&lt;/li&gt;
&lt;li&gt;allowed origins;&lt;/li&gt;
&lt;li&gt;challenge creation and expiration;&lt;/li&gt;
&lt;li&gt;credential counters;&lt;/li&gt;
&lt;li&gt;credential revocation;&lt;/li&gt;
&lt;li&gt;account recovery.&lt;br&gt;
Magic links will require short-lived, single-use verification tokens and replay protection.&lt;br&gt;
Step-up authentication&lt;br&gt;
Sensitive operations should require recent stronger verification.&lt;br&gt;
Examples include:&lt;/li&gt;
&lt;li&gt;changing the primary email;&lt;/li&gt;
&lt;li&gt;changing a password;&lt;/li&gt;
&lt;li&gt;registering or deleting passkeys;&lt;/li&gt;
&lt;li&gt;deleting the account;&lt;/li&gt;
&lt;li&gt;accessing wallet or payment functionality;&lt;/li&gt;
&lt;li&gt;modifying high-impact Marketplace settings.&lt;br&gt;
A valid session should not automatically authorize every sensitive action indefinitely.&lt;br&gt;
Password-change session policy&lt;br&gt;
The application must define what happens after a password change:&lt;/li&gt;
&lt;li&gt;revoke every session;&lt;/li&gt;
&lt;li&gt;keep only the current session;&lt;/li&gt;
&lt;li&gt;allow the user to choose;&lt;/li&gt;
&lt;li&gt;require step-up authentication.&lt;br&gt;
This policy must be enforced on the server and tested with real session records.&lt;br&gt;
Production OAuth validation&lt;br&gt;
Google, GitHub and Facebook login still need production verification for:&lt;/li&gt;
&lt;li&gt;exact callback URLs;&lt;/li&gt;
&lt;li&gt;public frontend and API origins;&lt;/li&gt;
&lt;li&gt;provider secrets;&lt;/li&gt;
&lt;li&gt;redirect allowlists;&lt;/li&gt;
&lt;li&gt;account-linking behavior;&lt;/li&gt;
&lt;li&gt;missing-email cases;&lt;/li&gt;
&lt;li&gt;cookie behavior across domains.&lt;br&gt;
MongoDB Atlas validation&lt;br&gt;
The production deployment must verify:&lt;/li&gt;
&lt;li&gt;TTL-index creation;&lt;/li&gt;
&lt;li&gt;refresh-token indexes;&lt;/li&gt;
&lt;li&gt;cleanup of expired sessions;&lt;/li&gt;
&lt;li&gt;document growth;&lt;/li&gt;
&lt;li&gt;rolling-deployment compatibility;&lt;/li&gt;
&lt;li&gt;session behavior under concurrent refresh requests.&lt;br&gt;
Rate limiting&lt;br&gt;
Authentication endpoints need dedicated abuse protection.&lt;br&gt;
This includes limits for:&lt;/li&gt;
&lt;li&gt;password login;&lt;/li&gt;
&lt;li&gt;refresh requests;&lt;/li&gt;
&lt;li&gt;password recovery;&lt;/li&gt;
&lt;li&gt;magic-link generation;&lt;/li&gt;
&lt;li&gt;WebAuthn challenge creation;&lt;/li&gt;
&lt;li&gt;social-auth callbacks.&lt;br&gt;
Security monitoring&lt;br&gt;
Structured security events should be produced for:&lt;/li&gt;
&lt;li&gt;login success and failure;&lt;/li&gt;
&lt;li&gt;session creation;&lt;/li&gt;
&lt;li&gt;session revocation;&lt;/li&gt;
&lt;li&gt;remote device removal;&lt;/li&gt;
&lt;li&gt;refresh-token replay;&lt;/li&gt;
&lt;li&gt;password changes;&lt;/li&gt;
&lt;li&gt;passkey registration;&lt;/li&gt;
&lt;li&gt;suspicious authentication activity.&lt;br&gt;
Logs must never contain passwords, raw access tokens, raw refresh tokens, OAuth authorization codes or provider secrets.&lt;br&gt;
Recommended deployment sequence&lt;br&gt;
The safest rollout order is:&lt;ol&gt;
&lt;li&gt;review the Draft pull request;&lt;/li&gt;
&lt;li&gt;deploy with legacy compatibility enabled;&lt;/li&gt;
&lt;li&gt;verify MongoDB indexes;&lt;/li&gt;
&lt;li&gt;validate cookies using the real public domains;&lt;/li&gt;
&lt;li&gt;test login, refresh, logout and remote revocation;&lt;/li&gt;
&lt;li&gt;execute a controlled refresh-token replay test;&lt;/li&gt;
&lt;li&gt;implement centralized frontend renewal;&lt;/li&gt;
&lt;li&gt;add and validate CSRF protection;&lt;/li&gt;
&lt;li&gt;monitor session and replay events;&lt;/li&gt;
&lt;li&gt;allow legacy credentials to expire;&lt;/li&gt;
&lt;li&gt;activate strict server-session enforcement;&lt;/li&gt;
&lt;li&gt;remove localStorage and legacy bearer authentication;&lt;/li&gt;
&lt;li&gt;introduce passkeys, magic links and step-up authentication.&lt;br&gt;
Current progress&lt;br&gt;
Persistent server-side sessions             DONE&lt;br&gt;
Session-bound JWT access tokens              DONE&lt;br&gt;
Real logout                                  DONE&lt;br&gt;
Active-device listing                        DONE&lt;br&gt;
Remote session revocation                    DONE&lt;br&gt;
Opaque rotating refresh tokens               DONE&lt;br&gt;
Refresh-token replay detection               DONE&lt;br&gt;
Password and social-login integration        DONE&lt;br&gt;
Account-security interface                   DONE&lt;br&gt;
Focused backend and frontend testing         DONE&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Centralized access-token renewal             PENDING&lt;br&gt;
CSRF hardening                               PENDING&lt;br&gt;
Cookie-only migration                        PENDING&lt;br&gt;
Removal of localStorage authentication       PENDING&lt;br&gt;
Passkeys and magic links                     PENDING&lt;br&gt;
Step-up authentication                       PENDING&lt;br&gt;
Production OAuth validation                  PENDING&lt;br&gt;
MongoDB Atlas production validation          PENDING&lt;br&gt;
Repository-wide green CI                     PENDING&lt;br&gt;
Final thoughts&lt;br&gt;
Secure authentication is not just about generating a token.&lt;br&gt;
It requires a complete lifecycle:&lt;br&gt;
Issue&lt;br&gt;
Verify&lt;br&gt;
Renew&lt;br&gt;
Observe&lt;br&gt;
Revoke&lt;br&gt;
Recover&lt;br&gt;
Monitor&lt;br&gt;
MyZubster now has the foundation for persistent multi-device sessions, secure refresh-token rotation and automatic replay response.&lt;br&gt;
The next major objective is completing the browser migration safely through centralized renewal, CSRF protection and the removal of JavaScript-accessible authentication credentials.&lt;br&gt;
Only after those controls are validated should strict session enforcement be activated in production.&lt;br&gt;
The current implementation can be reviewed in the Draft pull request:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/danieldirimini-myzubster/myzubster/pull/10&quot;&gt;https://github.com/danieldirimini-myzubster/myzubster/pull/10&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28847/how-built-revocable-sessions-and-refresh-token-replay-protection-for-myzubster-metaverse</guid>
<pubDate>Mon, 28 Sep 2026 01:35:52 +0000</pubDate>
</item>
<item>
<title>Spring Boot Authentication Boilerplate: From JWT Basics to Production-Ready Auth</title>
<link>https://coderlegion.com/27856/spring-boot-authentication-boilerplate-from-jwt-basics-to-production-ready-auth</link>
<description>&lt;p&gt;Starting a new Spring Boot project usually feels great.&lt;/p&gt;
&lt;p&gt;You create the project, add a few dependencies, write your first controller...&lt;/p&gt;
&lt;p&gt;Then authentication enters the chat.&lt;/p&gt;
&lt;p&gt;Suddenly you're configuring:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Spring Security&lt;/li&gt;
&lt;li&gt;JWT access tokens&lt;/li&gt;
&lt;li&gt;refresh tokens&lt;/li&gt;
&lt;li&gt;roles and permissions&lt;/li&gt;
&lt;li&gt;password hashing&lt;/li&gt;
&lt;li&gt;exception handling&lt;/li&gt;
&lt;li&gt;database migrations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Three hours later, your original feature still has exactly &lt;strong&gt;zero lines of code&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Congratulations. You built authentication again.&lt;/p&gt;
&lt;h3 id=&quot;the-auth-code-isn-t-the-interesting-part&quot;&gt;The auth code isn't the interesting part&lt;/h3&gt;
&lt;p&gt;Most applications need roughly the same authentication foundation.&lt;/p&gt;
&lt;p&gt;Register a user.&lt;/p&gt;
&lt;p&gt;Login.&lt;/p&gt;
&lt;p&gt;Return an access token.&lt;/p&gt;
&lt;p&gt;Refresh it safely.&lt;/p&gt;
&lt;p&gt;Protect endpoints.&lt;/p&gt;
&lt;p&gt;Check roles.&lt;/p&gt;
&lt;p&gt;Logout.&lt;/p&gt;
&lt;p&gt;Maybe add passkeys, OAuth, account recovery, or multiple sessions later.&lt;/p&gt;
&lt;p&gt;None of this is particularly unique to your SaaS, internal tool, API, or side project.&lt;/p&gt;
&lt;p&gt;But we keep rebuilding it.&lt;/p&gt;
&lt;p&gt;I was doing the same thing with my Spring Boot projects, so I eventually turned that repeated setup into &lt;strong&gt;AuthKit&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;authkit-lite&quot;&gt;AuthKit Lite&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;AuthKit Lite&lt;/strong&gt; is the free and open-source version.&lt;/p&gt;
&lt;p&gt;It gives you a working Spring Boot authentication foundation with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JWT access tokens&lt;/li&gt;
&lt;li&gt;rotating refresh tokens&lt;/li&gt;
&lt;li&gt;Spring Security&lt;/li&gt;
&lt;li&gt;role-based authorization&lt;/li&gt;
&lt;li&gt;BCrypt passwords&lt;/li&gt;
&lt;li&gt;optional WebAuthn passkeys&lt;/li&gt;
&lt;li&gt;Flyway migrations&lt;/li&gt;
&lt;li&gt;H2 for instant local testing&lt;/li&gt;
&lt;li&gt;MySQL support&lt;/li&gt;
&lt;li&gt;integration tests&lt;/li&gt;
&lt;li&gt;a browser-based API testing page&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The goal is simple:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Clone → run → test the APIs → start building your application.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/buildbasekit/AuthKit-Lite.git
cd AuthKit-Lite
./mvnw spring-boot:run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then open:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;http://localhost:8080/api-test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No Docker ceremony.&lt;/p&gt;
&lt;p&gt;No spending your evening creating JWT filters for the 17th time.&lt;/p&gt;
&lt;p&gt;No pretending you'll &quot;clean up the auth code later.&quot;&lt;/p&gt;
&lt;p&gt;You can find AuthKit Lite here:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/authkit-lite/&quot;&gt;https://buildbasekit.com/boilerplates/authkit-lite/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;when-lite-isn-t-enough&quot;&gt;When Lite isn't enough&lt;/h3&gt;
&lt;p&gt;Basic authentication is enough for many projects.&lt;/p&gt;
&lt;p&gt;But production applications often grow into things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;email verification&lt;/li&gt;
&lt;li&gt;password recovery&lt;/li&gt;
&lt;li&gt;multiple user sessions&lt;/li&gt;
&lt;li&gt;session revocation&lt;/li&gt;
&lt;li&gt;refresh-token replay protection&lt;/li&gt;
&lt;li&gt;OAuth2 / OIDC&lt;/li&gt;
&lt;li&gt;account linking&lt;/li&gt;
&lt;li&gt;magic links&lt;/li&gt;
&lt;li&gt;passkeys&lt;/li&gt;
&lt;li&gt;account lifecycle handling&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is where &lt;strong&gt;AuthKit Pro&lt;/strong&gt; comes in.&lt;/p&gt;
&lt;p&gt;It takes the same idea further: provide the authentication foundation while keeping the application-specific business logic in your application.&lt;/p&gt;
&lt;p&gt;The important part for me was avoiding a giant framework that tries to own the entire application.&lt;/p&gt;
&lt;p&gt;Auth should be infrastructure.&lt;/p&gt;
&lt;p&gt;Not your new hobby.&lt;/p&gt;
&lt;p&gt;AuthKit Pro is here:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/authkit-pro&quot;&gt;https://buildbasekit.com/boilerplates/authkit-pro&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;lite-or-pro-&quot;&gt;Lite or Pro?&lt;/h3&gt;
&lt;p&gt;The easiest way to think about it:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AuthKit Lite&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Use it when you need a clean Spring Boot authentication starting point or want to understand and extend the implementation yourself.&lt;/p&gt;
&lt;p&gt;It's free and open source.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AuthKit Pro&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Use it when the project needs more complete authentication flows and production-focused session and account lifecycle features.&lt;/p&gt;
&lt;p&gt;Both are source-code foundations.&lt;/p&gt;
&lt;p&gt;You still own the application.&lt;/p&gt;
&lt;p&gt;You can change the architecture.&lt;/p&gt;
&lt;p&gt;You can remove features.&lt;/p&gt;
&lt;p&gt;You can add your own authorization rules.&lt;/p&gt;
&lt;p&gt;And your business logic doesn't have to live inside somebody else's authentication service.&lt;/p&gt;
&lt;h3 id=&quot;why-a-boilerplate-&quot;&gt;Why a boilerplate?&lt;/h3&gt;
&lt;p&gt;I'm not trying to eliminate authentication decisions.&lt;/p&gt;
&lt;p&gt;You still need to decide things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;token lifetimes&lt;/li&gt;
&lt;li&gt;authorization rules&lt;/li&gt;
&lt;li&gt;production secrets&lt;/li&gt;
&lt;li&gt;rate limiting&lt;/li&gt;
&lt;li&gt;monitoring&lt;/li&gt;
&lt;li&gt;application-specific permissions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A boilerplate just means you don't have to start those decisions from an empty &lt;code&gt;SecurityConfig.java&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That's the part I wanted to solve.&lt;/p&gt;
&lt;p&gt;Because there are much more interesting bugs waiting for us in the actual product.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;AuthKit Lite — Free &amp;amp; Open Source&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/authkit-lite/&quot;&gt;https://buildbasekit.com/boilerplates/authkit-lite/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AuthKit Pro — Production-Focused&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://buildbasekit.com/boilerplates/authkit-pro&quot;&gt;https://buildbasekit.com/boilerplates/authkit-pro&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Built with Spring Boot and Spring Security.&lt;/p&gt;
&lt;p&gt;If you're building Spring Boot APIs, I'd also be interested to know:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Do you rebuild authentication for every project, maintain your own starter, or use an external identity provider?&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27856/spring-boot-authentication-boilerplate-from-jwt-basics-to-production-ready-auth</guid>
<pubDate>Sun, 20 Sep 2026 16:50:00 +0000</pubDate>
</item>
<item>
<title>How to Authenticate Users with Solana Wallets in NestJS</title>
<link>https://coderlegion.com/27699/how-to-authenticate-users-with-solana-wallets-in-nestjs</link>
<description>&lt;p&gt;Let's talk about wallet based auth in NestJS. What this is, is basically an auth system where the user is authenticated and authorized based on their web3 wallet. We'll be doing this with a Solana wallet. The process is quite straightforward:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;User requests a nonce (a single-use random number that prevents replay attacks)&lt;/li&gt;
&lt;li&gt;Backend sends the nonce&lt;/li&gt;
&lt;li&gt;User signs the nonce with their wallet address and returns the signed nonce back to the backend&lt;/li&gt;
&lt;li&gt;Backend verifies that the nonce was signed by the right address&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;From this point on it's your regular auth flow and you can hook this up to any existing JWT or session auth system you've implemented.&lt;/p&gt;
&lt;p&gt;One thing to note is that a frontend is required for this or at least some way for the client to sign nonces with their web3 wallet. We'll be using &lt;code&gt;@solana/kit&lt;/code&gt; SDK for this as it's the modern implementation of the &lt;code&gt;@solana/web3.js&lt;/code&gt; SDK and recommended for production use by Anza.&lt;/p&gt;
&lt;h3 id=&quot;backend&quot;&gt;Backend&lt;/h3&gt;
&lt;p&gt;The backend's job is to generate unique challenges (nonces), verify that signatures were created by the claimed wallet address, and issue JWT tokens on successful authentication. &lt;/p&gt;
&lt;p&gt;We'll need to install &lt;code&gt;@solana/kit&lt;/code&gt; as well as &lt;code&gt;redis&lt;/code&gt; since we will be using that to store our nonces. Redis is ideal here because it provides fast in-memory storage with built-in TTL (time-to-live) expiration, automatically cleaning up old nonces without manual intervention.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install @solana/kit redis
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;redis-service&quot;&gt;Redis Service&lt;/h4&gt;
&lt;p&gt;Let's set up our redis client and service.&lt;/p&gt;
&lt;p&gt;In NestJS, when you're working with non-class instances like Redis clients, you can't use the standard class-based dependency injection. Instead, you use string tokens as injection identifiers. The Redis client is created by a factory function (not instantiated via new RedisService()), so TypeScript can't use the class itself as a token. String constants provide a unique identifier that NestJS can use to register and retrieve the instance.&lt;/p&gt;
&lt;p&gt;This is why we define &lt;code&gt;REDIS_CLIENT&lt;/code&gt; as a constant string token that we'll use to register and inject our Redis client throughout the application.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;//redis.constants.ts
export const REDIS_CLIENT = 'REDIS_CLIENT';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We need two specific methods here. The &lt;code&gt;setNonce&lt;/code&gt; method stores the nonce with a 5-minute expiration using Redis's &lt;code&gt;EX&lt;/code&gt; option, which automatically cleans up old nonces and prevents replay attacks.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;getAndDeleteNonce&lt;/code&gt; method atomically retrieves and deletes the nonce in one operation, ensuring each nonce can only be used once for verification. This pattern prevents an attacker from reusing a previously signed nonce.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;//redis.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { REDIS_CLIENT } from './redis.constants';
import type { RedisClientType } from 'redis';

@Injectable()
export class RedisService {
  constructor(@Inject(REDIS_CLIENT) private readonly redis: RedisClientType) {}

  // Store nonce with 5-minute expiration
  async setNonce(address: string, nonce: string): Promise&amp;lt;void&amp;gt; {
    await this.redis.set(`nonce:${address}`, nonce, { EX: 300 });
  }

  // Retrieve and immediately delete nonce to prevent replay attacks
  async getAndDeleteNonce(address: string): Promise&amp;lt;string | null&amp;gt; {
    const nonce = await this.redis.get(`nonce:${address}`);
    
    if (nonce) {
      await this.redis.del(`nonce:${address}`);
    }
    
    return nonce;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We're using NestJS's &lt;code&gt;ConfigService&lt;/code&gt; to manage environment variables in a type-safe way. This is better than directly accessing &lt;code&gt;process.env&lt;/code&gt; because it centralizes configuration, provides validation, and makes testing easier. You can validate required variables at startup (fail fast), type-check them, and mock them in tests without polluting the global process object.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;@Global()&lt;/code&gt; decorator makes this module available throughout your application without needing to import it into every module. Make sure to set up &lt;code&gt;ConfigModule&lt;/code&gt; in your &lt;code&gt;AppModule&lt;/code&gt; like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
// ... other imports

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true, // Makes ConfigModule available globally
    }),
    // ... other modules
  ],
  // ... controllers and providers
})
export class AppModule {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can set up our Redis module:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;//redis.module.ts
import { Global, Module } from '@nestjs/common';
import { RedisService } from './redis.service';
import { REDIS_CLIENT } from './redis.constants';
import { ConfigService } from '@nestjs/config';
import { createClient } from 'redis';

@Global()
@Module({
  providers: [
    {
      provide: REDIS_CLIENT,
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) =&amp;gt; {
        const client = createClient({
          url: configService.get&amp;lt;string&amp;gt;('REDIS_URL'),
        });
        
        client.on('error', (err) =&amp;gt; console.error('Redis client error', err));
        
        await client.connect();
        
        return client;
      },
    },
    RedisService,
  ],
  exports: [REDIS_CLIENT, RedisService],
})
export class RedisModule {}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;auth-service&quot;&gt;Auth Service&lt;/h4&gt;
&lt;p&gt;With Redis configured to handle nonce storage, we can build the authentication logic. The auth service will generate nonces, create SIWS-compliant messages, verify signatures, and issue JWT tokens. Let's implement our service logic.&lt;/p&gt;
&lt;p&gt;We need to provide a way for the frontend to request a nonce. We also need to use our redis methods to store the nonce we provide to be used during verification later.&lt;/p&gt;
&lt;p&gt;We use &lt;code&gt;randomBytes&lt;/code&gt; from Node's crypto module to generate cryptographically secure random values. The 16 bytes of random data are then converted to base64url encoding, which is URL-safe and produces a string without padding characters. This encoding ensures the nonce can be safely included in URLs and JSON without escaping issues, while 16 bytes provides sufficient entropy (2^128 possible values) to prevent collision attacks. This gives us a unique, unpredictable nonce for each authentication attempt.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { randomBytes } from 'crypto';

generateNonce(): string {
  return randomBytes(16).toString('base64url');
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The message format follows the Sign-In with Solana (SIWS) standard, which is analogous to Sign-In with Ethereum (SIWE). This standardized format includes the domain requesting authentication, the user's wallet address, a human-readable message, the unique nonce, and a timestamp. This structure ensures the user knows exactly what they're signing and prevents phishing attacks where a malicious site might try to get users to sign messages intended for other domains.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async getChallenge(address: string) {
  const nonce = this.generateNonce();
  
  await this.redisService.setNonce(address, nonce);
  
  // Standard SIWS message format for wallet authentication
  const message = `titan-app.com wants you to sign in with your Solana account:\n${address}\n\nSign this message to authenticate.\n\nNonce: ${nonce}\nTimestamp: ${Date.now()}`;
  
  return { message, nonce };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The signature verification method is the heart of the authentication system. It retrieves the stored nonce, reconstructs the exact message that was signed, and uses Solana's cryptographic primitives to verify that the signature was created by the private key corresponding to the claimed wallet address. If any part of this fails, authentication is rejected.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { getAddressFromPublicKey, getBase58Decoder, getBase58Encoder } from '@solana/kit';

async verifySolanaSignature(data: {
  address: string;
  signatureBase58: Uint8Array;
  message: Uint8Array;
  nonce: string;
}): Promise&amp;lt;boolean&amp;gt; {
  const { address, signatureBase58, message, nonce } = data;

  // Retrieve and delete the nonce to prevent replay attacks
  const storedNonce = await this.redisService.getAndDeleteNonce(address);

  if (!storedNonce || storedNonce !== nonce) {
    return false;
  }

  try {
    // Decode the signature and message from base58
    const signature = getBase58Decoder().decode(signatureBase58);
    const messageBytes = getBase58Decoder().decode(message);
    
    // Verify the signature matches the address
    const publicKey = getAddressFromPublicKey(
      getBase58Decoder().decode(address)
    );
    
    // Use Solana's signature verification
    const isValid = await verifySignature(publicKey, messageBytes, signature);
    
    return isValid;
  } catch (error) {
    console.error('Signature verification error:', error);
    return false;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;validateUserWithWallet&lt;/code&gt; method either finds an existing user by wallet address or creates a new one. This is where you'd integrate with your user management system. In this example, we're using Prisma to handle database operations, but you could adapt this to any ORM or database layer.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async validateUserWithWallet(address: string): Promise&amp;lt;UserEntity | null&amp;gt; {
  // Find existing user by wallet address
  let user = await this.prisma.user.findUnique({
    where: { walletAddress: address },
  });

  // If no user exists, create one
  if (!user) {
    user = await this.prisma.user.create({
      data: {
        walletAddress: address,
      },
    });
  }

  return user;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The login function creates JWT tokens after successful authentication. We build a payload containing the user's ID and device information, then conditionally add email and wallet address if they exist. The function returns both an access token for immediate use and a refresh token for obtaining new access tokens when the current one expires. The refresh token is handled by a separate service that manages longer-lived tokens with different expiration rules.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async login(
  user: UserEntity,
  deviceInfo: string,
  walletAddress?: string,
): Promise&amp;lt;{
  access_token: string;
  refresh_token: string;
}&amp;gt; {
  const payload = {
    sub: user.id,
    deviceInfo,
  };

  if (user.email) {
    payload['email'] = user.email;
  }

  if (walletAddress) {
    payload['walletAddress'] = walletAddress;
  }

  const refresh_token = await this.jwtRefreshService.create(payload);
  
  return {
    access_token: this.jwtService.sign(payload),
    refresh_token,
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;wallet-auth-guard&quot;&gt;Wallet Auth Guard&lt;/h4&gt;
&lt;p&gt;Now we need a way to protect our login endpoint. NestJS guards intercept incoming requests before they reach the controller, making them perfect for authentication logic. &lt;/p&gt;
&lt;p&gt;The guard is responsible for intercepting requests to protected endpoints and verifying that the wallet signature is valid before allowing the request to proceed. It extracts the signed data from the request body, verifies the signature using our auth service, finds or creates the user, and attaches both the user entity and wallet address to the request object for use in the controller.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// guards/wallet-auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { AuthService } from '../auth.service';
import { UnauthorizedException } from '@nestjs/common';

@Injectable()
export class LocalWalletAuthGuard implements CanActivate {
  constructor(private readonly authService: AuthService) {}

  async canActivate(context: ExecutionContext): Promise&amp;lt;boolean&amp;gt; {
    const request = context.switchToHttp().getRequest();
    const { address, signatureBase58, message, nonce } = request.body;

    // Verify signature first
    const isValid = await this.authService.verifySolanaSignature({
      address,
      signatureBase58,
      message,
      nonce,
    });

    if (!isValid) {
      throw new UnauthorizedException('Invalid signature');
    }

    // Find or create user
    const user = await this.authService.validateUserWithWallet(address);

    if (!user) {
      throw new UnauthorizedException('Could not validate user');
    }

    // Attach user and address to request for use in controller
    request.user = user;
    request.address = address;

    return true;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;auth-controller&quot;&gt;Auth Controller&lt;/h4&gt;
&lt;p&gt;With our guard handling verification, the controller becomes simple. It just needs to provide nonces to unauthenticated users and issue tokens after the guard confirms valid signatures.&lt;/p&gt;
&lt;p&gt;In the controller, we have two endpoints. The &lt;code&gt;loginWithWallet&lt;/code&gt; endpoint uses our &lt;code&gt;LocalWalletAuthGuard&lt;/code&gt; which verifies the signature and attaches both the user entity and wallet address to the request object. By the time we reach this controller method, we know the authentication is valid. We extract device information from the user-agent header, call the login service, set the refresh token as an httpOnly cookie for security, and return the access token. The &lt;code&gt;getChallenge&lt;/code&gt; endpoint is marked with &lt;code&gt;@Public()&lt;/code&gt; decorator since users need to request a nonce before they can authenticate. It simply returns the SIWS message and nonce for the given wallet address.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;@Post('login-with-wallet')
@UseGuards(LocalWalletAuthGuard)
async loginWithWallet(
  @Request() req: RequestType &amp;amp; { user: UserEntity; address: string },
  @Response({ passthrough: true }) res: ResponseType,
) {
  const deviceInfo = req.headers['user-agent'] || 'Unknown Device';

  const { access_token, refresh_token } = await this.authService.login(
    req.user,
    deviceInfo,
    req.address,
  );

  // Set refresh token as httpOnly cookie for security
  res.cookie('titan_refresh_token', refresh_token, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
  });

  return { access_token };
}

@Public()
@Get('nonce/:address')
@ApiOperation({ summary: 'Request nonce for wallet-based authentication' })
@ApiResponse({
  description: 'Nonce and SIWS message sent successfully.',
  status: 200,
  example: {
    message:
      'titan-app.com wants you to sign in with your Solana account:\n${address}\n\nSign this message to authenticate.\n\nNonce: ${nonce}\nTimestamp: ${Date.now()}',
    nonce: '1234xlrhex',
  },
})
async getChallenge(@Param('address') address: string) {
  return await this.authService.getChallenge(address);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;backend-summary&quot;&gt;Backend Summary&lt;/h4&gt;
&lt;p&gt;That covers the backend implementation. We've set up a Redis-backed nonce system that generates unique challenges for each authentication attempt, an auth service that creates SIWS-compliant messages and manages JWT token generation, a guard that verifies signatures using Solana's cryptographic primitives, and controller endpoints that handle the nonce request and login flow. The signature verification ensures that only the holder of the wallet's private key can authenticate, while the nonce system prevents replay attacks.&lt;/p&gt;
&lt;h3 id=&quot;frontend&quot;&gt;Frontend&lt;/h3&gt;
&lt;p&gt;The backend is now ready to issue nonces and verify signatures. The frontend's job is to request these nonces, get the user to sign them with their wallet's private key (which never leaves their browser), and send the signed message back for verification. Now let's implement the frontend of our auth system.&lt;/p&gt;
&lt;p&gt;Now let's implement the frontend of our auth system. The user needs to sign our message with their private key. It is important this private key stays secure and never leaves the frontend. A tool that facilitates this is a wallet. Since we're using Solana we'll be using the Phantom wallet.&lt;/p&gt;
&lt;p&gt;The setup steps are straightforward. First, install Phantom as a browser extension. Once installed, create a new wallet or import an existing one. Then navigate to Settings, go to Developer Settings, and enable Testnet mode. In Testnet mode, select Solana Devnet. You could use Localnet if you have a test validator running locally or Testnet for a shared testing environment, but Devnet provides a good balance of accessibility and realistic network conditions.&lt;/p&gt;
&lt;p&gt;Once all this is done we can start hacking. We'll spin up a small React frontend. This is just for speed and general accessibility. You can implement this in any frontend stack as long as you understand the core steps. I used Vite to set up my React project with TypeScript and Tailwind. We should also install the &lt;code&gt;@solana/kit&lt;/code&gt; package, which automatically includes &lt;code&gt;@solana/react&lt;/code&gt; and other necessary dependencies, along with &lt;code&gt;@wallet-standard/react-core&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install @solana/kit @wallet-standard/react-core
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After we're done with project setup, the first thing we want to do is define our Solana client instance. This will be used to communicate with the Solana chain via its RPC endpoints.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// lib/solana-client.ts
import type {
  Rpc,
  RpcSubscriptions,
  SolanaRpcApi,
  SolanaRpcSubscriptionsApi,
} from &quot;@solana/kit&quot;;

export type Client = {
  rpc: Rpc&amp;lt;SolanaRpcApi&amp;gt;;
  rpcSubscriptions: RpcSubscriptions&amp;lt;SolanaRpcSubscriptionsApi&amp;gt;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// lib/helpers.ts
import { createSolanaRpc, createSolanaRpcSubscriptions } from &quot;@solana/kit&quot;;
import type { Client } from &quot;./solana-client&quot;;

let client: Client | undefined;

export function createClient(): Client {
  if (!client) {
    client = {
      rpc: createSolanaRpc(`${import.meta.env.SOLANA_RPC_URL}`),
      rpcSubscriptions: createSolanaRpcSubscriptions(
        `${import.meta.env.SOLANA_RPC_SUBSCRIPTIONS_URL}`
      ),
    };
  }
  
  return client;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make sure your &lt;code&gt;.env&lt;/code&gt; file contains the Devnet URLs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-env&quot;&gt;SOLANA_RPC_URL=https://api.devnet.solana.com
SOLANA_RPC_SUBSCRIPTIONS_URL=wss://api.devnet.solana.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the Solana client configured, we need functions to communicate with our backend. In our &lt;code&gt;features/auth/api&lt;/code&gt; folder, we'll create two API calls: one to request a nonce and another to submit the signed message. Let's define them both.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// features/auth/types
export type RequestNonceOutput = {
  message: string;
  nonce: string;
};

export type VerifyNonceInput = {
  address: string;
  signatureBase58: Uint8Array&amp;lt;ArrayBufferLike&amp;gt;;
  message: Uint8Array&amp;lt;ArrayBufferLike&amp;gt;;
  nonce: string;
};

export type LoginResponse = {
  access_token: string;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// features/auth/api/requestNonce.ts
import type { RequestNonceOutput } from &quot;../types&quot;;

export async function requestNonce(
  address: string
): Promise&amp;lt;RequestNonceOutput&amp;gt; {
  const response = await fetch(
    `${import.meta.env.VITE_API_URL}/auth/nonce/${address}`
  );

  if (!response.ok) {
    throw new Error(`Failed to request nonce: ${response.statusText}`);
  }

  const data = await response.json();
  
  // Validate response structure
  if (!data.message || !data.nonce) {
    throw new Error('Invalid response format from nonce endpoint');
  }
  
  return data as RequestNonceOutput;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// features/auth/api/loginWithWallet.ts
import type { VerifyNonceInput, LoginResponse } from &quot;../types&quot;;

export async function loginWithWallet(
  signedData: VerifyNonceInput
): Promise&amp;lt;LoginResponse&amp;gt; {
  const response = await fetch(
    `${import.meta.env.VITE_API_URL}/auth/login-with-wallet`,
    {
      method: &quot;POST&quot;,
      headers: {
        &quot;Content-Type&quot;: &quot;application/json&quot;,
      },
      body: JSON.stringify(signedData),
    }
  );

  if (!response.ok) {
    const errorData = await response.json().catch(() =&amp;gt; ({}));
    throw new Error(
      errorData.message || `Login failed: ${response.statusText}`
    );
  }

  const data = await response.json();
  
  // Validate response contains access token
  if (!data.access_token) {
    throw new Error('Invalid response format from login endpoint');
  }
  
  return data as LoginResponse;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These are going to serve as our connection to the backend we made previously. &lt;/p&gt;
&lt;p&gt;Before we build the UI components, we need to manage wallet connection state across the app. React Context provides a clean way to share the selected wallet without prop drilling.&lt;/p&gt;
&lt;p&gt;As this is only an auth demonstration it's not going to be anything fancy, just enough to implement the authentication flow and some state to show we're logged in.&lt;/p&gt;
&lt;p&gt;First off, our wallet account context. You can use some other way to manage context in your application. Here we'll be using the React Context API.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// src/context/SelectedWalletAccountContext.tsx
import { createContext } from &quot;react&quot;;
import type { UiWallet, UiWalletAccount } from &quot;@wallet-standard/react-core&quot;;

export type SelectedWalletAccountState = UiWalletAccount | undefined;

export const SelectedWalletAccountContext = createContext&amp;lt;
  readonly [
    selectedWalletAccount: SelectedWalletAccountState,
    setSelectedWalletAccount: React.Dispatch&amp;lt;
      React.SetStateAction&amp;lt;SelectedWalletAccountState&amp;gt;
    &amp;gt;,
    selectedWallet: UiWallet | undefined
  ]
&amp;gt;([
  undefined,
  function setSelectedWalletAccount() {
    /* empty */
  },
  undefined,
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The wallet account context provider handles persistence and synchronization of the selected wallet across sessions. When a user selects a wallet, we save a storage key composed of the wallet name and account address to localStorage. On subsequent visits, the provider attempts to restore the previously selected wallet if it's still available. The provider also handles cleanup when wallets disconnect and ensures the selected account stays synchronized with the actual connected wallets from the Wallet Standard registry.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// src/context/SelectedWalletAccountContextProvider.tsx
import { useEffect, useMemo, useState } from &quot;react&quot;;
import {
  SelectedWalletAccountContext,
  type SelectedWalletAccountState,
} from &quot;./SelectedWalletAccountContext&quot;;
import {
  getUiWalletAccountStorageKey,
  uiWalletAccountBelongsToUiWallet,
  uiWalletAccountsAreSame,
  useWallets,
  type UiWallet,
  type UiWalletAccount,
} from &quot;@wallet-standard/react-core&quot;;

const STORAGE_KEY = &quot;solana-wallet-standard-react:selected-wallet-and-address&quot;;

let wasSetterInvoked = false;

// Attempts to restore previously selected wallet from localStorage
function getSavedWalletAccountAndWallet(
  wallets: readonly UiWallet[]
):
  | { account: UiWalletAccount | undefined; wallet: UiWallet | undefined }
  | undefined {
  // Stop auto-selecting after user makes explicit choice
  if (wasSetterInvoked) {
    return;
  }
  
  const savedWalletNameAndAddress = localStorage.getItem(STORAGE_KEY);
  if (
    !savedWalletNameAndAddress ||
    typeof savedWalletNameAndAddress !== &quot;string&quot;
  ) {
    return;
  }
  
  const [savedWalletName, savedAccountAddress] =
    savedWalletNameAndAddress.split(&quot;:&quot;);
    
  if (!savedWalletName || !savedAccountAddress) {
    return;
  }
  
  // Find matching wallet and account in current connected wallets
  for (const wallet of wallets) {
    if (wallet.name === savedWalletName) {
      for (const account of wallet.accounts) {
        if (account.address === savedAccountAddress) {
          return { account, wallet };
        }
      }
    }
  }
}

export function SelectedWalletAccountContextProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  const wallets = useWallets();
  
  const [selectedWalletAccount, setSelectedWalletAccountInternal] =
    useState&amp;lt;SelectedWalletAccountState&amp;gt;(
      () =&amp;gt; getSavedWalletAccountAndWallet(wallets)?.account
    );
  
  const [selectedWallet, setSelectedWalletInternal] = useState&amp;lt;
    UiWallet | undefined
  &amp;gt;(() =&amp;gt; getSavedWalletAccountAndWallet(wallets)?.wallet);
  
  // Wrapper that saves selection to localStorage
  const setSelectedWalletAccount: React.Dispatch&amp;lt;
    React.SetStateAction&amp;lt;SelectedWalletAccountState&amp;gt;
  &amp;gt; = (setStateAction) =&amp;gt; {
    setSelectedWalletAccountInternal((prevSelectedWalletAccount) =&amp;gt; {
      wasSetterInvoked = true;
      const nextWalletAccount =
        typeof setStateAction === &quot;function&quot;
          ? setStateAction(prevSelectedWalletAccount)
          : setStateAction;
          
      const accountKey = nextWalletAccount
        ? getUiWalletAccountStorageKey(nextWalletAccount)
        : undefined;
        
      if (accountKey) {
        localStorage.setItem(STORAGE_KEY, accountKey);
      } else {
        localStorage.removeItem(STORAGE_KEY);
      }
      
      return nextWalletAccount;
    });
  };
  
  // Restore saved wallet on mount
  useEffect(() =&amp;gt; {
    const savedWalletAccount = getSavedWalletAccountAndWallet(wallets)?.account;
    const savedWallet = getSavedWalletAccountAndWallet(wallets)?.wallet;
    
    if (savedWalletAccount &amp;amp;&amp;amp; savedWallet) {
      setSelectedWalletAccountInternal(savedWalletAccount);
      setSelectedWalletInternal(savedWallet);
    }
  }, [wallets]);
  
  // Keep selected account synchronized with connected wallets
  const walletAccount = useMemo(() =&amp;gt; {
    if (selectedWalletAccount) {
      for (const uiWallet of wallets) {
        for (const uiWalletAccount of uiWallet.accounts) {
          if (uiWalletAccountsAreSame(selectedWalletAccount, uiWalletAccount)) {
            return uiWalletAccount;
          }
        }
        
        // If selected account's wallet is connected, use its first account
        if (
          uiWalletAccountBelongsToUiWallet(selectedWalletAccount, uiWallet) &amp;amp;&amp;amp;
          uiWallet.accounts[0]
        ) {
          return uiWallet.accounts[0];
        }
      }
    }
  }, [selectedWalletAccount, wallets]);
  
  const wallet = useMemo(() =&amp;gt; {
    if (selectedWallet) {
      for (const uiWallet of wallets) {
        if (uiWallet.name === selectedWallet.name) {
          return uiWallet;
        }
      }
    }
  }, [selectedWallet, wallets]);
  
  // Clear selection if wallet disconnects
  useEffect(() =&amp;gt; {
    if (selectedWalletAccount &amp;amp;&amp;amp; !walletAccount) {
      setSelectedWalletAccountInternal(undefined);
    }
  }, [selectedWalletAccount, walletAccount]);
  
  return (
    &amp;lt;SelectedWalletAccountContext.Provider
      value={useMemo(
        () =&amp;gt; [walletAccount, setSelectedWalletAccount, wallet],
        [walletAccount, wallet]
      )}
    &amp;gt;
      {children}
    &amp;lt;/SelectedWalletAccountContext.Provider&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once our context is set up we can import and use it like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// src/main.tsx
import { StrictMode } from &quot;react&quot;;
import { createRoot } from &quot;react-dom/client&quot;;
import &quot;./index.css&quot;;
import App from &quot;./App.tsx&quot;;
import { SelectedWalletAccountContextProvider } from &quot;./context/SelectedWalletAccountContextProvider.tsx&quot;;

createRoot(document.getElementById(&quot;root&quot;)!).render(
  &amp;lt;StrictMode&amp;gt;
    &amp;lt;SelectedWalletAccountContextProvider&amp;gt;
      &amp;lt;App /&amp;gt;
    &amp;lt;/SelectedWalletAccountContextProvider&amp;gt;
  &amp;lt;/StrictMode&amp;gt;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;wallet-list&quot;&gt;Wallet List&lt;/h4&gt;
&lt;p&gt;With context set up to track wallet state, we can build the UI. Users need a way to see available wallets and connect them to our app. The Wallet Standard exposes all compatible browser wallets, so we'll create a dialog that lists them. We'll start with the wallet list item.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// features/auth/components/wallet_list_item.tsx
import { Button } from &quot;@/components/ui/button&quot;
import { SelectedWalletAccountContext } from &quot;@/context/SelectedWalletAccountContext&quot;;
import { useConnect, type UiWallet } from &quot;@wallet-standard/react-core&quot;;
import { useContext } from &quot;react&quot;;

type Props = {
  wallet: UiWallet;
  setOpen: () =&amp;gt; void;
};

export const WalletListItem = ({ wallet, setOpen }: Props) =&amp;gt; {
  const [isConnecting, connect] = useConnect(wallet);
  const [_, setSelectedWalletAccount] = useContext(
    SelectedWalletAccountContext
  );
  
  const handleConnect = () =&amp;gt; {
    try {
      // Connect returns array of accounts, we select the first one
      connect().then((res) =&amp;gt; {
        setSelectedWalletAccount(res[0]);
      });
    } catch (error) {
      console.error(&quot;Failed to connect:&quot;, error);
    } finally {
      setOpen();
    }
  };
  
  if (!wallet) return;

  return (
    &amp;lt;li&amp;gt;
      &amp;lt;Button
        variant=&quot;outline&quot;
        disabled={isConnecting}
        onClick={handleConnect}
        className=&quot;flex flex-row p-4 items-center justify-start gap-3 w-full&quot;
      &amp;gt;
        &amp;lt;img alt=&quot;wallet logo&quot; src={wallet.icon} className=&quot;w-6 h-6&quot; /&amp;gt;
        &amp;lt;p className=&quot;text-lg font-semibold&quot;&amp;gt;{wallet.name}&amp;lt;/p&amp;gt;
      &amp;lt;/Button&amp;gt;
    &amp;lt;/li&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One thing to note is that while we can fetch all wallets connected to the browser, you often only want the ones that support the chain your application is based on. To do this we can create a hook to fetch and filter these wallets for us. You can extend this to work for multiple chains if need be. Extracting this logic into a custom hook is production-grade because it encapsulates the filtering logic in a reusable, testable unit. It makes your components cleaner by separating concerns, and if you need to add more complex filtering logic later or support multiple chains, you only need to modify this one hook rather than hunting through multiple components.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// features/auth/hooks/useFilteredWallets.tsx
import { useWallets } from &quot;@wallet-standard/react-core&quot;;

export function useFilteredWallets() {
  const wallets = useWallets();
  
  // Filter wallets that support Solana chains
  const solanaWallets = wallets.filter((uiWallet) =&amp;gt;
    uiWallet.chains.some((chain) =&amp;gt; chain.startsWith(&quot;solana:&quot;))
  );

  return solanaWallets;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can implement the actual wallet list to render all connected wallets that match our base chain.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// features/auth/components/wallet_list.tsx
import { Button } from &quot;@/components/ui/button&quot;;
import {
  DialogHeader,
  Dialog,
  DialogTrigger,
  DialogContent,
  DialogTitle,
  DialogDescription,
} from &quot;@/components/ui/dialog&quot;;
import { WalletListItem } from &quot;./wallet_list_item&quot;;
import { useFilteredWallets } from &quot;../hooks/useFilteredWallets&quot;;
import { useState } from &quot;react&quot;;
  
export const WalletList = () =&amp;gt; {
  const [open, setOpen] = useState(false);
  const solanaWallets = useFilteredWallets();

  return (
    &amp;lt;Dialog open={open} onOpenChange={setOpen}&amp;gt;
      &amp;lt;form&amp;gt;
        &amp;lt;DialogTrigger asChild&amp;gt;
          &amp;lt;Button variant=&quot;outline&quot;&amp;gt;View Wallets&amp;lt;/Button&amp;gt;
        &amp;lt;/DialogTrigger&amp;gt;

        &amp;lt;DialogContent className=&quot;sm:max-w-106.25&quot;&amp;gt;
          &amp;lt;DialogHeader&amp;gt;
            &amp;lt;DialogTitle&amp;gt;Select Wallet&amp;lt;/DialogTitle&amp;gt;
            &amp;lt;DialogDescription&amp;gt;
              Select the wallet you want to connect to Titan.
            &amp;lt;/DialogDescription&amp;gt;
          &amp;lt;/DialogHeader&amp;gt;

          &amp;lt;ul className=&quot;flex flex-col gap-2&quot;&amp;gt;
            {solanaWallets.map((wallet, idx) =&amp;gt; (
              &amp;lt;WalletListItem
                key={idx}
                wallet={wallet}
                setOpen={() =&amp;gt; setOpen(false)}
              /&amp;gt;
            ))}
          &amp;lt;/ul&amp;gt;
        &amp;lt;/DialogContent&amp;gt;
      &amp;lt;/form&amp;gt;
    &amp;lt;/Dialog&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;sign-in-button&quot;&gt;Sign-In Button&lt;/h4&gt;
&lt;p&gt;Once a wallet is connected, we need to authenticate the user. This is where the full flow comes together: request nonce, sign it, send to backend. This is just going to be a simple button that triggers the auth flow.&lt;/p&gt;
&lt;p&gt;The first thing that will happen on click is that we request a nonce from our backend. Immediately after we get the nonce we use the &lt;code&gt;useSignIn&lt;/code&gt; hook to sign our message. We then extract the results we need and send them in our login request.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// features/auth/components/auth_buttons.tsx
import { useSignIn } from &quot;@solana/react&quot;;
import { Button } from &quot;@/components/ui/button&quot;;
import { requestNonce } from &quot;../api/requestNonce&quot;;
import { loginWithWallet } from &quot;../api/loginWithWallet&quot;;
import type { UiWallet } from &quot;@wallet-standard/react-core&quot;;

type SignInProps = {
  address: string;
  wallet: UiWallet;
};

export const SignInButton = ({ wallet, address }: SignInProps) =&amp;gt; {
  const signIn = useSignIn(wallet);
  
  if (!wallet) return;

  const handleSignIn = async () =&amp;gt; {
    try {
      // Request nonce from backend
      const res = await requestNonce(address);

      // Sign the message with wallet's private key
      const { account, signedMessage, signature } = await signIn({
        nonce: res.nonce,
        domain: import.meta.env.VITE_APP_DOMAIN || &quot;localhost:5173&quot;,
      });

      // Send signed data to backend for verification
      const loginResponse = await loginWithWallet({
        address: account.address,
        signatureBase58: signature,
        message: signedMessage,
        nonce: res.nonce,
      });
      
      // Store access token (you might want to use a state management solution)
      localStorage.setItem('access_token', loginResponse.access_token);
      
      console.log('Successfully authenticated!');
    } catch (error) {
      console.error(&quot;Failed to sign in:&quot;, error);
      // You might want to show this error to the user via a toast or alert
    }
  };
  
  return &amp;lt;Button onClick={handleSignIn}&amp;gt;Sign In&amp;lt;/Button&amp;gt;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Our finished App.tsx looks like this.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// App.tsx
import { useContext } from &quot;react&quot;;
import { WalletList } from &quot;./features/auth/components/wallet_list&quot;;
import { SelectedWalletAccountContext } from &quot;./context/SelectedWalletAccountContext&quot;;
import { SignInButton } from &quot;./features/auth/components/auth_buttons&quot;;

function App() {
  const [selectedWalletAccount, _, selectedWallet] = useContext(
    SelectedWalletAccountContext
  );

  return (
    &amp;lt;div className=&quot;flex gap-3 min-h-svh flex-col items-center justify-center&quot;&amp;gt;
      &amp;lt;div&amp;gt;
        &amp;lt;p className=&quot;text-lg font-semibold&quot;&amp;gt;
          Active account: {selectedWalletAccount?.address || 'None'}
        &amp;lt;/p&amp;gt;
      &amp;lt;/div&amp;gt;

      &amp;lt;WalletList /&amp;gt;

      {selectedWalletAccount &amp;amp;&amp;amp; selectedWallet &amp;amp;&amp;amp; (
        &amp;lt;SignInButton
          address={selectedWalletAccount.address}
          wallet={selectedWallet}
        /&amp;gt;
      )}
    &amp;lt;/div&amp;gt;
  );
}

export default App;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;frontend-summary&quot;&gt;Frontend Summary&lt;/h4&gt;
&lt;p&gt;That wraps up the frontend implementation. We've built a wallet connection system that integrates with the Wallet Standard to support any compatible Solana wallet, not just Phantom. The context provider manages wallet selection persistence across sessions, while the sign-in flow requests a nonce from the backend, prompts the user to sign it with their wallet, and sends the signed message back for verification. The private key never leaves the user's wallet, ensuring security throughout the process.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;We've implemented a complete wallet-based authentication system for Solana. The backend generates unique nonces with Redis-backed storage, creates SIWS-compliant challenge messages, verifies signatures using Solana's cryptographic primitives, and manages JWT token generation after successful verification. The frontend leverages the Wallet Standard to support multiple wallet providers and handles the signing flow securely within the user's wallet extension.&lt;/p&gt;
&lt;p&gt;This auth pattern is fundamentally different from traditional username/password authentication because the user proves ownership of their wallet through cryptographic signatures rather than shared secrets. The nonce prevents replay attacks, the standardized message format prevents phishing, and the time-limited Redis storage ensures old challenges can't be reused. You can extend this system by adding additional user data to the JWT payload, implementing token refresh logic, or supporting multiple blockchain networks with minimal modifications to the core flow.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/27699/how-to-authenticate-users-with-solana-wallets-in-nestjs</guid>
<pubDate>Sat, 19 Sep 2026 07:40:14 +0000</pubDate>
</item>
<item>
<title>What Is Authentication? A Beginner's Guide (2026)</title>
<link>https://coderlegion.com/25969/what-is-authentication-a-beginners-guide-2026</link>
<description>&lt;p&gt;Every time you type your email and password into a login form, something is quietly deciding whether you actually get in. That decision — proving you are who you say you are — is called &lt;strong&gt;authentication&lt;/strong&gt;, and it's one of those concepts every developer touches constantly but rarely stops to explain from scratch.&lt;/p&gt;
&lt;p&gt;If you've ever wondered what actually happens between &quot;click Sign In&quot; and &quot;welcome back,&quot; or mixed up &lt;em&gt;authentication&lt;/em&gt; with &lt;em&gt;authorization&lt;/em&gt; in an interview, this guide is for you. By the end, you'll know exactly what authentication is, how the major methods work under the hood, and how to build a basic authentication flow yourself.&lt;/p&gt;
&lt;h3 id=&quot;what-is-authentication-quick-answer-&quot;&gt;What Is Authentication? (Quick Answer)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Authentication is the process of verifying that someone is who they claim to be.&lt;/strong&gt; It answers one question: &lt;em&gt;&quot;Who are you?&quot;&lt;/em&gt; This usually happens by checking a password, a code from your phone, a fingerprint, or a token — something only the real user should have or know.&lt;/p&gt;
&lt;p&gt;Authentication is different from &lt;strong&gt;authorization&lt;/strong&gt;, which answers a separate question: &lt;em&gt;&quot;What are you allowed to do?&quot;&lt;/em&gt; A logged-in user is authenticated. Whether that user can delete another person's account is authorization. Mixing these two up is one of the most common beginner mistakes — keep them mentally separate from the start.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Simple way to remember it:&lt;/strong&gt; Authentication = ID check at the door. Authorization = the guest list that decides which rooms you can enter once you're inside.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;why-authentication-matters&quot;&gt;Why Authentication Matters&lt;/h3&gt;
&lt;p&gt;Without proper authentication and authorization, an app that stores personal data could expose private information to unauthorized users. Authentication provides the identity context that many authorization, personalization, and auditing systems rely on.&lt;/p&gt;
&lt;p&gt;It matters for a few concrete reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Privacy&lt;/strong&gt; — only the right person sees their own data&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security&lt;/strong&gt; — prevents account takeover and unauthorized access&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Personalization&lt;/strong&gt; — apps can remember who you are across visits&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accountability&lt;/strong&gt; — actions can be traced back to a real identity (important for admin tools, financial apps, audit logs)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;authentication-vs-authorization-the-difference-that-trips-everyone-up&quot;&gt;Authentication vs. Authorization: The Difference That Trips Everyone Up&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Authentication &lt;/th&gt;&lt;th&gt;Authorization&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Question it answers &lt;/td&gt;&lt;td&gt;Who are you? &lt;/td&gt;&lt;td&gt;What can you do?&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Happens &lt;/td&gt;&lt;td&gt;First &lt;/td&gt;&lt;td&gt;After authentication&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Example &lt;/td&gt;&lt;td&gt;Logging in with a password &lt;/td&gt;&lt;td&gt;Only admins can access &lt;code&gt;/admin&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Failure result &lt;/td&gt;&lt;td&gt;401 Unauthorized &lt;/td&gt;&lt;td&gt;403 Forbidden&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Why the HTTP status codes are confusingly named:&lt;/strong&gt; &lt;code&gt;401 Unauthorized&lt;/code&gt; actually means &quot;you're not authenticated&quot; (log in first), while &lt;code&gt;403 Forbidden&lt;/code&gt; means &quot;you're authenticated, but not allowed to do this.&quot; Yes, &lt;code&gt;401&lt;/code&gt; should really be called &quot;Unauthenticated&quot; — it's a long-standing naming quirk in the HTTP spec that trips up almost every developer at least once.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/role-based-authentication-nextjs-16&quot;&gt;Related: Role-Based Access Control (RBAC) in Next.js 16&lt;/a&gt; goes deep on the authorization half of this once you're ready for it.&lt;/p&gt;
&lt;h3 id=&quot;how-authentication-works-step-by-step-&quot;&gt;How Authentication Works (Step by Step)&lt;/h3&gt;
&lt;p&gt;At a high level, almost every authentication system follows the same basic loop:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The user provides credentials&lt;/strong&gt; — a password, a fingerprint, a one-time code, or an existing account from another provider (like signing in with Google)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The server verifies those credentials&lt;/strong&gt; — checking a hashed password, validating a signed token, or confirming a biometric match&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The server issues proof of identity&lt;/strong&gt; — usually a session or a token, stored in a cookie or sent back to the client&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;That proof is sent with future requests&lt;/strong&gt; — so the user doesn't have to log in again on every page&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;User submits credentials
        ↓
Server verifies credentials
        ↓
Server issues session/token
        ↓
Client stores it (cookie)
        ↓
Future requests include it
        ↓
Server verifies it's still valid
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That last step — verifying the proof of identity on every request — is what keeps a user &quot;logged in&quot; without asking for their password on every single page.&lt;/p&gt;
&lt;h3 id=&quot;the-main-types-of-authentication&quot;&gt;The Main Types of Authentication&lt;/h3&gt;
&lt;p&gt;There isn't just one way to authenticate someone. Here are the methods you'll actually run into as a developer.&lt;/p&gt;
&lt;h4 id=&quot;1-password-based-authentication&quot;&gt;1. Password-Based Authentication&lt;/h4&gt;
&lt;p&gt;The classic: a username/email plus a password, checked against a hashed value stored in your database. Never store plain-text passwords — always hash them with a slow, password-specific hashing algorithm such as &lt;strong&gt;Argon2id&lt;/strong&gt;. If it's not available, &lt;strong&gt;bcrypt&lt;/strong&gt; or another appropriate password hashing algorithm can be used.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/auth/hash-password.ts
import bcrypt from &quot;bcryptjs&quot;;

const SALT_ROUNDS = 12;

export async function hashPassword(plainPassword: string): Promise&amp;lt;string&amp;gt; {
  return bcrypt.hash(plainPassword, SALT_ROUNDS);
}

export async function verifyPassword(
  plainPassword: string,
  hashedPassword: string
): Promise&amp;lt;boolean&amp;gt; {
  return bcrypt.compare(plainPassword, hashedPassword);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;bcrypt.hash()&lt;/code&gt; doesn't just scramble the password — it applies a slow, computationally expensive algorithm on purpose, which increases the cost of each password guess and makes large-scale brute-force attacks significantly more expensive. &lt;code&gt;SALT_ROUNDS = 12&lt;/code&gt; is above OWASP's minimum bcrypt work factor of 10, but the right value depends on your server's hardware and traffic. Benchmark your production environment and choose a cost that makes password verification sufficiently expensive without creating unacceptable latency.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;bcrypt limitation:&lt;/strong&gt; bcrypt has a hard 72-byte input limit, and most implementations (including &lt;code&gt;bcryptjs&lt;/code&gt;) silently truncate anything past it rather than erroring. Because UTF-8 characters like emoji or accented letters can take up to 4 bytes each, a password that looks well under the limit in &lt;em&gt;characters&lt;/em&gt; can still get silently cut short in &lt;em&gt;bytes&lt;/em&gt; — which can cause two different passwords to be treated as equivalent after truncation.&lt;br&gt;
OWASP currently recommends enforcing a maximum of 72 bytes (or less, depending on the implementation) when bcrypt is used. Because UTF-8 characters can occupy multiple bytes, a character-count limit alone doesn't necessarily correspond to bcrypt's byte limit.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Comparing passwords with a plain &lt;code&gt;===&lt;/code&gt; check after &quot;encrypting&quot; them with something reversible like Base64. That's encoding, not hashing — it can be reversed instantly. Only use a dedicated password-hashing library.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;2-session-based-authentication&quot;&gt;2. Session-Based Authentication&lt;/h4&gt;
&lt;p&gt;After a successful login, the server creates a &lt;strong&gt;session&lt;/strong&gt; record stored in server-side session storage such as a database or shared cache — and gives the browser a random session ID in an &lt;code&gt;httpOnly&lt;/code&gt; cookie. On every request, the server looks up that ID to know who's logged in.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/auth/session.ts
import { cookies } from &quot;next/headers&quot;;
import { randomUUID } from &quot;crypto&quot;;

export async function createSession(userId: string, expiresAt: Date) {
  // Generate a random, unguessable session ID — never store the raw
  // userId (or anything else predictable) directly as the cookie's
  // value. The cookie is a reference you look up server-side; it
  // should never double as proof of identity on its own.
  const sessionId = randomUUID();

  // Persist the mapping server-side, e.g.:
  // await db.session.create({ data: { sessionId, userId, expiresAt } });

  const cookieStore = await cookies();

  cookieStore.set(&quot;session_id&quot;, sessionId, {
    httpOnly: true,
    secure: true,
    sameSite: &quot;lax&quot;,
    expires: expiresAt,
    path: &quot;/&quot;,
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why each flag matters:&lt;/strong&gt; &lt;code&gt;httpOnly&lt;/code&gt; stops client-side JavaScript from reading the cookie (blocking a common XSS attack path). &lt;code&gt;secure&lt;/code&gt; means it's only sent over HTTPS. &lt;code&gt;sameSite: &quot;lax&quot;&lt;/code&gt; limits when the browser sends the cookie in cross-site requests and provides some CSRF protection. For state-changing operations, you should still implement an appropriate CSRF defense rather than relying on SameSite alone.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; straightforward server-side revocation; deleting a session record can invalidate that session immediately, and deleting all of a user's sessions can log them out everywhere.&lt;br&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; can require a server-side session lookup on authenticated requests, although caching can reduce the cost.&lt;/p&gt;
&lt;h4 id=&quot;3-token-based-authentication-jwt-&quot;&gt;3. Token-Based Authentication (JWT)&lt;/h4&gt;
&lt;p&gt;Instead of looking anything up server-side, a &lt;strong&gt;JSON Web Token (JWT)&lt;/strong&gt; packs the user's identity directly into a signed token. In a stateless JWT design, the server can verify the token cryptographically without looking up a session record in the database.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/auth/jwt.ts
import { SignJWT, jwtVerify } from &quot;jose&quot;;

const secretKey = new TextEncoder().encode(process.env.AUTH_SECRET);

export async function signToken(userId: string, expiresAt: Date) {
  return new SignJWT({})
    .setProtectedHeader({ alg: &quot;HS256&quot; })
    .setSubject(userId)
    .setIssuedAt()
    .setExpirationTime(expiresAt)
    .sign(secretKey);
}

export async function verifyToken(token: string) {
  try {
    const { payload } = await jwtVerify(token, secretKey, {
      algorithms: [&quot;HS256&quot;],
    });
    return payload;
  } catch {
    return null; // invalid or expired
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;jose&lt;/code&gt; is the standard, actively maintained library for signing and verifying JWTs using the Web Crypto API — it works in Node.js, edge runtimes, and browsers. &lt;code&gt;setSubject(userId)&lt;/code&gt; stores the user's ID in the token's standard &lt;code&gt;sub&lt;/code&gt; claim. &lt;code&gt;jwtVerify()&lt;/code&gt; automatically rejects the token if the signature doesn't match or if it's expired, so you never write manual expiry checks.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Signing is not encryption.&lt;/strong&gt; A JWT's payload is only base64url-encoded, not encrypted — anyone can decode and read it. The signature only proves it wasn't tampered with. Never put sensitive data (passwords, secrets) inside a JWT payload.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; no database lookup needed to verify identity, works well across distributed/stateless servers.&lt;br&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; harder to instantly revoke a single token before it expires (it's valid until it naturally expires unless you build extra machinery around it).&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 For a deeper side-by-side of these two models — including how each behaves at scale — see &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/session-vs-jwt-for-nextjs-16&quot;&gt;Session vs JWT: Which Should You Use in Next.js 16?&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;4-oauth-openid-connect-social-login-&quot;&gt;4. OAuth / OpenID Connect (Social Login)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;OAuth 2.0&lt;/strong&gt; is an authorization framework that lets an application obtain limited access to resources on behalf of a user. It doesn't, by itself, define how an application verifies the user's identity.&lt;/p&gt;
&lt;p&gt;For &quot;Sign in with Google,&quot; &quot;Sign in with Microsoft,&quot; and similar login flows, applications commonly use &lt;strong&gt;OpenID Connect (OIDC)&lt;/strong&gt; — an identity layer built on top of OAuth 2.0. The identity provider authenticates the user and returns an &lt;strong&gt;ID token&lt;/strong&gt; containing claims about that authentication.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;User clicks &quot;Sign in with GitHub&quot;
        ↓
Redirected to GitHub
        ↓
User authenticates and approves access
        ↓
GitHub redirects back with a code
        ↓
Your server exchanges the code for tokens
        ↓
Your server validates the OIDC identity information
        ↓
Your app creates its own session
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is why OAuth is so popular for beginners and production apps alike — you outsource the hardest parts (password storage, breach risk, 2FA) to a provider that already does it well.&lt;/p&gt;
&lt;h4 id=&quot;5-multi-factor-authentication-mfa-2fa-&quot;&gt;5. Multi-Factor Authentication (MFA / 2FA)&lt;/h4&gt;
&lt;p&gt;MFA requires &lt;strong&gt;more than one&lt;/strong&gt; proof of identity — typically &quot;something you know&quot; (a password) plus &quot;something you have&quot; (a phone, an authenticator app code, a hardware key). Even if a password leaks, the attacker still can't get in without the second factor.&lt;/p&gt;
&lt;h4 id=&quot;6-passwordless-authentication-magic-links-amp-passkeys-&quot;&gt;6. Passwordless Authentication (Magic Links &amp;amp; Passkeys)&lt;/h4&gt;
&lt;p&gt;Two increasingly common alternatives to passwords:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Magic links&lt;/strong&gt; — the user enters their email, gets a one-time sign-in link, and clicking it logs them in. No password to remember or leak.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Passkeys (WebAuthn)&lt;/strong&gt; — device-based authentication using Face ID, Windows Hello, or a hardware security key. No shared secret exists to steal in the first place, which makes passkeys resistant to phishing in a way passwords fundamentally aren't.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;a-minimal-authentication-flow-in-next-js&quot;&gt;A Minimal Authentication Flow in Next.js&lt;/h3&gt;
&lt;p&gt;Here's a simplified but realistic login flow using &lt;strong&gt;Next.js 16&lt;/strong&gt;, &lt;strong&gt;TypeScript&lt;/strong&gt;, and session cookies — the same core pattern used by most production apps, just without the database wiring.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// actions/login.ts
&quot;use server&quot;;

import { redirect } from &quot;next/navigation&quot;;
import { verifyPassword } from &quot;@/lib/auth/hash-password&quot;;
import { createSession } from &quot;@/lib/auth/session&quot;;
import { z } from &quot;zod&quot;;

const loginSchema = z.object({
  email: z.email(),
  password: z.string().min(8),
});

export async function login(prevState: unknown, formData: FormData) {
  const parsed = loginSchema.safeParse({
    email: formData.get(&quot;email&quot;),
    password: formData.get(&quot;password&quot;),
  });

  if (!parsed.success) {
    return { error: &quot;Enter a valid email and password.&quot; };
  }

  // Look up the user in your database (pseudocode)
  const user = await db.user.findUnique({ where: { email: parsed.data.email } });

  if (!user || !(await verifyPassword(parsed.data.password, user.passwordHash))) {
    // Same generic error for both cases — don't reveal which one was wrong
    return { error: &quot;Invalid email or password.&quot; };
  }

  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
  await createSession(user.id, expiresAt);

  redirect(&quot;/dashboard&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; the form data is validated with Zod before touching the database — never trust raw input. Notice the login failure returns the &lt;strong&gt;same generic message&lt;/strong&gt; whether the email doesn't exist or the password is wrong; revealing which one failed makes it easier for attackers to enumerate valid accounts. A working session gets created only after the password check succeeds, then the user is redirected.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Checking authentication only in proxy and assuming that's enough. Proxy-level checks are a good UX shortcut (fast redirect before a page even loads), but sensitive Server Actions, Route Handlers, and data-access functions should independently verify authentication and authorization rather than relying solely on &lt;code&gt;proxy.ts&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Building this by hand is a great way to learn, but most production apps reach for a maintained library instead — see &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/how-to-add-nextauth-in-nextjs-16&quot;&gt;How to Add Auth.js (NextAuth v5) in Next.js 16&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/how-to-add-clerk-authentication-in-nextjs-16&quot;&gt;Add Clerk Authentication to Next.js 16&lt;/a&gt;, or (if you're already on Supabase) &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/supabase-auth-vs-firebase-auth&quot;&gt;Supabase Auth vs Firebase Auth&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;common-authentication-mistakes-to-avoid&quot;&gt;Common Authentication Mistakes to Avoid&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Storing plain-text passwords&lt;/strong&gt; — always hash with Argon2id or bcrypt, never store or log the raw password&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storing a predictable value as the session cookie&lt;/strong&gt; — the cookie should be a random, unguessable reference, never the raw user ID or anything else an attacker could construct&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Weak or reused session/JWT secrets&lt;/strong&gt; — generate a long, random secret and never commit it to git&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Revealing which field was wrong&lt;/strong&gt; — &quot;Invalid email&quot; vs. &quot;Invalid password&quot; tells attackers an account exists&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skipping rate limiting&lt;/strong&gt; — without it, attackers can brute-force passwords with unlimited attempts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Relying only on client-side checks&lt;/strong&gt; — a hidden button or disabled form field doesn't stop a direct API request&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Forgetting to expire sessions/tokens&lt;/strong&gt; — every session and token should have a sane expiry, not live forever&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;frequently-asked-questions&quot;&gt;Frequently Asked Questions&lt;/h3&gt;
&lt;h4 id=&quot;what-is-the-difference-between-authentication-and-authorization-&quot;&gt;What is the difference between authentication and authorization?&lt;/h4&gt;
&lt;p&gt;Authentication confirms &lt;em&gt;who&lt;/em&gt; someone is (logging in). Authorization decides &lt;em&gt;what&lt;/em&gt; they're allowed to do once they're recognized (permissions, roles, access control). You always need authentication before authorization can happen.&lt;/p&gt;
&lt;h4 id=&quot;is-a-login-system-the-same-thing-as-authentication-&quot;&gt;Is a login system the same thing as authentication?&lt;/h4&gt;
&lt;p&gt;Yes — a login system is simply the implementation of authentication. Whatever mechanism confirms a user's identity (password, OAuth, passkey) is functioning as authentication, regardless of what you call the feature.&lt;/p&gt;
&lt;h4 id=&quot;what-s-the-difference-between-sessions-and-jwts-&quot;&gt;What's the difference between sessions and JWTs?&lt;/h4&gt;
&lt;p&gt;A session stores authentication/session state on the server and gives the browser only a random reference ID, which allows instant revocation. A JWT stores the identity data directly inside a signed token the server can verify without a database lookup, trading easy revocation for less server-side state.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Keep JWT minimal — a JWT should not be treated as a general-purpose user profile or database replacement.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;do-i-need-a-library-to-build-authentication-or-can-i-build-it-myself-&quot;&gt;Do I need a library to build authentication, or can I build it myself?&lt;/h4&gt;
&lt;p&gt;You can build basic password + session authentication yourself using the patterns above, but for anything production-facing, an established library (&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/how-to-add-nextauth-in-nextjs-16&quot;&gt;Auth.js&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/how-to-add-clerk-authentication-in-nextjs-16&quot;&gt;Clerk&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/authjs-vs-better-auth&quot;&gt;Better Auth&lt;/a&gt;, or your database provider's built-in auth) is strongly recommended — they handle edge cases like token rotation, breach detection, and secure defaults that are easy to get subtly wrong by hand.&lt;/p&gt;
&lt;h4 id=&quot;is-two-factor-authentication-2fa-actually-necessary-&quot;&gt;Is two-factor authentication (2FA) actually necessary?&lt;/h4&gt;
&lt;p&gt;For anything holding sensitive data — financial info, personal messages, admin access — yes. 2FA dramatically reduces account takeover risk even when a password leaks, since the attacker also needs the second factor.&lt;/p&gt;
&lt;h4 id=&quot;are-passkeys-better-than-passwords-&quot;&gt;Are passkeys better than passwords?&lt;/h4&gt;
&lt;p&gt;Generally yes, from a security standpoint — passkeys are phishing-resistant because there's no shared secret to trick someone into revealing. Adoption is still growing, though, so most apps currently offer passkeys as an &lt;em&gt;option&lt;/em&gt; alongside passwords rather than a full replacement.&lt;/p&gt;
&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;/h3&gt;
&lt;p&gt;Authentication boils down to one question — &quot;who are you?&quot; — answered through a password, a token, a device, or some combination of them. Once you separate authentication from authorization, the ecosystem becomes easier to understand: passwords and passkeys are authentication mechanisms, sessions and JWTs are ways to maintain authenticated state, and OAuth/OIDC provide delegated authorization and federated identity.&lt;/p&gt;
&lt;p&gt;If you're building this in a real Next.js app, the next logical steps are wiring up role-based authorization on top of your authentication layer, and deciding between session-based and JWT-based auth based on whether instant revocation matters for your product.&lt;/p&gt;
&lt;h3 id=&quot;continue-learning&quot;&gt;Continue Learning&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/session-vs-jwt-for-nextjs-16&quot;&gt;Session vs JWT: Which Should You Use in Next.js 16?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/role-based-authentication-nextjs-16&quot;&gt;Role-Based Access Control (RBAC) in Next.js 16 (2026 Beginner's Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/authjs-vs-better-auth&quot;&gt;Auth.js vs Better Auth in 2026: Which Should You Use?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;/https://devstacked.tech/blog/clerk-vs-nextauth-2026&quot;&gt;NextAuth (Auth.js) vs Clerk in 2026: Compared&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/supabase-auth-vs-firebase-auth&quot;&gt;Supabase Auth vs Firebase Auth in 2026&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;free-developer-tools&quot;&gt;Free Developer Tools&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/tools/zod-schema-generator&quot;&gt;Zod Schema Generator&lt;/a&gt; — generate a validated schema for your role/permission payloads straight from a JSON sample or TypeScript interface, including a ready-to-use Server Action.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/tools/rls-policy-generator&quot;&gt;Supabase RLS Policy Generator&lt;/a&gt; — pair this guide's application-level RBAC with database-level Row Level Security. Generates owner-only, team, and admin-override policies with the &lt;code&gt;auth.uid()&lt;/code&gt; performance wrapper built in.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25969/what-is-authentication-a-beginners-guide-2026</guid>
<pubDate>Tue, 01 Sep 2026 05:07:56 +0000</pubDate>
</item>
<item>
<title>Session vs JWT: Which Should You Use in Next.js 16?</title>
<link>https://coderlegion.com/25323/session-vs-jwt-which-should-you-use-in-next-js-16</link>
<description>&lt;p&gt;You're building a login flow in Next.js 16, and you hit the question every auth tutorial glosses over: should the logged-in state live in a &lt;strong&gt;session&lt;/strong&gt; or a &lt;strong&gt;JWT&lt;/strong&gt;? Half the guides you find use one, half use the other, and almost none of them tell you &lt;em&gt;why&lt;/em&gt; — or what breaks if you pick wrong.&lt;/p&gt;
&lt;p&gt;Here's the annoying part: both approaches look nearly identical from the outside. A cookie gets set, the user stays logged in, everyone's happy. The difference only shows up later — when you need to log a user out of every device instantly, when your app scales past one server, or when you're debugging why a &quot;revoked&quot; user can still hit your API for another six days.&lt;/p&gt;
&lt;p&gt;By the end of this guide, you'll know exactly how sessions and JWTs work under the hood, see real, working code for both in Next.js 16 — using &lt;code&gt;jose&lt;/code&gt;, Server Actions, and &lt;code&gt;proxy.ts&lt;/code&gt; — and know which one actually fits your project instead of copying whatever the last tutorial you read happened to use.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-this-is-tricky&quot;&gt;Why This Is Tricky&lt;/h3&gt;
&lt;p&gt;Most explanations of &quot;session vs JWT&quot; get stuck comparing them as if they're two totally separate technologies. They're not. In both approaches, the browser stores a cookie. The real difference is &lt;strong&gt;what that cookie contains, and where the source of truth lives&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Session-based auth&lt;/strong&gt;: the cookie holds a random ID. The actual user data lives in your database. Every request that needs to know &quot;who is this?&quot; looks it up server-side.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JWT-based auth&lt;/strong&gt;: the cookie holds the user data itself, cryptographically signed. Your server can verify it's untampered without ever touching a database.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That one distinction — &lt;em&gt;lookup vs. self-contained&lt;/em&gt; — is what drives every tradeoff in this post: revocation, scaling, latency, and how much you need to trust the cookie itself.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; If you've used &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/how-to-add-nextauth-in-nextjs-16&quot;&gt;Auth.js (NextAuth v5)&lt;/a&gt;, you've already used both without necessarily noticing — its &lt;code&gt;session: { strategy: &quot;jwt&quot; }&lt;/code&gt; and &lt;code&gt;session: { strategy: &quot;database&quot; }&lt;/code&gt; options are exactly this same choice, just configured for you.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;This guide uses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Next.js 16&lt;/strong&gt; with the App Router&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TypeScript&lt;/strong&gt; in strict mode&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;React Compiler&lt;/strong&gt; enabled (no manual &lt;code&gt;useMemo&lt;/code&gt;/&lt;code&gt;useCallback&lt;/code&gt; needed)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tailwind CSS&lt;/strong&gt; for the (minimal) UI examples&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You don't need a real database to follow along — the JWT section has zero dependencies, and the session section uses a generic &lt;code&gt;db&lt;/code&gt; client you can swap for Postgres, Supabase, or whatever you're already using.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-sessions-work&quot;&gt;How Sessions Work&lt;/h3&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Think of it like:&lt;/strong&gt; a coat check ticket. The ticket itself is just a number — it means nothing on its own. The actual coat (your user data) stays with the attendant (your database). Lose the ticket, and the attendant can also just... stop honoring it. Instantly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;User logs in with the right credentials.&lt;/li&gt;
&lt;li&gt;Your server generates a random token, stores it in a &lt;code&gt;sessions&lt;/code&gt; table alongside the user's ID and an expiry, and sends that token back as an &lt;code&gt;httpOnly&lt;/code&gt; cookie.&lt;/li&gt;
&lt;li&gt;On every request, your server takes the cookie value, looks it up in the database, and either finds a valid session or doesn't.&lt;/li&gt;
&lt;li&gt;To log someone out — including forcibly, from your side — you delete the row. Done. The cookie is now worthless.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;The tradeoff:&lt;/strong&gt; every authenticated request costs a database round trip (unless you cache it), but you get instant, guaranteed revocation.&lt;/p&gt;
&lt;h3 id=&quot;how-jwts-work&quot;&gt;How JWTs Work&lt;/h3&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Think of it like:&lt;/strong&gt; a sealed, signed letter. Anyone can read what's inside once it's opened, but nobody can forge your signature or quietly edit the contents without breaking the seal. The letter is self-sufficient — you don't need to call anyone to confirm it's real, just check the seal.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;User logs in with the right credentials.&lt;/li&gt;
&lt;li&gt;Your server creates a token containing the user's ID (and whatever else you want) and &lt;strong&gt;signs&lt;/strong&gt; it with a secret key.&lt;/li&gt;
&lt;li&gt;On every request, your server checks the signature against that same secret. If it matches, the data inside is trusted — no database lookup required.&lt;/li&gt;
&lt;li&gt;To log someone out, you delete the cookie client-side. But if someone already has a copy of that token, it's still valid — and still verifiable — until it naturally expires. There's no built-in way to say &quot;actually, ignore this one.&quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;The tradeoff:&lt;/strong&gt; verification is fast and needs no database, but you give up the ability to instantly kill a single session without extra machinery.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Calling a JWT stored in a cookie a &quot;session&quot; and assuming it behaves like one. It doesn't — a JWT has no server-side record at all unless you deliberately build one (see the revocation section below). If your mental model is &quot;session,&quot; you'll be surprised the first time you try to force-logout a user and nothing happens.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Cookie size limits:&lt;/strong&gt; browsers cap a single cookie at roughly 4KB, and most reverse proxies (Nginx, Cloudflare) reject requests once the &lt;em&gt;total&lt;/em&gt; header size gets much past 8KB. A session-ID cookie is always tiny — it's just a random string. A JWT cookie grows with every claim you cram into the payload (roles, permissions, profile fields). Stuff enough into it and the cookie silently fails to set, or your CDN starts 400-ing requests, with no obvious error pointing back at &quot;your JWT got too big.&quot; Keep JWT payloads minimal — an ID and maybe a role, not a serialized user object.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-1-set-up-the-shared-pieces&quot;&gt;Step 1: Set Up the Shared Pieces&lt;/h3&gt;
&lt;p&gt;Both approaches need somewhere to keep a signing secret. Add one to your environment file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# .env.local
SESSION_SECRET=your-long-random-secret-here
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Generate a strong value instead of typing something memorable:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;node -e &quot;console.log(crypto.randomBytes(32).toString('hex'))&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Committing &lt;code&gt;SESSION_SECRET&lt;/code&gt; to git, or reusing your &lt;code&gt;.env.local&lt;/code&gt; value in production. Treat it exactly like a database password — if it leaks, an attacker can forge valid sessions for any user, for either approach below.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-2-build-jwt-based-auth-with-jose&quot;&gt;Step 2: Build JWT-Based Auth with &lt;code&gt;jose&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;jose&lt;/code&gt; is the standard library for signing and verifying JWTs in an Edge-compatible way — it uses the Web Crypto API instead of Node's &lt;code&gt;crypto&lt;/code&gt; module, so it works identically in Server Actions, Route Handlers, and &lt;code&gt;proxy.ts&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install jose
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;create-the-sign-verify-helpers&quot;&gt;Create the sign/verify helpers&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/jwt/session.ts
import &quot;server-only&quot;;
import { SignJWT, jwtVerify } from &quot;jose&quot;;

const secretKey = process.env.SESSION_SECRET;
const encodedKey = new TextEncoder().encode(secretKey);

export async function encrypt(userId: string, expiresAt: Date) {
  return new SignJWT({})
    .setProtectedHeader({ alg: &quot;HS256&quot; })
    .setSubject(userId) // sub — who this token belongs to
    .setIssuedAt() // iat — when it was issued
    .setExpirationTime(expiresAt) // exp — when it stops being valid
    .sign(encodedKey);
}

export async function decrypt(token: string | undefined = &quot;&quot;) {
  try {
    const { payload } = await jwtVerify(token, encodedKey, {
      algorithms: [&quot;HS256&quot;],
    });
    return payload; // payload.sub is the userId; iat/exp are unix seconds
  } catch {
    // Invalid signature, tampered payload, or expired token
    return null;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; instead of a custom &lt;code&gt;userId&lt;/code&gt; field, this uses the standard, registered JWT claims — &lt;code&gt;sub&lt;/code&gt; (subject, via &lt;code&gt;.setSubject()&lt;/code&gt;), &lt;code&gt;iat&lt;/code&gt; (issued-at, via &lt;code&gt;.setIssuedAt()&lt;/code&gt;), and &lt;code&gt;exp&lt;/code&gt; (expiry, via &lt;code&gt;.setExpirationTime()&lt;/code&gt;). These three are part of the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc7519#section-4.1&quot;&gt;JWT spec itself&lt;/a&gt;, not a &lt;code&gt;jose&lt;/code&gt;-specific convention, so any JWT library — in any language — knows how to read them. &lt;code&gt;jwtVerify&lt;/code&gt; automatically checks &lt;code&gt;exp&lt;/code&gt; and rejects an expired token before your code ever sees the payload, so you never write manual date comparisons.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Signing is not encryption.&lt;/strong&gt; &lt;code&gt;SignJWT&lt;/code&gt; — despite this file calling the wrapper functions &lt;code&gt;encrypt&lt;/code&gt;/&lt;code&gt;decrypt&lt;/code&gt; for readability — does &lt;strong&gt;not&lt;/strong&gt; hide the payload. A JWT's contents are only base64url-&lt;em&gt;encoded&lt;/em&gt;, not encrypted, so anyone who intercepts the cookie (or just opens DevTools → Application → Cookies) can decode and read &lt;code&gt;sub&lt;/code&gt;, &lt;code&gt;iat&lt;/code&gt;, and &lt;code&gt;exp&lt;/code&gt; in plain text. What the signature guarantees is that the payload &lt;strong&gt;wasn't tampered with&lt;/strong&gt; — change one character and &lt;code&gt;jwtVerify&lt;/code&gt; rejects it. Never put anything genuinely secret (passwords, tokens for other services, PII you wouldn't want exposed) inside a signed JWT. If you actually need to hide the contents too, that's a different primitive — JWE — which &lt;code&gt;jose&lt;/code&gt; supports separately via &lt;code&gt;EncryptJWT&lt;/code&gt;/&lt;code&gt;jwtDecrypt&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Do you still need &lt;code&gt;setExpirationTime&lt;/code&gt; if you also track expiry elsewhere?&lt;/strong&gt; No — and the version above deliberately avoids the redundancy. Compute the expiry date &lt;strong&gt;once&lt;/strong&gt;, and hand that same &lt;code&gt;Date&lt;/code&gt; to both &lt;code&gt;setExpirationTime()&lt;/code&gt; (for the JWT's own &lt;code&gt;exp&lt;/code&gt; claim) and the cookie's &lt;code&gt;expires&lt;/code&gt; option, as shown in the next step. Two separately-computed &quot;7 days from now&quot; values can drift apart by milliseconds and give you two conflicting sources of truth for the same thing — pick one.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;create-the-session-cookie-on-login&quot;&gt;Create the session cookie on login&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/jwt/actions.ts
&quot;use server&quot;;

import { cookies } from &quot;next/headers&quot;;
import { redirect } from &quot;next/navigation&quot;;
import { encrypt } from &quot;./session&quot;;

export async function createSession(userId: string) {
  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
  const session = await encrypt(userId, expiresAt); // same Date used for exp and the cookie
  const cookieStore = await cookies();

  cookieStore.set(&quot;session&quot;, session, {
    httpOnly: true,
    secure: true,
    expires: expiresAt,
    sameSite: &quot;lax&quot;,
    path: &quot;/&quot;,
  });
}

export async function deleteSession() {
  const cookieStore = await cookies();
  cookieStore.delete(&quot;session&quot;);
  redirect(&quot;/login&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;httpOnly: true&lt;/code&gt; keeps the cookie invisible to client-side JavaScript, which stops it from being stolen via an XSS attack. &lt;code&gt;secure: true&lt;/code&gt; means it's only ever sent over HTTPS. &lt;code&gt;sameSite: &quot;lax&quot;&lt;/code&gt; blocks the cookie from being attached to most cross-site requests, which is your main defense against CSRF here. None of this is JWT-specific — you'd want the exact same flags on a session-ID cookie too.&lt;/p&gt;
&lt;h4 id=&quot;verify-the-session-with-a-data-access-layer&quot;&gt;Verify the session with a Data Access Layer&lt;/h4&gt;
&lt;p&gt;Don't scatter &lt;code&gt;decrypt()&lt;/code&gt; calls across every page. Centralize verification in one cached function — this is what Next.js's own docs call a &lt;strong&gt;Data Access Layer (DAL)&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/jwt/dal.ts
import &quot;server-only&quot;;
import { cache } from &quot;react&quot;;
import { cookies } from &quot;next/headers&quot;;
import { redirect } from &quot;next/navigation&quot;;
import { decrypt } from &quot;./session&quot;;

export const verifySession = cache(async () =&amp;gt; {
  const cookieStore = await cookies();
  const token = cookieStore.get(&quot;session&quot;)?.value;
  const session = await decrypt(token);

  if (!session?.sub) {
    redirect(&quot;/login&quot;);
  }

  return { isAuth: true, userId: session.sub };
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; wrapping this in React's &lt;code&gt;cache()&lt;/code&gt; means that no matter how many components on the same page call &lt;code&gt;verifySession()&lt;/code&gt;, the actual decrypt-and-check logic only runs once per request — every subsequent call reuses the result. &lt;code&gt;session.sub&lt;/code&gt; is the standard claim set by &lt;code&gt;.setSubject()&lt;/code&gt; earlier, so this reads it back out under a friendlier &lt;code&gt;userId&lt;/code&gt; name for the rest of your app. Call this at the top of any Server Component, Server Action, or Route Handler that needs to know who's logged in.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/dashboard/page.tsx
import { verifySession } from &quot;@/lib/jwt/dal&quot;;

export default async function DashboardPage() {
  const { userId } = await verifySession();

  return &amp;lt;h1&amp;gt;Welcome back, user {userId}&amp;lt;/h1&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;add-an-optimistic-check-in-proxy-ts&quot;&gt;Add an optimistic check in &lt;code&gt;proxy.ts&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;In Next.js 16, &lt;code&gt;middleware.ts&lt;/code&gt; was renamed to &lt;code&gt;proxy.ts&lt;/code&gt; — same file convention, same idea. It's the first thing that runs before a request reaches a page, which makes it a good place for a &lt;em&gt;fast, optimistic&lt;/em&gt; redirect. Full verification still happens in the DAL above — &lt;code&gt;proxy.ts&lt;/code&gt; just avoids letting an obviously-unauthenticated request render a protected page at all.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// proxy.ts
import { NextRequest, NextResponse } from &quot;next/server&quot;;

const protectedRoutes = [&quot;/dashboard&quot;];
const publicRoutes = [&quot;/login&quot;, &quot;/signup&quot;];

export default function proxy(req: NextRequest) {
  const path = req.nextUrl.pathname;
  const isProtectedRoute = protectedRoutes.some((route) =&amp;gt; path.startsWith(route));
  const isPublicRoute = publicRoutes.includes(path);

  // Optimistic check only — we just look for the cookie's presence.
  // We deliberately don't verify the signature here; that happens
  // in verifySession() inside the Data Access Layer.
  const cookie = req.cookies.get(&quot;session&quot;)?.value;

  if (isProtectedRoute &amp;amp;&amp;amp; !cookie) {
    return NextResponse.redirect(new URL(&quot;/login&quot;, req.nextUrl));
  }

  if (isPublicRoute &amp;amp;&amp;amp; cookie) {
    return NextResponse.redirect(new URL(&quot;/dashboard&quot;, req.nextUrl));
  }

  return NextResponse.next();
}

export const config = {
  matcher: [&quot;/((?!_next/static|_next/image|favicon.ico).*)&quot;],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Treating &lt;code&gt;proxy.ts&lt;/code&gt; as your only security check. It's a UX nicety that avoids a flash of protected content — it is not a substitute for re-verifying the session inside every Server Action and Route Handler that actually touches data. A request can always be sent directly, bypassing whatever page it &quot;should&quot; have come from.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-3-build-database-backed-sessions&quot;&gt;Step 3: Build Database-Backed Sessions&lt;/h3&gt;
&lt;p&gt;Now the other approach — a real session table your server checks on each request.&lt;/p&gt;
&lt;h4 id=&quot;create-the-sessions-table&quot;&gt;Create the sessions table&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Works for Postgres/Supabase; adjust types for your database
create table sessions (
  token_hash text primary key,
  user_id uuid not null references auth.users(id) on delete cascade,
  expires_at timestamptz not null,
  created_at timestamptz not null default now()
);

create index idx_sessions_user_id on sessions (user_id);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; notice the primary key is &lt;code&gt;token_hash&lt;/code&gt;, not &lt;code&gt;token&lt;/code&gt;. We never store the raw session token in the database — only its hash. This mirrors how you'd never store a plaintext password: if your database ever leaked, an attacker with the hashes still couldn't reconstruct a working cookie.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Cleanup:&lt;/strong&gt; expiring a session only stops it from being &lt;em&gt;accepted&lt;/em&gt; — the &lt;code&gt;expires_at &amp;lt; now()&lt;/code&gt; check in &lt;code&gt;verifyDbSession&lt;/code&gt; below handles that. It does &lt;strong&gt;not&lt;/strong&gt; delete the row. Left alone, this table grows forever, one row per login, most of them long past their expiry date. Run a scheduled cleanup — a &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/tools/vercel-cron-generator&quot;&gt;Vercel Cron&lt;/a&gt; job hitting a small Route Handler once a day is enough for most apps:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;delete from sessions where expires_at &amp;lt; now();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is purely a housekeeping/performance concern (an unbounded table gets slower to index and back up over time), not a security one — an expired row is already useless for authentication either way.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;create-the-session-on-login&quot;&gt;Create the session on login&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/db-session/create.ts
import &quot;server-only&quot;;
import { randomBytes, createHash } from &quot;crypto&quot;;
import { cookies } from &quot;next/headers&quot;;
import { db } from &quot;@/lib/db&quot;;

function hashToken(token: string) {
  return createHash(&quot;sha256&quot;).update(token).digest(&quot;hex&quot;);
}

export async function createDbSession(userId: string) {
  const token = randomBytes(32).toString(&quot;hex&quot;); // goes to the browser
  const tokenHash = hashToken(token); // goes to the database
  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);

  await db.session.create({
    data: { tokenHash, userId, expiresAt },
  });

  const cookieStore = await cookies();
  cookieStore.set(&quot;session&quot;, token, {
    httpOnly: true,
    secure: true,
    sameSite: &quot;lax&quot;,
    expires: expiresAt,
    path: &quot;/&quot;,
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;randomBytes(32)&lt;/code&gt; generates a token with enough entropy that guessing it is computationally infeasible — this is the piece doing the actual security work, not the hashing. We hash it with SHA-256 purely so a database leak doesn't hand out working session tokens; the browser still holds the real, unhashed token in its cookie.&lt;/p&gt;
&lt;h4 id=&quot;verify-the-session-with-caching-so-it-doesn-t-hit-the-database-on-every-render-&quot;&gt;Verify the session (with caching, so it doesn't hit the database on every render)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/db-session/dal.ts
import &quot;server-only&quot;;
import { cache } from &quot;react&quot;;
import { cookies } from &quot;next/headers&quot;;
import { createHash } from &quot;crypto&quot;;
import { redirect } from &quot;next/navigation&quot;;
import { db } from &quot;@/lib/db&quot;;

function hashToken(token: string) {
  return createHash(&quot;sha256&quot;).update(token).digest(&quot;hex&quot;);
}

export const verifyDbSession = cache(async () =&amp;gt; {
  const cookieStore = await cookies();
  const token = cookieStore.get(&quot;session&quot;)?.value;

  if (!token) redirect(&quot;/login&quot;);

  const tokenHash = hashToken(token);
  const session = await db.session.findUnique({
    where: { tokenHash },
    include: { user: true },
  });

  if (!session || session.expiresAt &amp;lt; new Date()) {
    redirect(&quot;/login&quot;);
  }

  return { isAuth: true, userId: session.userId, user: session.user };
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; just like the JWT version, &lt;code&gt;cache()&lt;/code&gt; means one database lookup per request no matter how many components ask &quot;who's logged in?&quot; This is the piece that makes database sessions viable in practice — without it, a page with five components each checking auth would fire five identical queries.&lt;/p&gt;
&lt;h4 id=&quot;revoking-a-session-the-whole-point-of-this-approach&quot;&gt;Revoking a session — the whole point of this approach&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/db-session/delete.ts
&quot;use server&quot;;

import { cookies } from &quot;next/headers&quot;;
import { redirect } from &quot;next/navigation&quot;;
import { createHash } from &quot;crypto&quot;;
import { db } from &quot;@/lib/db&quot;;

function hashToken(token: string) {
  return createHash(&quot;sha256&quot;).update(token).digest(&quot;hex&quot;);
}

export async function deleteDbSession() {
  const cookieStore = await cookies();
  const token = cookieStore.get(&quot;session&quot;)?.value;

  if (token) {
    await db.session.delete({ where: { tokenHash: hashToken(token) } }).catch(() =&amp;gt; {});
  }

  cookieStore.delete(&quot;session&quot;);
  redirect(&quot;/login&quot;);
}

// Force-logout every session for a user — e.g. after a password reset
export async function revokeAllSessionsForUser(userId: string) {
  await db.session.deleteMany({ where: { userId } });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;revokeAllSessionsForUser&lt;/code&gt; is the function you can't build with plain JWTs — one query, and every device that user is logged in on is instantly signed out, because the next request they make finds no matching row. This is the single biggest practical reason teams reach for database sessions over JWTs.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;proxy.ts&lt;/code&gt; looks identical to the JWT version above — it's still just checking whether the &lt;code&gt;session&lt;/code&gt; cookie exists, since that check never needed to know which auth strategy is behind it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;session-vs-jwt-side-by-side&quot;&gt;Session vs JWT: Side-by-Side&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Session (database) &lt;/th&gt;&lt;th&gt;JWT&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Where the data lives&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Server-side (database row) &lt;/td&gt;&lt;td&gt;Inside the token itself&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Cookie contents&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Random, meaningless ID &lt;/td&gt;&lt;td&gt;Signed, self-contained payload&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Instant revocation&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;✅ Delete the row &lt;/td&gt;&lt;td&gt;❌ Not without extra machinery&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Force-logout everywhere&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;✅ One query &lt;/td&gt;&lt;td&gt;❌ Requires a blocklist or short expiry + refresh&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Per-request cost&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;One DB lookup (cacheable per request) &lt;/td&gt;&lt;td&gt;No DB call — just signature verification&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Works well at scale / multi-server&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Needs a shared database (usually already true) &lt;/td&gt;&lt;td&gt;Naturally stateless, nothing to share&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Payload can go stale&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;No — always reads current data &lt;/td&gt;&lt;td&gt;Yes — data inside is frozen until re-issued&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Implementation complexity&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Slightly more (schema, hashing, cleanup) &lt;/td&gt;&lt;td&gt;Slightly less (no schema needed)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Apps where instant revocation matters &amp;lt;br/&amp;gt; (banking, admin tools, anything security-sensitive) &lt;/td&gt;&lt;td&gt;High-traffic, distributed systems where avoiding a &amp;lt;br/&amp;gt;DB hit per request matters&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; &quot;JWTs don't need a database&quot; is true for verification, but most real apps still store &lt;em&gt;something&lt;/em&gt; — a refresh-token table, a denylist, or at minimum a &lt;code&gt;passwordChangedAt&lt;/code&gt; timestamp checked against the token's &lt;code&gt;iat&lt;/code&gt; (issued-at) claim. The stateless promise of JWTs is a little softer in practice than it sounds in theory.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;a-closer-look-what-horizontal-scaling-actually-means-here&quot;&gt;A Closer Look: What &quot;Horizontal Scaling&quot; Actually Means Here&lt;/h4&gt;
&lt;p&gt;&quot;Naturally stateless&quot; in the table above is doing a lot of work, so let's unpack it. Say your app runs on five server instances behind a load balancer — a request from the same user could land on a different instance every time.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;With database sessions:&lt;/strong&gt; whichever instance handles the request just needs to reach the &lt;em&gt;same&lt;/em&gt; database everyone else does. In practice this is a non-issue for most Next.js apps, since you already have one shared database — every instance was already talking to it for everything else. The &quot;scaling problem&quot; people worry about here mostly doesn't exist unless you're deliberately running sessions in something instance-local, like in-memory storage (don't do that).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;With JWTs:&lt;/strong&gt; verification needs nothing but the signing secret, which every instance already has as an environment variable. No instance has to coordinate with any other instance, or with a database, to answer &quot;is this token valid?&quot; That's genuinely useful once you're running at the edge — Cloudflare Workers, Vercel Edge Functions — across multiple regions, where a round trip to a single-region database could add real, noticeable latency to &lt;em&gt;every&lt;/em&gt; request.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the honest version: for a typical app with one shared Postgres/Supabase database, this isn't really a deciding factor — you're already &quot;horizontally scaled&quot; for sessions for free. It starts to matter once you're running truly distributed, multi-region, or edge-first infrastructure where a database round trip per request is a real, measured cost, not a hypothetical one.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;sliding-rolling-sessions&quot;&gt;Sliding (Rolling) Sessions&lt;/h3&gt;
&lt;p&gt;So far, every example here uses a &lt;strong&gt;fixed&lt;/strong&gt; expiry — log in, get 7 days, and after 7 days you're logged out no matter how active you were. A &lt;strong&gt;sliding session&lt;/strong&gt; (also called a rolling session) instead extends the expiry every time the user does something, so an active user effectively never gets logged out, while an idle one still expires on schedule.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;With database sessions, this is simple:&lt;/strong&gt; on every successful &lt;code&gt;verifyDbSession()&lt;/code&gt; call, update that row's &lt;code&gt;expires_at&lt;/code&gt; to &quot;now + 7 days&quot; again.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Inside verifyDbSession, after confirming the session is valid
await db.session.update({
  where: { tokenHash },
  data: { expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One extra write per (cached) verification — cheap, and the source of truth (the row) is already being touched anyway.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; In production it's common to only extend the session once every few hours instead of on every request to avoid unnecessary writes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;With JWTs, this is more work:&lt;/strong&gt; the &lt;code&gt;exp&lt;/code&gt; claim is baked into the signature at issue time — you can't quietly extend it without re-signing a brand-new token and setting a brand-new cookie on the response. Doable inside a Route Handler or Server Action (create a fresh token with a new &lt;code&gt;expiresAt&lt;/code&gt;, call &lt;code&gt;cookies().set()&lt;/code&gt; again), but it means writing a cookie back on requests that otherwise wouldn't need one, which is easy to forget to wire up consistently across every route.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; If &quot;stay logged in while active, expire after a week of silence&quot; is a real requirement for your app, that alone is a reasonable tie-breaker toward database sessions — it's a one-line &lt;code&gt;update&lt;/code&gt; there versus meaningfully more plumbing with JWTs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;can-you-get-revocation-with-jwts-&quot;&gt;Can You Get Revocation With JWTs?&lt;/h3&gt;
&lt;p&gt;Yes, with a tradeoff. Two common patterns:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Short expiry + refresh tokens.&lt;/strong&gt; Issue an access token that expires in 10–15 minutes, plus a longer-lived refresh token stored in the database. To revoke, you delete the refresh token — the access token is still technically valid, but only for a few more minutes, which is an acceptable blast radius for most apps.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A denylist.&lt;/strong&gt; Keep a small, fast-to-check store (Redis is the usual choice) of token IDs (&lt;code&gt;jti&lt;/code&gt; claims) that have been explicitly revoked. Every verification also checks this list. This reintroduces a lookup on every request — at that point, you've mostly reinvented database sessions with extra steps.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Neither of these makes JWTs behave exactly like sessions — they just narrow the gap. If instant, guaranteed revocation is a hard requirement (think: a compromised admin account), database sessions remove the problem entirely instead of managing around it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;which-one-should-you-actually-use-&quot;&gt;Which One Should You Actually Use?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Building a typical SaaS dashboard, internal tool, or content site?&lt;/strong&gt; Database sessions. The DB hit is cheap with caching, and &quot;log this user out right now&quot; is a feature you'll eventually need, even if you don't think so today.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Building something with genuinely massive, distributed, stateless traffic&lt;/strong&gt; (think: an API consumed by thousands of independent clients, or infrastructure where adding a database round trip to every request is a real cost)? JWTs earn their keep here.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Already using &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/how-to-add-nextauth-in-nextjs-16&quot;&gt;Auth.js&lt;/a&gt; or &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/how-to-add-clerk-authentication-in-nextjs-16&quot;&gt;Clerk&lt;/a&gt;?&lt;/strong&gt; You don't have to choose by hand — Auth.js exposes &lt;code&gt;session: { strategy: &quot;jwt&quot; | &quot;database&quot; }&lt;/code&gt; directly, and Clerk manages this decision for you behind its own infrastructure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Not sure yet?&lt;/strong&gt; Default to database sessions. It's the safer, more debuggable choice, and the performance cost is rarely the bottleneck people assume it'll be — cache the lookup with &lt;code&gt;cache()&lt;/code&gt; as shown above and it disappears from your profiler.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;frequently-asked-questions-1&quot;&gt;Frequently Asked Questions&lt;/h3&gt;
&lt;h4 id=&quot;can-i-combine-both-approaches-&quot;&gt;Can I combine both approaches?&lt;/h4&gt;
&lt;p&gt;Yes — this is actually what &quot;refresh tokens&quot; are: a short-lived JWT for fast verification, backed by a database-stored refresh token for revocation. You get JWT-speed reads most of the time, with a real revocation point when you need one.&lt;/p&gt;
&lt;h4 id=&quot;is-storing-a-jwt-in-a-cookie-the-same-as-storing-it-in-localstorage-&quot;&gt;Is storing a JWT in a cookie the same as storing it in localStorage?&lt;/h4&gt;
&lt;p&gt;No, and the difference matters for security. An &lt;code&gt;httpOnly&lt;/code&gt; cookie can't be read by client-side JavaScript, so it's protected from XSS attacks that steal tokens. &lt;code&gt;localStorage&lt;/code&gt; is fully readable by any script running on your page — a single XSS vulnerability anywhere in your dependency tree can leak every token stored there. Always use &lt;code&gt;httpOnly&lt;/code&gt; cookies for auth tokens in a Next.js app.&lt;/p&gt;
&lt;h4 id=&quot;does-proxy-ts-need-to-fully-verify-the-session-on-every-request-&quot;&gt;Does &lt;code&gt;proxy.ts&lt;/code&gt; need to fully verify the session on every request?&lt;/h4&gt;
&lt;p&gt;No, and for JWT-based auth verifying in &lt;code&gt;proxy.ts&lt;/code&gt; with &lt;code&gt;jose&lt;/code&gt; is genuinely fine performance-wise since it needs no database call. The stronger rule is: never treat &lt;code&gt;proxy.ts&lt;/code&gt; as your &lt;em&gt;only&lt;/em&gt; check. Always re-verify inside the Server Action or Route Handler that actually reads or writes data — &lt;code&gt;proxy.ts&lt;/code&gt; can be bypassed by hitting that endpoint directly.&lt;/p&gt;
&lt;h4 id=&quot;why-hash-the-session-token-before-storing-it-in-the-database-&quot;&gt;Why hash the session token before storing it in the database?&lt;/h4&gt;
&lt;p&gt;The same reason you hash passwords — if your database is ever compromised (a leaked backup, a misconfigured access policy), the attacker gets a list of hashes they can't turn back into working session cookies. It costs you one &lt;code&gt;sha256&lt;/code&gt; call per request, which is effectively free.&lt;/p&gt;
&lt;h4 id=&quot;which-one-does-auth-js-use-by-default-&quot;&gt;Which one does Auth.js use by default?&lt;/h4&gt;
&lt;p&gt;Auth.js defaults to the &lt;code&gt;&quot;jwt&quot;&lt;/code&gt; strategy unless you configure a database adapter, in which case it switches to &lt;code&gt;&quot;database&quot;&lt;/code&gt; automatically. You can also set &lt;code&gt;session: { strategy: &quot;jwt&quot; }&lt;/code&gt; explicitly even with an adapter configured, if you want JWTs for speed while still persisting user accounts.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;wrapping-up-1&quot;&gt;Wrapping Up&lt;/h3&gt;
&lt;p&gt;Sessions and JWTs aren't competing standards — they're two different answers to &quot;where does the source of truth for a logged-in user live?&quot; Sessions keep it on your server, trading a database lookup for instant, total control over who stays logged in. JWTs keep it in the token itself, trading that control for verification that never has to leave the request.&lt;/p&gt;
&lt;p&gt;For most Next.js apps, start with database sessions — the caching pattern above keeps the performance cost near zero, and you'll be glad revocation is a solved problem the first time you actually need it. Reach for JWTs specifically when you've measured that the database hit matters, not because a tutorial defaulted to it.&lt;/p&gt;
&lt;p&gt;From here, a natural next step is wiring either pattern into a real login form with &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/tools/zod-schema-generator&quot;&gt;Zod&lt;/a&gt; validating the credentials before they ever reach your session logic, or layering &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/role-based-authentication-nextjs-16&quot;&gt;role-based permissions&lt;/a&gt; on top of whichever session strategy you pick.&lt;/p&gt;
&lt;h3 id=&quot;continue-learning-1&quot;&gt;Continue Learning&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/how-to-add-nextauth-in-nextjs-16&quot;&gt;How to Add Auth.js (NextAuth v5) Authentication in Next.js 16 (2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/how-to-add-clerk-authentication-in-nextjs-16&quot;&gt;How to Add Clerk Authentication in Next.js 16 (2026 Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/clerk-vs-nextauth-2026&quot;&gt;Clerk vs NextAuth (Auth.js) in 2026: Which One Should You Choose?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/role-based-authentication-nextjs-16&quot;&gt;Role-Based Access Control (RBAC) in Next.js 16 (2026 Beginner's Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/supabase-auth-vs-firebase-auth&quot;&gt;Supabase Auth vs Firebase Auth: Which One Should You Choose in 2026?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;useful-resources&quot;&gt;Useful Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/panva/jose&quot;&gt;jose Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/guides/authentication&quot;&gt;Next.js Authentication Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html&quot;&gt;OWASP Session Management Cheat Sheet&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25323/session-vs-jwt-which-should-you-use-in-next-js-16</guid>
<pubDate>Tue, 25 Aug 2026 04:12:26 +0000</pubDate>
</item>
<item>
<title>Supabase Auth vs Firebase Auth: Which One Should You Choose in 2026?</title>
<link>https://coderlegion.com/24980/supabase-auth-vs-firebase-auth-which-one-should-you-choose-in-2026</link>
<description>&lt;p&gt;You're starting a new Next.js project, and before you write a single feature, you have to answer one boring but critical question: &lt;strong&gt;who's going to handle login?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Two names keep showing up at the top of every list — &lt;strong&gt;Supabase Auth&lt;/strong&gt; and &lt;strong&gt;Firebase Auth&lt;/strong&gt;. Both are free to start, both support Google/GitHub/email login, and both promise to save you from writing your own password-hashing logic. But they're built on completely different foundations, and picking the wrong one can mean months of awkward workarounds later — like discovering you need SQL joins for a permissions system, but you're stuck with Firestore's document model.&lt;/p&gt;
&lt;p&gt;This guide breaks down exactly how Supabase Auth and Firebase Auth differ in 2026 — architecture, pricing, security, and real Next.js 16 App Router code for both — so you can pick the right one &lt;em&gt;before&lt;/em&gt; you've built half your app on top of it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-this-comparison-is-tricky&quot;&gt;Why This Comparison Is Tricky&lt;/h3&gt;
&lt;p&gt;Most &quot;Supabase vs Firebase&quot; posts compare marketing pages, not real implementation experience. The truth is these two products solve authentication very differently under the hood:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Supabase Auth&lt;/strong&gt; is a layer on top of &lt;strong&gt;PostgreSQL&lt;/strong&gt; — every user lives in a real SQL table (&lt;code&gt;auth.users&lt;/code&gt;), and permissions are enforced with &lt;strong&gt;Row Level Security (RLS)&lt;/strong&gt; policies written in SQL.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Firebase Auth&lt;/strong&gt; is a layer on top of &lt;strong&gt;Firestore/Google Cloud&lt;/strong&gt; — users live in Google's own auth system, and permissions are enforced with &lt;strong&gt;Firestore Security Rules&lt;/strong&gt;, a completely different (non-SQL) rules language.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That single architectural difference is what actually decides which one fits your project — not the login form UI, which looks nearly identical on both.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;quick-answer&quot;&gt;Quick Answer&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Choose Supabase Auth&lt;/strong&gt; if you want a real Postgres database, SQL-based permissions (RLS), open-source flexibility, and the option to self-host later.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Choose Firebase Auth&lt;/strong&gt; if you're already invested in Firebase or Google Cloud services, or if Firebase's managed identity infrastructure fits your application's architecture.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now let's get into the details.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;supabase-auth-vs-firebase-auth-which-is-better-for-next-js-&quot;&gt;Supabase Auth vs Firebase Auth: Which Is Better for Next.js?&lt;/h3&gt;
&lt;p&gt;If you're building a Next.js SaaS application with PostgreSQL, Supabase Auth is usually the more natural choice because authentication, database access, and authorization can be designed around the same Postgres/RLS model.&lt;/p&gt;
&lt;p&gt;Firebase Auth is a better fit when your application already uses Firebase/Google Cloud services or your data model naturally fits Firestore.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;supabase-auth-vs-firebase-auth-feature-comparison&quot;&gt;Supabase Auth vs Firebase Auth: Feature Comparison&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Feature &lt;/th&gt;&lt;th&gt;Supabase Auth &lt;/th&gt;&lt;th&gt;Firebase Auth&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Authentication architecture &lt;/td&gt;&lt;td&gt;Integrated with Supabase/PostgreSQL ecosystem &lt;/td&gt;&lt;td&gt;Managed identity service&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Common database pairing     &lt;/td&gt;&lt;td&gt;PostgreSQL                                    &lt;/td&gt;&lt;td&gt;Firestore (NoSQL)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Free/no-cost auth tier &lt;/td&gt;&lt;td&gt;50,000 monthly active users (MAU) &lt;/td&gt;&lt;td&gt;Depends on Firebase Auth / Identity Platform configuration&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Paid pricing &lt;/td&gt;&lt;td&gt;Included in Supabase plan quota; $0.00325/MAU over Pro quota &lt;/td&gt;&lt;td&gt;Identity Platform: 50,000 MAU no-cost tier on Blaze, then usage-based pricing&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Email/password &lt;/td&gt;&lt;td&gt;✅ &lt;/td&gt;&lt;td&gt;✅&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Magic links &lt;/td&gt;&lt;td&gt;✅ &lt;/td&gt;&lt;td&gt;✅ (email link sign-in)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Social OAuth (Google, GitHub, etc.) &lt;/td&gt;&lt;td&gt;✅ &lt;/td&gt;&lt;td&gt;✅&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Phone/SMS OTP &lt;/td&gt;&lt;td&gt;✅ &lt;/td&gt;&lt;td&gt;✅&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Permissions model &lt;/td&gt;&lt;td&gt;Row Level Security (SQL policies) &lt;/td&gt;&lt;td&gt;Firestore Security Rules (custom rules language)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Self-hosting &lt;/td&gt;&lt;td&gt;✅ Yes (fully open source) &lt;/td&gt;&lt;td&gt;❌ No&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Open source &lt;/td&gt;&lt;td&gt;✅ Yes &lt;/td&gt;&lt;td&gt;❌ No&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Next.js SSR package &lt;/td&gt;&lt;td&gt;&lt;code&gt;@supabase/ssr&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Firebase JS SDK + &lt;code&gt;initializeServerApp&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Best paired with &lt;/td&gt;&lt;td&gt;Postgres-based SaaS apps &lt;/td&gt;&lt;td&gt;Google Cloud / Firestore apps&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Both platforms' free tiers are generous enough to cover most side projects and early-stage SaaS apps completely — pricing usually isn't the deciding factor until you're well past MVP stage.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;architecture-the-real-difference&quot;&gt;Architecture: The Real Difference&lt;/h3&gt;
&lt;p&gt;This is the part most comparison articles skip, and it's the one that actually matters.&lt;/p&gt;
&lt;h4 id=&quot;supabase-auth-runs-on-postgres&quot;&gt;Supabase Auth Runs on Postgres&lt;/h4&gt;
&lt;p&gt;When someone signs up with Supabase Auth, a row gets created in a special &lt;code&gt;auth.users&lt;/code&gt; table inside your actual Postgres database. Every other table you create — &lt;code&gt;profiles&lt;/code&gt;, &lt;code&gt;posts&lt;/code&gt;, &lt;code&gt;orders&lt;/code&gt; — can have a foreign key pointing straight at &lt;code&gt;auth.users.id&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That means your permissions logic is just SQL:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Only let a user see their own orders
create policy &quot;Users can view own orders&quot;
on orders
for select
using (auth.uid() = user_id);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; if your app has relational data — a user has many orders, an order has many line items, a team has many members — Postgres joins handle that naturally. You're not fighting the database to model relationships.&lt;/p&gt;
&lt;h4 id=&quot;firebase-auth-and-firestore&quot;&gt;Firebase Auth And Firestore&lt;/h4&gt;
&lt;p&gt;Firebase Auth manages identity separately from your application data. When paired with Firestore, authorization is commonly enforced using &lt;strong&gt;Firestore Security Rules&lt;/strong&gt; — a rules language, not SQL:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /orders/{orderId} {
      allow read: if request.auth != null &amp;amp;&amp;amp; request.auth.uid == resource.data.userId;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; Firestore is a document database. There's no native &lt;code&gt;JOIN&lt;/code&gt;. If your data is deeply relational (think: multi-tenant SaaS with teams, roles, and shared resources), you'll end up duplicating data across documents or running multiple round-trip queries to stitch things together client-side.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Neither model is &quot;wrong&quot; — Firestore's document model is genuinely great for chat apps, real-time feeds, and simple per-user data. Postgres shines once your data has real relationships between tables.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;setting-up-supabase-auth-in-next-js-16&quot;&gt;Setting Up Supabase Auth in Next.js 16&lt;/h3&gt;
&lt;p&gt;This guide assumes &lt;strong&gt;Next.js 16&lt;/strong&gt; with the App Router, &lt;strong&gt;TypeScript&lt;/strong&gt; in strict mode, and &lt;strong&gt;Tailwind CSS&lt;/strong&gt; — the current defaults for any new Next.js project.&lt;/p&gt;
&lt;h4 id=&quot;step-1-install-dependencies&quot;&gt;Step 1: Install Dependencies&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install @supabase/supabase-js @supabase/ssr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;@supabase/supabase-js&lt;/code&gt; is the core client library. &lt;code&gt;@supabase/ssr&lt;/code&gt; is the package built specifically for frameworks like Next.js — it handles the tricky part of syncing auth cookies between the browser and the server so your session works correctly in Server Components, Server Actions, and the browser alike.&lt;/p&gt;
&lt;h4 id=&quot;step-2-add-environment-variables&quot;&gt;Step 2: Add Environment Variables&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-env&quot;&gt;# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://yourproject.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your_publishable_key
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ Never expose a Supabase secret key (&lt;code&gt;sb_secret_...&lt;/code&gt;) or legacy &lt;code&gt;service_role&lt;/code&gt; key in client-side code. Both bypass RLS.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; For new Supabase projects, use the publishable key rather than the legacy anon key. Supabase is transitioning away from the legacy API keys during 2026.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;step-3-create-the-browser-client&quot;&gt;Step 3: Create the Browser Client&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/supabase/client.ts
import { createBrowserClient } from &quot;@supabase/ssr&quot;;

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use this inside &lt;strong&gt;Client Components&lt;/strong&gt; — anywhere you need &lt;code&gt;&quot;use client&quot;&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&quot;step-4-create-the-server-client&quot;&gt;Step 4: Create the Server Client&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/supabase/server.ts
import { createServerClient } from &quot;@supabase/ssr&quot;;
import { cookies } from &quot;next/headers&quot;;

export async function createClient() {
  const cookieStore = await cookies();

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll();
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =&amp;gt;
              cookieStore.set(name, value, options)
            );
          } catch {
            // Safe to ignore when called from a Server Component
          }
        },
      },
    }
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; this client reads and writes auth cookies through Next.js's &lt;code&gt;cookies()&lt;/code&gt; API, so a session created during login is visible to Server Components and Server Actions immediately — no manual token passing needed.&lt;/p&gt;
&lt;h4 id=&quot;step-5-add-the-auth-proxy-middleware-&quot;&gt;Step 5: Add the Auth Proxy (Middleware)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// utils/supabase/proxy.ts
import { createServerClient } from &quot;@supabase/ssr&quot;;
import { NextResponse, type NextRequest } from &quot;next/server&quot;;

export async function updateSession(request: NextRequest) {
  let supabaseResponse = NextResponse.next({ request });

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll();
        },
        setAll(cookiesToSet, headers) {
          cookiesToSet.forEach(({ name, value }) =&amp;gt;
            request.cookies.set(name, value)
          );
          supabaseResponse = NextResponse.next({ request });
          cookiesToSet.forEach(({ name, value, options }) =&amp;gt;
            supabaseResponse.cookies.set(name, value, options)
          );
          Object.entries(headers).forEach(([key, value]) =&amp;gt;
            supabaseResponse.headers.set(key, value)
          );
        },
      },
    }
  );

  await supabase.auth.getClaims();

  return supabaseResponse;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// proxy.ts (root of project)
import { type NextRequest } from &quot;next/server&quot;;
import { updateSession } from &quot;@/utils/supabase/proxy&quot;;

export async function proxy(request: NextRequest) {
  return await updateSession(request);
}

export const config = {
  matcher: [
    &quot;/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)&quot;,
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Skipping this proxy file. Without it, Supabase's refresh token doesn't get renewed on every request, and users get randomly logged out mid-session — one of the most common Supabase Auth bugs beginners hit.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;step-6-sign-in-with-email-and-password&quot;&gt;Step 6: Sign In With Email and Password&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// actions/auth.ts
&quot;use server&quot;;

import { redirect } from &quot;next/navigation&quot;;
import { createClient } from &quot;@/lib/supabase/server&quot;;

export async function login(prev: unknown, formData: FormData) {
  const supabase = await createClient();

  const { error } = await supabase.auth.signInWithPassword({
    email: formData.get(&quot;email&quot;) as string,
    password: formData.get(&quot;password&quot;) as string,
  });

  if (error) {
    return { error: error.message };
  }

  redirect(&quot;/dashboard&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;step-7-protect-data-with-row-level-security&quot;&gt;Step 7: Protect Data with Row Level Security&lt;/h4&gt;
&lt;p&gt;This is Supabase Auth's biggest advantage — security lives at the &lt;strong&gt;database level&lt;/strong&gt;, not just in your app code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;alter table profiles enable row level security;

create policy &quot;Users can view own profile&quot;
on profiles for select
using ((select auth.uid()) = id);

create policy &quot;Users can update own profile&quot;
on profiles for update
using ((select auth.uid()) = id)
with check ((select auth.uid()) = id);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; even if someone bypasses your Next.js app entirely and hits the Supabase API directly, RLS still blocks them from seeing data that isn't theirs. Your frontend code stops being the only line of defense.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 If you're using Supabase Auth with multiple tables, writing RLS policies manually can become repetitive. DevStacked's &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/tools/rls-policy-generator&quot;&gt;Supabase RLS Policy Generator&lt;/a&gt; can generate strict policies based on your access pattern.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;setting-up-firebase-auth-in-next-js-16&quot;&gt;Setting Up Firebase Auth in Next.js 16&lt;/h3&gt;
&lt;h4 id=&quot;step-1-install-dependencies-1&quot;&gt;Step 1: Install Dependencies&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install firebase firebase-admin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;firebase&lt;/code&gt; is the client SDK (runs in the browser and in Server Components via Firebase's newer server app support). &lt;code&gt;firebase-admin&lt;/code&gt; is for privileged, server-only operations like verifying tokens.&lt;/p&gt;
&lt;h4 id=&quot;step-2-add-environment-variables-1&quot;&gt;Step 2: Add Environment Variables&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-env&quot;&gt;# .env.local
NEXT_PUBLIC_FIREBASE_API_KEY=your_api_key
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=yourproject.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=yourproject
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;step-3-initialize-firebase-client-side-&quot;&gt;Step 3: Initialize Firebase (Client Side)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/firebase/client.ts
import { initializeApp, getApps, getApp } from &quot;firebase/app&quot;;
import { getAuth } from &quot;firebase/auth&quot;;

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
};

const app = getApps().length ? getApp() : initializeApp(firebaseConfig);

export const auth = getAuth(app);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;getApps().length ? getApp() : initializeApp(...)&lt;/code&gt; prevents Firebase from being re-initialized on every hot reload in development — a classic Firebase + Next.js gotcha if you skip this check.&lt;/p&gt;
&lt;h4 id=&quot;step-4-sign-in-with-email-and-password&quot;&gt;Step 4: Sign In With Email and Password&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// components/login-form.tsx
&quot;use client&quot;;

import { signInWithEmailAndPassword } from &quot;firebase/auth&quot;;
import { auth } from &quot;@/lib/firebase/client&quot;;
import { useRouter } from &quot;next/navigation&quot;;
import { useState } from &quot;react&quot;;

export function LoginForm() {
  const router = useRouter();
  const [error, setError] = useState(&quot;&quot;);

  const handleSubmit = async (e: React.FormEvent&amp;lt;HTMLFormElement&amp;gt;) =&amp;gt; {
    e.preventDefault();
    const form = new FormData(e.currentTarget);

    try {
      await signInWithEmailAndPassword(
        auth,
        form.get(&quot;email&quot;) as string,
        form.get(&quot;password&quot;) as string
      );
      router.push(&quot;/dashboard&quot;);
    } catch (err) {
      setError(err instanceof Error ? err.message : &quot;Login failed&quot;);
    }
  };

  return (
    &amp;lt;form onSubmit={handleSubmit}&amp;gt;
      &amp;lt;input name=&quot;email&quot; type=&quot;email&quot; required /&amp;gt;
      &amp;lt;input name=&quot;password&quot; type=&quot;password&quot; required /&amp;gt;
      &amp;lt;button type=&quot;submit&quot;&amp;gt;Sign in&amp;lt;/button&amp;gt;
      {error &amp;amp;&amp;amp; &amp;lt;p&amp;gt;{error}&amp;lt;/p&amp;gt;}
    &amp;lt;/form&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Doing all your auth logic client-side and assuming that's enough for SSR. Firebase's client SDK doesn't automatically make the logged-in user visible to your Next.js Server Components — you need a separate step to bridge that gap (next).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;step-5-bridge-the-session-to-the-server&quot;&gt;Step 5: Bridge the Session to the Server&lt;/h4&gt;
&lt;p&gt;Firebase's newer &lt;strong&gt;Firebase Server App&lt;/strong&gt; feature lets you read the authenticated user inside Server Components:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/firebase/server.ts
import { cookies } from &quot;next/headers&quot;;
import { initializeApp, initializeServerApp } from &quot;firebase/app&quot;;
import { getAuth } from &quot;firebase/auth&quot;;

export async function getAuthenticatedAppForUser() {
  const authIdToken = (await cookies()).get(&quot;__session&quot;)?.value;

  const firebaseServerApp = initializeServerApp(initializeApp(), {
    authIdToken,
  });

  const auth = getAuth(firebaseServerApp);
  await auth.authStateReady();

  return { firebaseServerApp, currentUser: auth.currentUser };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Learn more &lt;code&gt;Firebase Server App&lt;/code&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://firebase.google.com/codelabs/firebase-nextjs#5&quot;&gt;here&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; the client sets a &lt;code&gt;__session&lt;/code&gt; cookie containing the Firebase ID token after login (you write this cookie yourself, typically via a small API route or &lt;code&gt;onIdTokenChanged&lt;/code&gt; listener). The server then rebuilds an authenticated Firebase app instance from that cookie — this is meaningfully more manual than Supabase's &lt;code&gt;@supabase/ssr&lt;/code&gt; package, which handles cookie syncing for you out of the box.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This example demonstrates the server-side bridge rather than a complete production session-cookie implementation. In production, use a secure HTTP-only session cookie and validate it server-side.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Assuming Firebase Auth &quot;just works&quot; with Server Components the way it works with a client-only SPA. The App Router's server/client split means you need this extra bridging step — it's the single biggest source of Firebase + Next.js App Router complaints online.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;step-6-protect-data-with-firestore-security-rules&quot;&gt;Step 6: Protect Data with Firestore Security Rules&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /profiles/{userId} {
      allow read, update: if request.auth != null &amp;amp;&amp;amp; request.auth.uid == userId;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Firestore rules are enforced server-side by Google's infrastructure, similar in spirit to RLS — but written in a proprietary rules language instead of SQL, so there's a real learning curve if your team already knows SQL but not Firestore's rule syntax.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;pricing-comparison&quot;&gt;Pricing Comparison&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Supabase Auth &lt;/th&gt;&lt;th&gt;Firebase Auth&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Free tier &lt;/td&gt;&lt;td&gt;50,000 MAU &lt;/td&gt;&lt;td&gt;Depends on Firebase Auth / Identity Platform configuration&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Paid tier &lt;/td&gt;&lt;td&gt;Included in Supabase plan quota; additional MAU billed after quota &lt;/td&gt;&lt;td&gt;Usage-based pricing with Identity Platform&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Self-hosting &lt;/td&gt;&lt;td&gt;✅ Available &lt;/td&gt;&lt;td&gt;❌ Not available&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Both platforms offer generous options for early-stage projects, although the exact limits and pricing depend on the plan and services you use. The real cost difference shows up later: Supabase gives you a self-hosting escape hatch if you ever want to leave the MAU pricing model entirely; Firebase does not.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;security-considerations-for-both&quot;&gt;Security Considerations for Both&lt;/h3&gt;
&lt;p&gt;Regardless of which you choose, keep these in mind for Next.js 16:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Never trust client-side auth checks alone.&lt;/strong&gt; Whether you're using Supabase or Firebase, always re-verify the session inside Server Actions and Route Handlers — a hidden button in the UI doesn't stop a direct API call.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;proxy.ts&lt;/code&gt; (formerly &lt;code&gt;middleware.ts&lt;/code&gt;) is a UX nicety, not your security boundary.&lt;/strong&gt; Both Supabase's session refresh and any Firebase token check you add there can be bypassed by calling your backend directly — the real gate is the check inside your Server Action or database rule.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Default new users to the lowest-privilege role.&lt;/strong&gt; Never let a signup form set its own &lt;code&gt;role&lt;/code&gt; or &lt;code&gt;is_admin&lt;/code&gt; field — assign it server-side.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;when-should-you-use-supabase-auth-&quot;&gt;When Should You Use Supabase Auth?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;You want a real relational database (Postgres) under your app, not just auth.&lt;/li&gt;
&lt;li&gt;Your app has complex relationships — teams, roles, shared resources, multi-tenancy.&lt;/li&gt;
&lt;li&gt;You want the option to self-host later, or you like knowing you &lt;em&gt;could&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;You're already comfortable with SQL, or want to learn it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;when-should-you-use-firebase-auth-&quot;&gt;When Should You Use Firebase Auth?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;You're already using Firestore, Cloud Functions, Firebase Cloud Messaging, or Google Analytics.&lt;/li&gt;
&lt;li&gt;Your data model is simple and document-shaped (a user has a profile, a handful of settings — no deep relational joins).&lt;/li&gt;
&lt;li&gt;You want Google's massive scale and long production track record specifically for mobile + web apps together.&lt;/li&gt;
&lt;li&gt;Your team already knows Firestore Security Rules.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;decision-table&quot;&gt;Decision Table&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;If you need...              &lt;/th&gt;&lt;th&gt;Better choice&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Next.js + PostgreSQL        &lt;/td&gt;&lt;td&gt;🟢 Supabase&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;SQL + relational data       &lt;/td&gt;&lt;td&gt;🟢 Supabase&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;RLS-based authorization     &lt;/td&gt;&lt;td&gt;🟢 Supabase&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Self-hosting                &lt;/td&gt;&lt;td&gt;🟢 Supabase&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Firestore                   &lt;/td&gt;&lt;td&gt;🟢 Firebase&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Firebase ecosystem          &lt;/td&gt;&lt;td&gt;🟢 Firebase&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Google Cloud integration    &lt;/td&gt;&lt;td&gt;🟢 Firebase&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Mobile + web Firebase stack &lt;/td&gt;&lt;td&gt;🟢 Firebase&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Complex relational SaaS     &lt;/td&gt;&lt;td&gt;🟢 Supabase&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Document-oriented data      &lt;/td&gt;&lt;td&gt;🟢 Firebase&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;frequently-asked-questions-2&quot;&gt;Frequently Asked Questions&lt;/h3&gt;
&lt;h4 id=&quot;can-i-switch-from-firebase-auth-to-supabase-auth-later-&quot;&gt;Can I switch from Firebase Auth to Supabase Auth later?&lt;/h4&gt;
&lt;p&gt;Yes, but it's real migration work. You'll need to migrate users and identities, account for password-hash compatibility and provider configuration, and rebuild authorization rules from your Firebase/Security Rules model into Supabase RLS.&lt;/p&gt;
&lt;h4 id=&quot;which-one-is-easier-for-a-complete-beginner-&quot;&gt;Which one is easier for a complete beginner?&lt;/h4&gt;
&lt;p&gt;Firebase Auth's client-only setup (no SSR bridging) is slightly faster to get a basic login working. But Supabase Auth's &lt;code&gt;@supabase/ssr&lt;/code&gt; package makes Next.js App Router integration smoother once you add server-side checks, which almost every real app eventually needs.&lt;/p&gt;
&lt;h4 id=&quot;does-supabase-auth-support-social-login-like-google-and-github-&quot;&gt;Does Supabase Auth support social login like Google and GitHub?&lt;/h4&gt;
&lt;p&gt;Yes — Supabase Auth supports Google, GitHub, Apple, Discord, and dozens of other OAuth providers, configured directly from the Supabase dashboard with no extra backend code required.&lt;/p&gt;
&lt;h4 id=&quot;is-firebase-auth-open-source-&quot;&gt;Is Firebase Auth open source?&lt;/h4&gt;
&lt;p&gt;No. Firebase Auth is a fully managed Google Cloud service — you can't self-host it or inspect its internals. Supabase Auth (part of the broader Supabase platform, built on the open-source GoTrue project) is open source and can be self-hosted.&lt;/p&gt;
&lt;h4 id=&quot;do-i-need-row-level-security-if-i-m-already-checking-auth-in-my-next-js-code-&quot;&gt;Do I need Row Level Security if I'm already checking auth in my Next.js code?&lt;/h4&gt;
&lt;p&gt;RLS protects your database from application-level mistakes and direct client access, but it does not protect against misuse of a secret/service-role key because those elevated keys bypass RLS.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;wrapping-up-2&quot;&gt;Wrapping Up&lt;/h3&gt;
&lt;p&gt;Supabase Auth and Firebase Auth both get you a working login system fast — the real decision comes down to what's underneath: a relational Postgres database with SQL-based security, or Firestore's document model with Google's rules language. If your app has real relationships between users, teams, and data, Supabase's Postgres foundation will save you pain down the road. If you're already deep in the Google Cloud ecosystem, Firebase Auth keeps everything under one roof.&lt;/p&gt;
&lt;p&gt;Whichever you pick, remember the same rule applies to both: UI checks are for user experience, and server-side/database-level checks are your actual security boundary.&lt;/p&gt;
&lt;h3 id=&quot;continue-learning-2&quot;&gt;Continue Learning&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/role-based-authentication-nextjs-16&quot;&gt;Role-Based Access Control (RBAC) in Next.js 16 (2026 Beginner's Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/nextjs-16-supabase-todo-app&quot;&gt;Build a Todo App with Next.js 16 and Supabase (2026 Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/clerk-vs-nextauth-2026&quot;&gt;Clerk vs NextAuth (Auth.js) in 2026: Which One Should You Choose?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;helpful-free-tools&quot;&gt;Helpful Free Tools&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/tools/rls-policy-generator&quot;&gt;Supabase RLS Policy Generator&lt;/a&gt; — generate strict, pattern-specific Row Level Security policies in seconds instead of hand-writing SQL.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/tools/zod-schema-generator&quot;&gt;Zod Schema Generator&lt;/a&gt; — generate validated schemas for your auth forms straight from JSON or TypeScript, including a ready-to-use Server Action.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24980/supabase-auth-vs-firebase-auth-which-one-should-you-choose-in-2026</guid>
<pubDate>Thu, 20 Aug 2026 10:26:22 +0000</pubDate>
</item>
<item>
<title>Role-Based Access Control (RBAC) in Next.js 16 (2026 Beginner's Guide)</title>
<link>https://coderlegion.com/24809/role-based-access-control-rbac-in-next-js-16-2026-beginners-guide</link>
<description>&lt;p&gt;Logging a user in is the easy part. The hard part shows up right after — when your &quot;admin&quot; and your &quot;customer&quot; both hit &lt;code&gt;/dashboard&lt;/code&gt; and somehow need to see completely different things. Or worse, when a regular user figures out they can just type &lt;code&gt;/admin&lt;/code&gt; into the URL bar and… get in, because nothing was actually checking who they were.&lt;/p&gt;
&lt;p&gt;That's the gap &lt;strong&gt;role-based authorization&lt;/strong&gt; (RBAC — Role-Based Access Control) fills. It's not about &lt;em&gt;whether&lt;/em&gt; someone is logged in anymore — it's about &lt;em&gt;what they're allowed to do&lt;/em&gt; once they are. An admin can delete users. An editor can publish posts. A viewer can only look. Same login system, three completely different sets of permissions.&lt;/p&gt;
&lt;p&gt;In this guide, you'll build a complete role-based authorization system in Next.js 16 — from storing roles, to protecting routes with &lt;code&gt;proxy.ts&lt;/code&gt;, to locking down Server Actions and API routes so a sneaky &lt;code&gt;curl&lt;/code&gt; request can't bypass your UI checks. By the end, you'll have a reusable pattern you can drop into any SaaS, dashboard, or internal tool.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-this-is-tricky-1&quot;&gt;Why This Is Tricky&lt;/h3&gt;
&lt;p&gt;Authentication just answers &quot;who are you?&quot; Authorization answers &quot;what are you allowed to do?&quot; — and that second question has to be checked in &lt;strong&gt;every place&lt;/strong&gt; data or actions are exposed, not just once at the login screen.&lt;/p&gt;
&lt;p&gt;The mistake almost everyone makes early on is checking roles only in the UI — hiding an &quot;Edit&quot; button if &lt;code&gt;role !== &quot;admin&quot;&lt;/code&gt;. That stops confused users, but it does nothing to stop a determined one, since a hidden button doesn't stop someone from calling the underlying Server Action or API route directly. Real role-based auth needs checks in at least three layers:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Route level&lt;/strong&gt; — don't even let the page load for the wrong role&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UI level&lt;/strong&gt; — hide actions the user can't perform (good UX, not security)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server level&lt;/strong&gt; — re-verify inside every Server Action, Route Handler, and database query (the actual security boundary)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We'll build all three.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-we-re-building&quot;&gt;What We're Building&lt;/h3&gt;
&lt;p&gt;A small app with three roles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;admin&lt;/strong&gt; — full access, can manage users&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;editor&lt;/strong&gt; — can create and edit posts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;viewer&lt;/strong&gt; — read-only access&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You'll end up with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A typed &lt;code&gt;Role&lt;/code&gt; system shared across the whole app&lt;/li&gt;
&lt;li&gt;Role storage using Auth.js v5's JWT session (works the same pattern with a database session)&lt;/li&gt;
&lt;li&gt;Route protection in &lt;code&gt;proxy.ts&lt;/code&gt; based on role, not just login state&lt;/li&gt;
&lt;li&gt;A reusable &lt;code&gt;requirePermission()&lt;/code&gt; helper for Server Actions and Route Handlers&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;&amp;lt;Can&amp;gt;&lt;/code&gt; component for conditionally showing UI based on permissions&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;                 ┌──────────────┐
                 │    Role      │
                 │ admin/editor │
                 │    viewer    │
                 └──────┬───────┘
                        ↓
                 ┌──────────────┐
                 │ Permissions  │
                 └──────┬───────┘
                        ↓
              ┌─────────┴─────────┐
              ↓                   ↓
      requirePermission()       &amp;lt;Can&amp;gt;
              ↓                   ↓
       Security boundary      UI visibility
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;tech-stack&quot;&gt;Tech Stack&lt;/h4&gt;
&lt;p&gt;This guide uses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Next.js 16&lt;/strong&gt; with the App Router&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TypeScript&lt;/strong&gt; in strict mode&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tailwind CSS&lt;/strong&gt; for the small UI examples&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auth.js v5&lt;/strong&gt; (&lt;code&gt;next-auth@beta&lt;/code&gt;) for authentication — the same library covered in &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/clerk-vs-nextauth-2026&quot;&gt;Clerk vs NextAuth (Auth.js) in 2026&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Don't have Auth.js set up yet? Read &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/how-to-add-nextauth-in-nextjs-16&quot;&gt;How to Add Auth.js (NextAuth v5) Authentication in Next.js 16&lt;/a&gt; first — this guide picks up right where that one leaves off. If you're using &lt;strong&gt;Clerk&lt;/strong&gt; instead, skip to the &lt;a rel=&quot;nofollow&quot; href=&quot;#doing-this-with-clerk-instead&quot;&gt;Clerk section&lt;/a&gt; near the end — the concepts are identical, just the plumbing changes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-1-define-your-roles-in-one-place&quot;&gt;Step 1: Define Your Roles in One Place&lt;/h3&gt;
&lt;p&gt;Before writing any auth code, decide what roles exist and what each one can actually do. Hardcoding role strings (&quot;admin&quot;, &quot;editor&quot;...) all over your codebase is exactly how typos slip in and silently break a permission check.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/auth/roles.ts
export const ROLES = [&quot;admin&quot;, &quot;editor&quot;, &quot;viewer&quot;] as const;

export type Role = (typeof ROLES)[number];

// What each role is allowed to do — the single source of truth
export const PERMISSIONS = {
  admin: [&quot;manage_users&quot;, &quot;create_post&quot;, &quot;edit_post&quot;, &quot;delete_post&quot;, &quot;view_post&quot;],
  editor: [&quot;create_post&quot;, &quot;edit_post&quot;, &quot;view_post&quot;],
  viewer: [&quot;view_post&quot;],
} as const satisfies Record&amp;lt;Role, readonly string[]&amp;gt;;

export type Permission = (typeof PERMISSIONS)[Role][number];

export function hasPermission(role: Role, permission: Permission): boolean {
  return (PERMISSIONS[role] as readonly string[]).includes(permission);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;ROLES&lt;/code&gt; is a fixed list — TypeScript's &lt;code&gt;as const&lt;/code&gt; makes &lt;code&gt;Role&lt;/code&gt; a union type (&lt;code&gt;&quot;admin&quot; | &quot;editor&quot; | &quot;viewer&quot;&lt;/code&gt;) instead of a loose &lt;code&gt;string&lt;/code&gt;, so a typo like &lt;code&gt;&quot;admn&quot;&lt;/code&gt; fails to compile instead of silently failing at runtime. &lt;code&gt;PERMISSIONS&lt;/code&gt; maps each role to what it's allowed to do, and &lt;code&gt;hasPermission()&lt;/code&gt; is the one function every check in this guide will eventually call.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Notice we're checking &lt;strong&gt;permissions&lt;/strong&gt; (&lt;code&gt;&quot;create_post&quot;&lt;/code&gt;), not roles (&lt;code&gt;&quot;editor&quot;&lt;/code&gt;), everywhere except this one file. If you ever add a new role or change what editors can do, you update this file once — nothing else in your app needs to change.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-2-store-the-role-on-the-user&quot;&gt;Step 2: Store the Role on the User&lt;/h3&gt;
&lt;p&gt;Your role has to live somewhere Auth.js can read it during login. In a real app, this is a &lt;code&gt;role&lt;/code&gt; column on your &lt;code&gt;users&lt;/code&gt; table. For this guide, here's the shape assuming you're using a database adapter (Prisma, Drizzle, or similar):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Minimal schema addition
alter table users
add column role text not null default 'viewer'
check (role in ('admin', 'editor', 'viewer'));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;New users default to the lowest-privilege role — &lt;code&gt;viewer&lt;/code&gt; — which is a good default for any RBAC system. Never default a new signup to &lt;code&gt;admin&lt;/code&gt;. &lt;/p&gt;
&lt;p&gt;TypeScript protects application code at compile time; the database constraint protects persisted data at runtime.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Letting the client send its own role during signup (&lt;code&gt;formData.get(&quot;role&quot;)&lt;/code&gt;). Always assign the default role server-side and require an existing admin to promote anyone — otherwise anyone can sign up and just declare themselves an admin.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-3-put-the-role-on-the-session&quot;&gt;Step 3: Put the Role on the Session&lt;/h3&gt;
&lt;p&gt;This is the step most tutorials skip, and it's the one that actually makes RBAC work. By default, Auth.js's session only contains the basics (name, email, image) — you have to explicitly add &lt;code&gt;role&lt;/code&gt; to it using two callbacks: &lt;code&gt;jwt&lt;/code&gt; and &lt;code&gt;session&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/auth.ts
import NextAuth from &quot;next-auth&quot;;
import GitHub from &quot;next-auth/providers/github&quot;;
import type { Role } from &quot;./auth/roles&quot;;

declare module &quot;next-auth&quot; {
  interface User {
    id: string;
    role: Role;
  }
  interface Session {
    user: {
      id: string;
      role: Role;
    } &amp;amp; DefaultSession[&quot;user&quot;];
  }
}

declare module &quot;@auth/core/jwt&quot; {
  interface JWT {
    role: Role;
    id: string;
  }
}

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [GitHub],
  callbacks: {
    async jwt({ token, user }) {
      // Runs on sign-in, and on every subsequent request
      if (user) {
        // `user` is only defined right after login — look up the role from your DB here
        token.id = user.id!;
        token.role = user.role ?? &quot;viewer&quot;;
      }
      return token;
    },
    async session({ session, token }) {
      // Copies data from the JWT token into the session object your app actually reads
      session.user.id = token.id;
      session.user.role = token.role;
      return session;
    },
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;jwt()&lt;/code&gt; can run whenever the JWT session is created or updated, but the &lt;code&gt;user&lt;/code&gt; object is available during the initial sign-in flow. That's where you copy the user's role into &lt;code&gt;token.role&lt;/code&gt;. On later requests, the role is read from the token rather than fetched again. &lt;code&gt;session()&lt;/code&gt; then runs on every &lt;code&gt;auth()&lt;/code&gt; call and copies fields from that token onto the &lt;code&gt;session&lt;/code&gt; object your pages and Server Actions actually read. The &lt;code&gt;declare module&lt;/code&gt; blocks aren't runtime code — they extend Auth.js's built-in TypeScript types so &lt;code&gt;session.user.role&lt;/code&gt; autocompletes instead of throwing a type error.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Trying to read the role directly off &lt;code&gt;session.user&lt;/code&gt; without adding it in these two callbacks first. Auth.js won't magically know about a custom &lt;code&gt;role&lt;/code&gt; column — you have to wire it through &lt;code&gt;jwt()&lt;/code&gt; → &lt;code&gt;session()&lt;/code&gt; explicitly, every time.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; If a user's role changes (e.g. an admin promotes them), the JWT won't reflect that until they log in again, since &lt;code&gt;jwt()&lt;/code&gt; only re-reads &lt;code&gt;user&lt;/code&gt; on fresh sign-in. For roles that need to update instantly, either switch to database sessions (&lt;code&gt;strategy: &quot;database&quot;&lt;/code&gt;) or add a &lt;code&gt;trigger === &quot;update&quot;&lt;/code&gt; branch that re-fetches the role on demand.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-4-protect-routes-by-role-in-proxy-ts&quot;&gt;Step 4: Protect Routes by Role in &lt;code&gt;proxy.ts&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;In Next.js 16, &lt;code&gt;middleware.ts&lt;/code&gt; was renamed to &lt;code&gt;proxy.ts&lt;/code&gt; — same file conventions, same idea: it runs before a request reaches a page, making it useful for a fast, optimistic route-level redirect before the page renders.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// proxy.ts
import { NextResponse } from &quot;next/server&quot;;
import { auth } from &quot;@/lib/auth&quot;;
import type { Role } from &quot;@/lib/auth/roles&quot;;

// Map route prefixes to the roles allowed to access them
const ROUTE_ROLES: { prefix: string; roles: Role[] }[] = [
  { prefix: &quot;/admin&quot;, roles: [&quot;admin&quot;] },
  { prefix: &quot;/editor&quot;, roles: [&quot;admin&quot;, &quot;editor&quot;] },
  { prefix: &quot;/dashboard&quot;, roles: [&quot;admin&quot;, &quot;editor&quot;, &quot;viewer&quot;] },
];

export default auth((req) =&amp;gt; {
  const { nextUrl } = req;
  const role = req.auth?.user?.role;

  const matched = ROUTE_ROLES.find((r) =&amp;gt; nextUrl.pathname === r.prefix || nextUrl.pathname.startsWith(`${r.prefix}/`));
  const isAuthPage = nextUrl.pathname === &quot;/login&quot;;
  const isLoggedIn = !!req.auth;

  if (isAuthPage &amp;amp;&amp;amp; isLoggedIn) {
      // Redirect authenticated users to the dashboard
      return NextResponse.redirect(new URL(&quot;/dashboard&quot;, nextUrl))
  }

  if (!matched) {
    return NextResponse.next(); // route isn't role-protected, let it through
  }

  if (!isLoggedIn) {
    // Not logged in at all — send to login
    return NextResponse.redirect(new URL(&quot;/login&quot;, nextUrl));
  }

  if (!role || !matched.roles.includes(role)) {
    // Logged in, but wrong role — send to a friendly &quot;not allowed&quot; page, not login
    return NextResponse.redirect(new URL(&quot;/unauthorized&quot;, nextUrl));
  }

  return NextResponse.next();
});

export const config = {
  matcher: [&quot;/((?!api|_next/static|_next/image|favicon.ico).*)&quot;],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;ROUTE_ROLES&lt;/code&gt; is a lookup table — no scattered &lt;code&gt;if&lt;/code&gt; statements per route, just prefixes mapped to the roles allowed in. The proxy checks the incoming path against that table, and only bothers checking &lt;code&gt;req.auth&lt;/code&gt; at all if the route is actually protected. Notice the two different redirects: no session → &lt;code&gt;/login&lt;/code&gt; (they need to authenticate), wrong role → &lt;code&gt;/unauthorized&lt;/code&gt; (they're authenticated, just not allowed here) — conflating these two cases is a common source of confusing UX.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Forgetting the &lt;code&gt;matcher&lt;/code&gt; config, or matching too broadly. Running the proxy on every static asset request adds unnecessary overhead and can cause odd caching behavior — always exclude &lt;code&gt;_next/static&lt;/code&gt;, &lt;code&gt;_next/image&lt;/code&gt;, and favicon paths.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-5-build-a-requirepermission-helper-for-the-real-security-boundary&quot;&gt;Step 5: Build a &lt;code&gt;requirePermission()&lt;/code&gt; Helper for the Real Security Boundary&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;proxy.ts&lt;/code&gt; stops the page from loading, but it does &lt;strong&gt;not&lt;/strong&gt; protect Server Actions or Route Handlers called directly — those need their own check. This is the layer that actually matters for security, since &lt;code&gt;proxy.ts&lt;/code&gt; is a UX nicety, not a guarantee.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/auth/require-role.ts
import { auth } from &quot;@/lib/auth&quot;;
import type { Permission } from &quot;./roles&quot;;
import { hasPermission } from &quot;./roles&quot;;

export class UnauthorizedError extends Error {}
export class ForbiddenError extends Error {}

export async function requirePermission(permission: Permission) {
  const session = await auth();

  if (!session?.user) {
    throw new UnauthorizedError(&quot;You must be signed in.&quot;);
  }

  if (!hasPermission(session.user.role, permission)) {
    throw new ForbiddenError(&quot;You don't have permission to do that.&quot;);
  }

  return session.user; // callers get a typed, guaranteed-authorized user back
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;requirePermission()&lt;/code&gt; checks permissions (&lt;code&gt;&quot;delete_post&quot;&lt;/code&gt;), not roles directly — this keeps every call site decoupled from &lt;em&gt;which&lt;/em&gt; roles can do what, so changing that mapping later only touches &lt;code&gt;roles.ts&lt;/code&gt;. Two distinct errors (&lt;code&gt;Unauthorized&lt;/code&gt; vs &lt;code&gt;Forbidden&lt;/code&gt;) let callers respond differently — a 401 (not logged in) is a different problem than a 403 (logged in, not allowed).&lt;/p&gt;
&lt;h4 id=&quot;using-it-in-a-server-action&quot;&gt;Using It in a Server Action&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// actions/posts.ts
&quot;use server&quot;;

import { requirePermission, ForbiddenError, UnauthorizedError } from &quot;@/lib/auth/require-role&quot;;
import { revalidatePath } from &quot;next/cache&quot;;

export async function deletePost(postId: string) {
  try {
    await requirePermission(&quot;delete_post&quot;);
  } catch (err) {
    if (err instanceof UnauthorizedError) return { error: &quot;Please sign in.&quot; };
    if (err instanceof ForbiddenError) return { error: &quot;You can't delete posts.&quot; };
    throw err;
  }

  // await db.post.delete({ where: { id: postId } });
  revalidatePath(&quot;/dashboard&quot;);
  return { success: true };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Only checking permissions in &lt;code&gt;proxy.ts&lt;/code&gt; and assuming Server Actions are automatically covered. They aren't — a Server Action is a real network endpoint under the hood, callable directly, and needs its own &lt;code&gt;requirePermission()&lt;/code&gt; call every single time it touches something sensitive.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;using-it-in-a-route-handler&quot;&gt;Using It in a Route Handler&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// app/api/users/[id]/route.ts
import { NextResponse } from &quot;next/server&quot;;
import { requirePermission, ForbiddenError, UnauthorizedError } from &quot;@/lib/auth/require-role&quot;;

export async function DELETE(req: Request, { params }: { params: Promise&amp;lt;{ id: string }&amp;gt; }) {
  try {
    await requirePermission(&quot;manage_users&quot;);
  } catch (err) {
    if (err instanceof UnauthorizedError) {
      return NextResponse.json({ error: &quot;Unauthorized&quot; }, { status: 401 });
    }
    if (err instanceof ForbiddenError) {
      return NextResponse.json({ error: &quot;Forbidden&quot; }, { status: 403 });
    }
    throw err;
  }

  const { id } = await params;
  // await db.user.delete({ where: { id } });
  return NextResponse.json({ success: true });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same pattern, correct HTTP status codes this time — &lt;code&gt;401&lt;/code&gt; for &quot;log in first,&quot; &lt;code&gt;403&lt;/code&gt; for &quot;you're logged in, but no.&quot;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-6-conditionally-show-ui-with-a-lt-can-gt-component&quot;&gt;Step 6: Conditionally Show UI with a &lt;code&gt;&amp;lt;Can&amp;gt;&lt;/code&gt; Component&lt;/h3&gt;
&lt;p&gt;Server-side checks are the real security boundary, but users still need a UI that doesn't dangle buttons they can't actually use. A small &lt;code&gt;&amp;lt;Can&amp;gt;&lt;/code&gt; component keeps this readable instead of &lt;code&gt;{role === &quot;admin&quot; &amp;amp;&amp;amp; ...}&lt;/code&gt; scattered through every page.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// components/can.tsx
import type { Permission } from &quot;@/lib/auth/roles&quot;;
import { hasPermission } from &quot;@/lib/auth/roles&quot;;
import { auth } from &quot;@/lib/auth&quot;;

interface CanProps {
  permission: Permission;
  children: React.ReactNode;
  fallback?: React.ReactNode;
}

export async function Can({ permission, children, fallback = null }: CanProps) {
  const session = await auth();

  if (!session?.user || !hasPermission(session.user.role, permission)) {
    return &amp;lt;&amp;gt;{fallback}&amp;lt;/&amp;gt;;
  }

  return &amp;lt;&amp;gt;{children}&amp;lt;/&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/dashboard/page.tsx
import { Can } from &quot;@/components/can&quot;;
import Link from 'next/link';

export default function DashboardPage() {
  return (
    &amp;lt;div className=&quot;space-y-4&quot;&amp;gt;
      &amp;lt;h1 className=&quot;text-2xl font-semibold&quot;&amp;gt;Dashboard&amp;lt;/h1&amp;gt;

      &amp;lt;Can permission=&quot;create_post&quot;&amp;gt;
        &amp;lt;button className=&quot;rounded-lg bg-primary px-4 py-2 text-white&quot;&amp;gt;
          New Post
        &amp;lt;/button&amp;gt;
      &amp;lt;/Can&amp;gt;

      &amp;lt;Can permission=&quot;manage_users&quot; fallback={&amp;lt;p className=&quot;text-sm text-muted-foreground&quot;&amp;gt;Admins only.&amp;lt;/p&amp;gt;}&amp;gt;
        &amp;lt;Link href=&quot;/admin/users&quot; className=&quot;underline&quot;&amp;gt;Manage Users&amp;lt;/Link&amp;gt;
      &amp;lt;/Can&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;Can&lt;/code&gt; is an async Server Component — it calls &lt;code&gt;auth()&lt;/code&gt; directly, no client-side loading flicker, no &lt;code&gt;useEffect&lt;/code&gt;. Wrap anything role-gated in it, optionally supply a &lt;code&gt;fallback&lt;/code&gt; for what non-permitted users see instead (or leave it out to render nothing at all).&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Because &lt;code&gt;Can&lt;/code&gt; reads the current session on the server, use it in parts of the tree where request-time authentication data is expected.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; This component controls &lt;em&gt;visibility&lt;/em&gt;, not &lt;em&gt;security&lt;/em&gt;. Someone could still call the underlying Server Action with dev tools open — that's exactly why Step 5's &lt;code&gt;requirePermission()&lt;/code&gt; exists as the real gate. &lt;code&gt;&amp;lt;Can&amp;gt;&lt;/code&gt; is UX, &lt;code&gt;requirePermission()&lt;/code&gt; is the lock.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-7-filter-data-at-the-query-level-too&quot;&gt;Step 7: Filter Data at the Query Level Too&lt;/h3&gt;
&lt;p&gt;There's a fourth layer worth calling out separately: what data a query even returns. A &lt;code&gt;viewer&lt;/code&gt; shouldn't just be blocked from an edit &lt;em&gt;button&lt;/em&gt; — if your query returns draft posts by default, make sure a viewer's query never fetches drafts in the first place, rather than fetching everything and hiding it in the UI.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/data/posts.ts
import { auth } from &quot;@/lib/auth&quot;;
import { UnauthorizedError } from &quot;../auth/require-role&quot;;

export async function getVisiblePosts() {
  const session = await auth();
  const role = session?.user?.role;

  if (!session?.user) {
    throw new UnauthorizedError(&quot;You must be signed in.&quot;);
  }

  if (role === &quot;admin&quot; || role === &quot;editor&quot;) {
    // return await db.post.findMany(); // everything, including drafts
  }

  // return await db.post.findMany({ where: { status: &quot;published&quot; } }); // viewers only see published
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This matters because a UI-only check can miss cases like server-rendered pages, RSS feeds, or an API response that isn't wired through the component you added &lt;code&gt;&amp;lt;Can&amp;gt;&lt;/code&gt; to.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;frequently-asked-questions-3&quot;&gt;Frequently Asked Questions&lt;/h3&gt;
&lt;h4 id=&quot;what-s-the-difference-between-authentication-and-authorization-&quot;&gt;What's the difference between authentication and authorization?&lt;/h4&gt;
&lt;p&gt;Authentication confirms &lt;em&gt;who&lt;/em&gt; someone is (logging in). Authorization decides &lt;em&gt;what&lt;/em&gt; they're allowed to do once they're recognized (roles and permissions). You need both — Auth.js/Clerk handle authentication, and everything in this guide is the authorization layer on top.&lt;/p&gt;
&lt;h4 id=&quot;should-i-store-roles-as-a-string-or-a-separate-roles-table-&quot;&gt;Should I store roles as a string or a separate roles table?&lt;/h4&gt;
&lt;p&gt;A single &lt;code&gt;role&lt;/code&gt; string column (like this guide uses) is fine for simple apps with a handful of fixed roles. If you need multiple roles per user, per-organization roles, or roles that admins can create dynamically, move to a proper &lt;code&gt;roles&lt;/code&gt; and &lt;code&gt;user_roles&lt;/code&gt; join table instead — the &lt;code&gt;hasPermission()&lt;/code&gt; pattern still works the same way on top of either.&lt;/p&gt;
&lt;h4 id=&quot;is-checking-roles-in-proxy-ts-enough-security-by-itself-&quot;&gt;Is checking roles in &lt;code&gt;proxy.ts&lt;/code&gt; enough security by itself?&lt;/h4&gt;
&lt;p&gt;No. Treat &lt;code&gt;proxy.ts&lt;/code&gt; as a fast UX redirect, not your security boundary — it can be bypassed by hitting a Server Action or API route directly. Always re-check permissions inside the Server Action or Route Handler itself, as shown in Step 5.&lt;/p&gt;
&lt;h4 id=&quot;how-do-i-handle-a-role-change-without-forcing-the-user-to-log-out-&quot;&gt;How do I handle a role change without forcing the user to log out?&lt;/h4&gt;
&lt;p&gt;With JWT sessions, the user value isn't available on every JWT callback invocation, so simply changing the role in your database doesn't automatically replace the role already stored in an existing token. Either switch to &lt;code&gt;strategy: &quot;database&quot;&lt;/code&gt; sessions (Auth.js re-reads the DB on every request), or add an &lt;code&gt;update()&lt;/code&gt; trigger that refreshes the token when an admin changes someone's role.&lt;/p&gt;
&lt;h4 id=&quot;can-i-have-more-granular-permissions-than-just-three-roles-&quot;&gt;Can I have more granular permissions than just three roles?&lt;/h4&gt;
&lt;p&gt;Yes — that's exactly why Step 1 separates &lt;code&gt;PERMISSIONS&lt;/code&gt; from &lt;code&gt;ROLES&lt;/code&gt;. Add a new permission string to whichever roles should have it, and every &lt;code&gt;requirePermission()&lt;/code&gt; call automatically respects the new mapping without touching the rest of your codebase.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;doing-this-with-clerk-instead&quot;&gt;Doing This with Clerk Instead&lt;/h2&gt;
&lt;p&gt;If you're using &lt;a rel=&quot;nofollow&quot; href=&quot;https://clerk.com/&quot;&gt;Clerk&lt;/a&gt; instead of Auth.js, the RBAC architecture stays the same. Only the way you retrieve the user's role changes.&lt;/p&gt;
&lt;p&gt;For a simple app, store the role in Clerk's &lt;code&gt;publicMetadata&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;{
  role: &quot;editor&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Set it server-side or through the Clerk Dashboard. &lt;strong&gt;Never let users submit their own role during signup.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Expose the metadata through the Clerk session token. In &lt;strong&gt;Clerk Dashboard → Sessions → Customize session token&lt;/strong&gt;, add:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;metadata&quot;: &quot;{{user.public_metadata}}&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then read the role with &lt;code&gt;auth()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { auth } from &quot;@clerk/nextjs/server&quot;;
import type { Role } from &quot;@/lib/auth/roles&quot;;

const { isAuthenticated, sessionClaims } = await auth();

if (!isAuthenticated) {
  // Not signed in
}

const role = sessionClaims?.metadata?.role as Role | undefined;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From here, the same &lt;code&gt;hasPermission()&lt;/code&gt;, &lt;code&gt;requirePermission()&lt;/code&gt;, and &lt;code&gt;&amp;lt;Can&amp;gt;&lt;/code&gt; pattern from this guide still applies. Your authorization layer doesn't need to care whether the role came from Auth.js or Clerk.&lt;/p&gt;
&lt;p&gt;Role changes may take a short time to appear in an existing session token, so don't assume a metadata update is immediately reflected in &lt;code&gt;sessionClaims&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; If you're building a multi-tenant SaaS with teams or organizations, consider Clerk's built-in &lt;strong&gt;Organizations, Roles, and Permissions&lt;/strong&gt; instead of storing organization roles in &lt;code&gt;publicMetadata&lt;/code&gt;. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://clerk.com/docs/guides/organizations/control-access/roles-and-permissions&quot;&gt;Clerk Roles &amp;amp; Permissions&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The important rule remains the same: &lt;strong&gt;UI checks improve UX; server-side permission checks provide security.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;wrapping-up-3&quot;&gt;Wrapping Up&lt;/h3&gt;
&lt;p&gt;Role-based authorization isn't one big feature — it's four small, boring checks layered together: a protected route in &lt;code&gt;proxy.ts&lt;/code&gt;, a re-check inside every Server Action and Route Handler, a &lt;code&gt;&amp;lt;Can&amp;gt;&lt;/code&gt; wrapper for the UI, and a query that filters data before it ever leaves the server. Skip any one layer and you've got a gap; stack all four and a &quot;hidden&quot; admin button stops being a real vulnerability.&lt;/p&gt;
&lt;p&gt;From here, a natural next step is adding an admin UI for promoting/demoting users, or layering row-level security on top if you're using &lt;a rel=&quot;nofollow&quot; href=&quot;https://supabase.com/&quot;&gt;Supabase&lt;/a&gt; alongside this pattern for defense in depth at the database layer too.&lt;/p&gt;
&lt;p&gt;📦 &lt;strong&gt;Source Code:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Muhammad-Ali-sma/Role-base-Authorization-Nextjs-setup&quot;&gt;View on GitHub&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;continue-learning-3&quot;&gt;Continue Learning&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/how-to-add-nextauth-in-nextjs-16&quot;&gt;How to Add Auth.js (NextAuth v5) Authentication in Next.js 16 (2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/how-to-add-clerk-authentication-in-nextjs-16&quot;&gt;How to Add Clerk Authentication in Next.js 16 (2026 Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/clerk-vs-nextauth-2026&quot;&gt;Clerk vs NextAuth (Auth.js) in 2026: Which One Should You Choose?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/nextjs-16-supabase-todo-app&quot;&gt;Build a Todo App with Next.js 16 and Supabase (2026 Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/saas-starter-architecture-nextjs&quot;&gt;SaaS Starter Architecture with Next.js 2026&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;free-developer-tools-1&quot;&gt;Free Developer Tools&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/tools/rls-policy-generator&quot;&gt;Supabase RLS Policy Generator&lt;/a&gt; — pair this guide's application-level RBAC with database-level Row Level Security. Generates owner-only, team, and admin-override policies with the &lt;code&gt;auth.uid()&lt;/code&gt; performance wrapper built in.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/tools/zod-schema-generator&quot;&gt;Zod Schema Generator&lt;/a&gt; — generate a validated schema for your role/permission payloads straight from a JSON sample or TypeScript interface, including a ready-to-use Server Action.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24809/role-based-access-control-rbac-in-next-js-16-2026-beginners-guide</guid>
<pubDate>Tue, 18 Aug 2026 03:49:46 +0000</pubDate>
</item>
<item>
<title>How to Add Auth.js (NextAuth v5) Authentication in Next.js 16 (2026)</title>
<link>https://coderlegion.com/24071/how-to-add-auth-js-nextauth-v5-authentication-in-next-js-16-2026</link>
<description>&lt;p&gt;Every real app eventually needs to answer one question: &lt;em&gt;who is this user?&lt;/em&gt; The moment you add a dashboard, a settings page, or anything that shouldn't be public, you need authentication — and rolling your own login system from scratch (password hashing, sessions, CSRF protection, OAuth flows) is a rabbit hole most developers don't have time for.&lt;/p&gt;
&lt;p&gt;That's exactly what &lt;strong&gt;Auth.js&lt;/strong&gt; (the project formerly known as NextAuth) solves. It's free, open source, and gives you production-ready authentication — email/password, GitHub, Google, and dozens of other providers — without owning any of that security-critical code yourself.&lt;/p&gt;
&lt;p&gt;In this guide, you'll set up Auth.js v5 in a Next.js 16 App Router project from scratch: installing it, configuring a provider, protecting routes with &lt;code&gt;proxy.ts&lt;/code&gt;, and reading the logged-in user inside Server Components. By the end, you'll have a working &quot;Sign in with GitHub&quot; flow you can extend to any provider you want.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-you-ll-learn&quot;&gt;What you'll learn&lt;/h3&gt;
&lt;p&gt;By the end of this guide you'll know how to:&lt;/p&gt;
&lt;p&gt;✓ Install Auth.js v5&lt;br&gt;
✓ Configure GitHub OAuth&lt;br&gt;
✓ Protect routes&lt;br&gt;
✓ Read sessions&lt;br&gt;
✓ Use auth() inside Server Components&lt;br&gt;
✓ Secure Server Actions&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-auth-js-exactly-&quot;&gt;What Is Auth.js, Exactly?&lt;/h3&gt;
&lt;p&gt;Auth.js is an authentication library built specifically for full-stack JavaScript frameworks. If you've read older tutorials, you've probably seen it called &lt;strong&gt;NextAuth&lt;/strong&gt; — that's the same project. It was renamed to Auth.js when the team expanded support beyond Next.js to other frameworks like SvelteKit and Express, while the npm package name stayed &lt;code&gt;next-auth&lt;/code&gt; for backwards compatibility.&lt;/p&gt;
&lt;p&gt;Think of Auth.js as a &lt;strong&gt;middleman&lt;/strong&gt; between your app and the identity providers (GitHub, Google, your own database, etc.). Instead of you writing the OAuth handshake, generating secure session cookies, and validating tokens by hand, Auth.js handles all of it — you just tell it &lt;em&gt;which&lt;/em&gt; providers to use.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; If you've seen &lt;code&gt;getServerSession()&lt;/code&gt; in an older tutorial, that's the Auth.js v4 API. In v5 (what this guide uses), everything is exported from a single &lt;code&gt;auth()&lt;/code&gt; function instead — much simpler.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-we-re-building-1&quot;&gt;What We're Building&lt;/h3&gt;
&lt;p&gt;By the end of this guide, you'll have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Auth.js v5 installed and configured in a Next.js 16 App Router app&lt;/li&gt;
&lt;li&gt;A working &lt;strong&gt;GitHub OAuth&lt;/strong&gt; sign-in flow (the pattern is the same for Google, Discord, etc.)&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;proxy.ts&lt;/code&gt; file that protects private routes automatically&lt;/li&gt;
&lt;li&gt;A Navbar that shows &quot;Sign in&quot; or the user's avatar depending on auth state&lt;/li&gt;
&lt;li&gt;Access to the logged-in user inside Server Components using &lt;code&gt;auth()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;prerequisites-1&quot;&gt;Prerequisites&lt;/h4&gt;
&lt;p&gt;Before we start, make sure you have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Next.js 16&lt;/strong&gt; with the App Router&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TypeScript&lt;/strong&gt; (strict mode)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tailwind CSS&lt;/strong&gt; for styling&lt;/li&gt;
&lt;li&gt;Basic familiarity with React and the App Router&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your project doesn't have these yet, scaffold one first:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx create-next-app@latest my-app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For this guide, we're using the &lt;code&gt;src/&lt;/code&gt; directory layout (choose &lt;strong&gt;Yes&lt;/strong&gt; when the CLI asks). Everything works the same without it — just drop the &lt;code&gt;src/&lt;/code&gt; prefix from the file paths below.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;folder-structure&quot;&gt;Folder Structure&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;src/
├── app/
│   └── api/
│       └── auth/
│           └── [...nextauth]/
│               └── route.ts
├── actions/
│   └── create-post.ts
├── components/
│   └── auth-buttons.tsx
├── lib/
│   └── auth.ts
├── proxy.ts
.env.local
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-does-auth-js-work-&quot;&gt;How does Auth.js work?&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;User clicks &quot;Sign in&quot;
        ↓
Redirect to GitHub
        ↓
User approves
        ↓
GitHub redirects back
        ↓
Auth.js validates
        ↓
Session cookie created
        ↓
auth() can now read the user
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-1-install-auth-js&quot;&gt;Step 1: Install Auth.js&lt;/h3&gt;
&lt;p&gt;Auth.js v5 is still installed under its old package name, &lt;code&gt;next-auth&lt;/code&gt;, using the &lt;code&gt;beta&lt;/code&gt; tag:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install next-auth@beta
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Installing &lt;code&gt;next-auth&lt;/code&gt; without &lt;code&gt;@beta&lt;/code&gt; gives you the old v4 API, which uses a completely different setup (&lt;code&gt;[...nextauth].ts&lt;/code&gt; pages, &lt;code&gt;authOptions&lt;/code&gt;, &lt;code&gt;getServerSession()&lt;/code&gt;). This guide is written entirely for v5.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-2-create-a-github-oauth-app&quot;&gt;Step 2: Create a GitHub OAuth App&lt;/h3&gt;
&lt;p&gt;We're using GitHub as our sign-in provider in this guide. In one sentence: &lt;strong&gt;OAuth lets users sign in using an existing account (like GitHub or Google) without ever sharing their password with your application.&lt;/strong&gt; Instead, GitHub itself confirms the user's identity and hands your app a token proving it — your app never sees or stores their GitHub password.&lt;/p&gt;
&lt;p&gt;Since we're using GitHub, GitHub needs to know about your app before it'll let anyone sign in with it.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/settings/developers&quot;&gt;github.com/settings/developers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;New OAuth App&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Fill in:&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Application name&lt;/strong&gt;: anything, e.g. &lt;code&gt;My App (Dev)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Homepage URL&lt;/strong&gt;: &lt;code&gt;http://localhost:3000&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authorization callback URL&lt;/strong&gt;: &lt;code&gt;http://localhost:3000/api/auth/callback/github&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Register application&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Copy the &lt;strong&gt;Client ID&lt;/strong&gt;, then generate and copy a &lt;strong&gt;Client Secret&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; The callback URL pattern &lt;code&gt;/api/auth/callback/&amp;lt;provider&amp;gt;&lt;/code&gt; is fixed by Auth.js — don't change it, or GitHub won't be able to redirect back to your app correctly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-3-add-environment-variables&quot;&gt;Step 3: Add Environment Variables&lt;/h3&gt;
&lt;p&gt;Create a &lt;code&gt;.env.local&lt;/code&gt; file in your project root:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# .env.local
AUTH_SECRET=replace-this-with-a-random-string
AUTH_GITHUB_ID=your_github_client_id
AUTH_GITHUB_SECRET=your_github_client_secret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;AUTH_SECRET&lt;/code&gt; is used to sign and encrypt session tokens — it must stay private. Generate a strong random value with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx auth secret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command (from the &lt;code&gt;auth&lt;/code&gt; CLI that ships with &lt;code&gt;next-auth&lt;/code&gt;) automatically writes a secure &lt;code&gt;AUTH_SECRET&lt;/code&gt; into your &lt;code&gt;.env.local&lt;/code&gt; file for you.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Forgetting to add &lt;code&gt;.env.local&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt;. If &lt;code&gt;AUTH_SECRET&lt;/code&gt; or your OAuth secret leaks into a public repo, attackers can forge valid sessions for your app.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-4-configure-auth-js&quot;&gt;Step 4: Configure Auth.js&lt;/h3&gt;
&lt;p&gt;This is the heart of the setup. Create an &lt;code&gt;auth.ts&lt;/code&gt; file inside &lt;code&gt;src/lib/&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// src/lib/auth.ts
import NextAuth from &quot;next-auth&quot;;
import GitHub from &quot;next-auth/providers/github&quot;;

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [GitHub],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;what-s-happening-here-&quot;&gt;What's happening here?&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;NextAuth(...)&lt;/code&gt; takes a config object and returns everything you need: &lt;code&gt;handlers&lt;/code&gt; (for the API route), &lt;code&gt;signIn&lt;/code&gt;/&lt;code&gt;signOut&lt;/code&gt; (Server Actions you can call from forms or buttons), and &lt;code&gt;auth()&lt;/code&gt; (a function to read the current session anywhere on the server).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;providers: [GitHub]&lt;/code&gt; tells Auth.js to use GitHub OAuth. Since our environment variables follow the &lt;code&gt;AUTH_GITHUB_ID&lt;/code&gt; / &lt;code&gt;AUTH_GITHUB_SECRET&lt;/code&gt; naming convention, Auth.js picks them up automatically — no need to pass them manually.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Want to add Google too? Just import &lt;code&gt;Google&lt;/code&gt; from &lt;code&gt;next-auth/providers/google&lt;/code&gt;, add matching &lt;code&gt;AUTH_GOOGLE_ID&lt;/code&gt; / &lt;code&gt;AUTH_GOOGLE_SECRET&lt;/code&gt; env variables, and drop it into the &lt;code&gt;providers&lt;/code&gt; array. Auth.js supports 80+ providers this way.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-5-create-the-api-route-handler&quot;&gt;Step 5: Create the API Route Handler&lt;/h3&gt;
&lt;p&gt;Auth.js needs a route to handle sign-in, sign-out, and OAuth callbacks. Create this file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// src/app/api/auth/[...nextauth]/route.ts
import { handlers } from &quot;@/lib/auth&quot;;

export const { GET, POST } = handlers;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;why-a-catch-all-route-&quot;&gt;Why a catch-all route?&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;[...nextauth]&lt;/code&gt; folder name is a &lt;strong&gt;catch-all dynamic route&lt;/strong&gt; — it matches every path under &lt;code&gt;/api/auth/&lt;/code&gt;, like &lt;code&gt;/api/auth/signin&lt;/code&gt;, &lt;code&gt;/api/auth/callback/github&lt;/code&gt;, and &lt;code&gt;/api/auth/session&lt;/code&gt;. Instead of writing a route for each one, Auth.js's &lt;code&gt;handlers&lt;/code&gt; object already knows how to respond to all of them internally.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-6-add-the-proxy-middleware-&quot;&gt;Step 6: Add the Proxy (Middleware)&lt;/h3&gt;
&lt;p&gt;In Next.js 16, what used to be called &lt;code&gt;middleware.ts&lt;/code&gt; is now named &lt;code&gt;proxy.ts&lt;/code&gt; — same concept, same file conventions, just a renamed file. This is the ideal place to check authentication before a request even reaches your page.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// src/proxy.ts
export { auth as proxy } from &quot;@/lib/auth&quot;;

export const config = {
  matcher: [
    &quot;/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)&quot;,
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;what-s-happening-here--1&quot;&gt;What's happening here?&lt;/h4&gt;
&lt;p&gt;Think of &lt;code&gt;proxy.ts&lt;/code&gt; like a &lt;strong&gt;security guard standing at the entrance of your app&lt;/strong&gt; — every matching request passes through it before reaching a page. By exporting &lt;code&gt;auth&lt;/code&gt; as &lt;code&gt;proxy&lt;/code&gt;, every request now has access to the session, which you can use to redirect unauthenticated users.&lt;/p&gt;
&lt;p&gt;By default, this doesn't block anything yet — it just makes auth checking available. Let's actually protect a route next.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-7-protect-a-route&quot;&gt;Step 7: Protect a Route&lt;/h3&gt;
&lt;p&gt;Let's say you have a &lt;code&gt;/dashboard&lt;/code&gt; page that only logged-in users should see, and you also want to bounce already-logged-in users away from your login/home page. Update &lt;code&gt;proxy.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// src/proxy.ts
import { NextResponse } from &quot;next/server&quot;;
import { auth } from &quot;@/lib/auth&quot;;

export default auth((req) =&amp;gt; {
    const isLoggedIn = !!req.auth;
    const { nextUrl } = req

    // Define routes that logged-in users shouldn't see
    const isAuthPage = nextUrl.pathname === &quot;/login&quot; || nextUrl.pathname === &quot;/&quot;

    if (isAuthPage) {
        if (isLoggedIn) {
            // Redirect authenticated users to the dashboard
            return NextResponse.redirect(new URL(&quot;/dashboard&quot;, nextUrl))
        }
        return NextResponse.next()
    }

    // Protect private routes
    if (!isLoggedIn &amp;amp;&amp;amp; nextUrl.pathname.startsWith(&quot;/dashboard&quot;)) {
        return NextResponse.redirect(new URL(&quot;/&quot;, nextUrl))
    }
});

export const config = {
    matcher: [
        &quot;/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)&quot;,
    ]
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;breaking-this-down&quot;&gt;Breaking this down&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;auth((req) =&amp;gt; {...})&lt;/code&gt; wraps your proxy function, giving you &lt;code&gt;req.auth&lt;/code&gt; — the current session, or &lt;code&gt;null&lt;/code&gt; if nobody's logged in.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;isAuthPage&lt;/code&gt; covers routes a logged-in user has no reason to revisit (like &lt;code&gt;/login&lt;/code&gt; or your home page) — if they're already signed in, we send them straight to &lt;code&gt;/dashboard&lt;/code&gt; instead.&lt;/li&gt;
&lt;li&gt;The second check does the opposite: if someone isn't logged in and tries to visit &lt;code&gt;/dashboard&lt;/code&gt;, we redirect them back to &lt;code&gt;/&lt;/code&gt; instead of letting the request through.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Relying on &lt;code&gt;proxy.ts&lt;/code&gt; alone for security. Middleware-level checks are great for UX (redirecting before a page even loads), but you should &lt;em&gt;also&lt;/em&gt; verify the session inside Server Actions and Route Handlers that touch sensitive data — never trust the frontend redirect as your only line of defense.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-8-add-sign-in-sign-out-buttons&quot;&gt;Step 8: Add Sign In / Sign Out Buttons&lt;/h3&gt;
&lt;p&gt;Auth.js exposes &lt;code&gt;signIn&lt;/code&gt; and &lt;code&gt;signOut&lt;/code&gt; as &lt;strong&gt;Server Actions&lt;/strong&gt;, so you can call them directly from a &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; without any client-side JavaScript.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// src/components/auth-buttons.tsx
import { auth, signIn, signOut } from &quot;@/lib/auth&quot;;

export default async function AuthButtons() {
  const session = await auth();

  if (!session?.user) {
    return (
      &amp;lt;form
        action={async () =&amp;gt; {
          &quot;use server&quot;;
          await signIn(&quot;github&quot;);
        }}
      &amp;gt;
        &amp;lt;button
          type=&quot;submit&quot;
          className=&quot;rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground&quot;
        &amp;gt;
          Sign in with GitHub
        &amp;lt;/button&amp;gt;
      &amp;lt;/form&amp;gt;
    );
  }

  return (
    &amp;lt;form
      action={async () =&amp;gt; {
        &quot;use server&quot;;
        await signOut();
      }}
    &amp;gt;
      &amp;lt;button
        type=&quot;submit&quot;
        className=&quot;rounded-lg border px-4 py-2 text-sm font-medium&quot;
      &amp;gt;
        Sign out
      &amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;what-s-happening-here--2&quot;&gt;What's happening here?&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AuthButtons&lt;/code&gt; is an &lt;strong&gt;async Server Component&lt;/strong&gt; — it calls &lt;code&gt;auth()&lt;/code&gt; directly to check whether someone's logged in, no &lt;code&gt;useEffect&lt;/code&gt; or loading state needed.&lt;/li&gt;
&lt;li&gt;The inline &lt;code&gt;&quot;use server&quot;&lt;/code&gt; function inside &lt;code&gt;action={...}&lt;/code&gt; is an &lt;strong&gt;inline Server Action&lt;/strong&gt;. When the form is submitted, this function runs on the server and calls &lt;code&gt;signIn(&quot;github&quot;)&lt;/code&gt; or &lt;code&gt;signOut()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;signIn(&quot;github&quot;)&lt;/code&gt; tells Auth.js exactly which provider to start the OAuth flow with — useful once you add more than one provider.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Drop this component into your navbar, and you've got a fully working auth toggle with zero client-side JavaScript.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-9-read-the-session-in-a-server-component&quot;&gt;Step 9: Read the Session in a Server Component&lt;/h3&gt;
&lt;p&gt;Anywhere on the server — a page, a layout, another component — you can call &lt;code&gt;auth()&lt;/code&gt; to get the current user:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// src/app/dashboard/page.tsx
import { auth } from &quot;@/lib/auth&quot;;

export default async function DashboardPage() {
  const session = await auth();

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1 className=&quot;text-2xl font-bold&quot;&amp;gt;Dashboard&amp;lt;/h1&amp;gt;
      &amp;lt;p className=&quot;text-muted-foreground&quot;&amp;gt;
        Welcome back, {session?.user?.name}!
      &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since we already protect &lt;code&gt;/dashboard&lt;/code&gt; in &lt;code&gt;proxy.ts&lt;/code&gt;, &lt;code&gt;session&lt;/code&gt; is guaranteed to exist by the time this page renders — but it's still good practice to use optional chaining (&lt;code&gt;session?.user?.name&lt;/code&gt;) in case that logic ever changes.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-10-protect-a-server-action-directly&quot;&gt;Step 10: Protect a Server Action Directly&lt;/h3&gt;
&lt;p&gt;Even with &lt;code&gt;proxy.ts&lt;/code&gt; in place, always double-check authentication inside anything that mutates data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// src/actions/create-post.ts
&quot;use server&quot;;

import { auth } from &quot;@/lib/auth&quot;;

export async function createPost(formData: FormData) {
  const session = await auth();

  if (!session?.user) {
    return { error: &quot;You must be signed in to do that.&quot; };
  }

  const title = formData.get(&quot;title&quot;) as string;

  // Your database logic here
  // await db.post.create({ data: { title, authorId: session.user.id } })

  return { success: true };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the same idea as validating input on the server even though your form already validates on the client — never trust that a request only came from your UI.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;frequently-asked-questions-4&quot;&gt;Frequently Asked Questions&lt;/h3&gt;
&lt;h4 id=&quot;is-auth-js-free-&quot;&gt;Is Auth.js free?&lt;/h4&gt;
&lt;p&gt;Yes, completely. Auth.js is open source and has no usage limits, unlike some hosted auth providers that charge per monthly active user. You're only responsible for hosting and maintaining your own database if you want persistent user records.&lt;/p&gt;
&lt;h4 id=&quot;why-is-auth-js-still-installed-as-next-auth-&quot;&gt;Why is Auth.js still installed as &lt;code&gt;next-auth&lt;/code&gt;?&lt;/h4&gt;
&lt;p&gt;Because the npm package name never changed, even though the project itself rebranded. Renaming a package that thousands of projects already depend on would break every existing install, so the team kept &lt;code&gt;next-auth&lt;/code&gt; as the package name and just changed the project's public name and docs to Auth.js. You'll see both names used interchangeably — they refer to the exact same library.&lt;/p&gt;
&lt;h4 id=&quot;why-did-nextauth-become-auth-js-&quot;&gt;Why did NextAuth become Auth.js?&lt;/h4&gt;
&lt;p&gt;NextAuth started out Next.js-only, but the maintainers wanted the same authentication core to work with other frameworks too — SvelteKit, Express, and more, via a shared &lt;code&gt;@auth/core&lt;/code&gt; package. &quot;NextAuth&quot; no longer described what the project did, so it was renamed to the framework-agnostic &lt;strong&gt;Auth.js&lt;/strong&gt;, with &lt;code&gt;next-auth&lt;/code&gt; continuing on as the Next.js-specific package built on top of that shared core.&lt;/p&gt;
&lt;h4 id=&quot;do-i-need-a-database-&quot;&gt;Do I need a database?&lt;/h4&gt;
&lt;p&gt;Not for OAuth-only sign-in like the GitHub example above — Auth.js can run in a stateless JWT session mode with zero database. You'll need a database (with an Auth.js &lt;strong&gt;adapter&lt;/strong&gt;, like the Drizzle or Prisma adapter) if you want persistent user records, email/password login, or multiple linked accounts per user.&lt;/p&gt;
&lt;h4 id=&quot;what-s-the-difference-between-auth-js-and-clerk-&quot;&gt;What's the difference between Auth.js and Clerk?&lt;/h4&gt;
&lt;p&gt;Auth.js gives you full ownership of your data — everything lives in your own database (or nowhere, if you're stateless) — but you build your own UI. Clerk is a hosted service with prebuilt sign-in components and a dashboard, which is faster to set up but means your user data lives on Clerk's infrastructure. If data ownership matters for compliance reasons, Auth.js is the safer default.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 If you're deciding between the two, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/clerk-vs-nextauth-2026&quot;&gt;Clerk vs NextAuth (Auth.js) in 2026&lt;/a&gt; for a full comparison.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;can-i-add-email-password-login-instead-of-oauth-&quot;&gt;Can I add email/password login instead of OAuth?&lt;/h4&gt;
&lt;p&gt;Yes, using the &lt;code&gt;Credentials&lt;/code&gt; provider. It requires you to write your own password hashing and verification logic (Auth.js won't do this for you, since it's security-sensitive), and you'll need a database to store user records.&lt;/p&gt;
&lt;h4 id=&quot;why-is-my-session-null-even-after-signing-in-&quot;&gt;Why is my session &lt;code&gt;null&lt;/code&gt; even after signing in?&lt;/h4&gt;
&lt;p&gt;This is almost always a mismatched &lt;code&gt;AUTH_SECRET&lt;/code&gt; or an incorrect callback URL. Double-check that your GitHub OAuth app's callback URL exactly matches &lt;code&gt;http://localhost:3000/api/auth/callback/github&lt;/code&gt; (or your production domain), and that &lt;code&gt;.env.local&lt;/code&gt; was loaded — restart your dev server after any environment variable change.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;wrapping-up-4&quot;&gt;Wrapping Up&lt;/h3&gt;
&lt;p&gt;You've now got a complete Auth.js v5 setup in Next.js 16 — GitHub OAuth sign-in, route protection via &lt;code&gt;proxy.ts&lt;/code&gt;, and session access in both Server Components and Server Actions. This same pattern extends cleanly: add more providers, connect a database adapter for persistent users, or layer role-based access control on top of the session object.&lt;/p&gt;
&lt;p&gt;From here, a natural next step is wiring up a database adapter so user accounts persist across sign-ins, or adding a second provider like Google for more sign-in options.&lt;/p&gt;
&lt;p&gt;📦 &lt;strong&gt;Source Code:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Muhammad-Ali-sma/next-auth-setup&quot;&gt;View on GitHub&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;useful-resources-1&quot;&gt;Useful Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://authjs.dev&quot;&gt;Auth.js Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://authjs.dev/getting-started/installation&quot;&gt;Auth.js Next.js 16 / App Router Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://authjs.dev/getting-started/providers/github&quot;&gt;Auth.js GitHub Provider Docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;continue-learning-4&quot;&gt;Continue Learning&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/clerk-vs-nextauth-2026&quot;&gt;Clerk vs NextAuth (Auth.js) in 2026: Which One Should You Choose?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/how-to-add-clerk-authentication-in-nextjs-16&quot;&gt;How to Add Clerk Authentication in Next.js 16 (2026 Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/saas-starter-architecture-nextjs&quot;&gt;SaaS Starter Architecture with Next.js 2026&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;helpful-free-tools-1&quot;&gt;Helpful Free Tools&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/tools/meta-tag-generator&quot;&gt;Meta Tag Generator for Next.js &amp;amp; HTML&lt;/a&gt; — generate SEO meta tags, Open Graph tags, Twitter Cards, canonical URLs, and JSON-LD structured data instantly. Handy for wiring up per-post metadata like the &lt;code&gt;generateMetadata()&lt;/code&gt; function above.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/tools/rls-policy-generator&quot;&gt;Supabase RLS Policy Generator&lt;/a&gt; — if your next project pairs this blog setup with a Supabase backend, generate secure, performance-optimized Row Level Security policies in seconds.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/tools/zod-schema-generator&quot;&gt;Zod Schema Generator&lt;/a&gt; — generate Zod schemas instantly from JSON or TypeScript. Includes smart inference, live validation, React Hook Form, Next.js API Routes, and Server Actions.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24071/how-to-add-auth-js-nextauth-v5-authentication-in-next-js-16-2026</guid>
<pubDate>Tue, 04 Aug 2026 04:46:25 +0000</pubDate>
</item>
<item>
<title>Stop Implementing Authentication Inside Containers on Kubernetes</title>
<link>https://coderlegion.com/23251/stop-implementing-authentication-inside-containers-on-kubernetes</link>
<description>
&lt;a href=&quot;https://alexandre-vazquez.com/auth-inside-container-antipattern-kubernetes/&quot; title=&quot;Stop Implementing Authentication Inside Containers on Kubernetes&quot; rel=&quot;nofollow&quot;&gt;&lt;img width=&quot;1536&quot; height=&quot;1024&quot; src=&quot;https://mlk5tkvgfeoa.i.optimole.com/cb:s8WW.68/w:1536/h:1024/q:mauto/f:best/https://alexandre-vazquez.com/wp-content/uploads/2026/07/auth.avif&quot; class=&quot;webfeedsFeaturedVisual wp-post-image&quot; alt=&quot;Stop Implementing Authentication Inside Containers on Kubernetes&quot; style=&quot;display: block; margin: auto; margin-bottom: 5px;max-width: 100%;&quot; title=&quot;Stop Implementing Authentication Inside Containers on Kubernetes 6&quot;&gt;&lt;/a&gt;
&lt;p&gt;You have ten microservices running in Kubernetes. Each one validates JWTs, checks scopes, maintains sessions, and implements its own RBAC rules. One team uses &lt;code&gt;jsonwebtoken&lt;/code&gt; v8, another uses a custom Go library, a third rolled their own HMAC check because &amp;ldquo;it was simple.&amp;rdquo; They all accept &lt;code&gt;alg: none&lt;/code&gt;. Three accept RS256 and HS256 simultaneously.&lt;/p&gt;



&lt;p&gt;This is not a security posture. This is a distributed security liability &amp;mdash; and Kubernetes makes the problem worse, because the cluster creates an illusion of isolation that encourages teams to treat each Pod as a security boundary it was never designed to be.&lt;/p&gt;



&lt;p&gt;The pattern of embedding authentication and authorization logic inside individual containers is one of the most pervasive anti-patterns in Kubernetes-based microservices. It feels like ownership and simplicity. It is, in practice, inconsistency at scale &amp;mdash; and the blast radius of a single misconfiguration is your entire service portfolio.&lt;/p&gt;



&lt;p&gt;Kubernetes provides the primitives to fix this at the infrastructure layer: Ingress controllers, the Gateway API, service mesh sidecars, admission webhooks, and workload identity via SPIFFE. None of these require a line of auth code inside your application containers.&lt;/p&gt;



&lt;p&gt;This article explains why the anti-pattern exists, what&amp;rsquo;s wrong with it technically, and what the correct Kubernetes-native alternatives are &amp;mdash; with concrete implementation guidance and references to the standards and incidents that validate the argument.&lt;/p&gt;



&lt;hr class=&quot;wp-block-separator has-alpha-channel-opacity&quot;&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;The Anti-Pattern: What It Looks Like&lt;/h2&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;In-Application JWT Validation&lt;/h3&gt;



&lt;p&gt;Every service imports an auth library and validates tokens independently:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;# Pattern seen in thousands of microservices
from jose import jwt

def authenticate(token: str):
    payload = jwt.decode(token, SECRET_KEY, algorithms=[&quot;HS256&quot;])
    return payload[&quot;sub&quot;]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Variations include:&lt;/p&gt;



&lt;ul class=&quot;wp-block-list&quot;&gt;

&lt;li&gt;&lt;strong&gt;Algorithm confusion&lt;/strong&gt;: accepting both &lt;code&gt;HS256&lt;/code&gt; and &lt;code&gt;RS256&lt;/code&gt;, or letting the token header drive verification behavior instead of pinning acceptable algorithms server-side. This is a distinct JWT implementation failure class, documented extensively by &lt;a href=&quot;https://portswigger.net/web-security/jwt&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;PortSwigger on JWT attacks&lt;/a&gt; and &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc8725&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;RFC 8725&lt;/a&gt;&lt;/li&gt;


&lt;li&gt;&lt;strong&gt;&lt;code&gt;alg: none&lt;/code&gt; bypass&lt;/strong&gt;: libraries that accept unsigned tokens when &lt;code&gt;alg&lt;/code&gt; is set to &lt;code&gt;none&lt;/code&gt;. This is a documented attack vector in &lt;a href=&quot;https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;Auth0&amp;rsquo;s JWT security analysis&lt;/a&gt;&lt;/li&gt;


&lt;li&gt;&lt;strong&gt;Missing &lt;code&gt;exp&lt;/code&gt; / &lt;code&gt;iss&lt;/code&gt; / &lt;code&gt;aud&lt;/code&gt; validation&lt;/strong&gt;: trusting a valid signature without checking whether the token is expired, for the right audience, or from the right issuer&lt;/li&gt;


&lt;li&gt;&lt;strong&gt;Key confusion attacks&lt;/strong&gt;: accepting a public RS256 key as an HS256 symmetric secret&lt;/li&gt;

&lt;/ul&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Session State in Every Service&lt;/h3&gt;



&lt;p&gt;When services maintain session state directly, they duplicate logic that has no business being duplicated &amp;mdash; cookie validation, refresh token flows, PKCE verification &amp;mdash; and each implementation diverges over time.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;RBAC Reimplemented Per Service&lt;/h3&gt;



&lt;p&gt;Authorization rules (&amp;ldquo;can this user access this resource?&amp;rdquo;) end up embedded in service logic, mixed with business logic, tested inconsistently, and impossible to audit across the portfolio.&lt;/p&gt;



&lt;hr class=&quot;wp-block-separator has-alpha-channel-opacity&quot;&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;Why This Is a Structural Problem&lt;/h2&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;1. The Vulnerability Surface Scales With Your Service Count&lt;/h3&gt;



&lt;p&gt;Each new microservice is a new JWT validation surface. A single incorrect library configuration &amp;mdash; an unvalidated &lt;code&gt;alg&lt;/code&gt;, a missing &lt;code&gt;aud&lt;/code&gt; check &amp;mdash; is an authentication bypass affecting that entire service. With ten services, you have ten potential misconfigurations. With a hundred services, the probability that at least one is misconfigured approaches certainty.&lt;/p&gt;



&lt;p&gt;The &lt;a href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/Kubernetes_Security_Cheat_Sheet.html&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;OWASP Kubernetes Security Cheat Sheet&lt;/a&gt; and &lt;a href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/Microservices_Security_Cheat_Sheet.html&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;OWASP Microservices Security Cheat Sheet&lt;/a&gt; both identify in-service auth as a primary attack surface in microservices environments. &lt;a href=&quot;https://doi.org/10.6028/NIST.SP.800-204&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;NIST SP 800-204&lt;/a&gt; and its companion &lt;a href=&quot;https://doi.org/10.6028/NIST.SP.800-204A&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;NIST SP 800-204A&lt;/a&gt; on DevSecOps make the same argument: &lt;a rel=&quot;nofollow&quot; href=&quot;https://alexandre-vazquez.com/kubernetes-security-best-practices/&quot;&gt;security controls belong at infrastructure boundaries&lt;/a&gt;, not inside application code.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;2. Maintenance Cost Is Multiplicative&lt;/h3&gt;



&lt;p&gt;When a JWT vulnerability is disclosed &amp;mdash; and they are disclosed regularly &amp;mdash; you update one library in one service. Then another. Then you discover service C is pinned to an old version because it has a transitive dependency conflict. Meanwhile the vulnerability is exploitable in production.&lt;/p&gt;



&lt;p&gt;The &lt;a href=&quot;https://github.com/cncf/tag-security/blob/main/security-whitepaper/v2/cloud-native-security-whitepaper.md&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;CNCF Cloud Native Security Whitepaper&lt;/a&gt; frames this directly: security controls implemented redundantly across services create maintenance overhead that teams cannot sustain, leading to version drift and policy divergence.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;3. Centralized Policy Is Impossible to Enforce&lt;/h3&gt;



&lt;p&gt;When policy is in code &amp;mdash; even well-factored library code &amp;mdash; it cannot be changed atomically across services. A policy update requires a coordinated deployment across every affected service. In practice, services deploy on different schedules, managed by different teams, with different testing cycles. The result is that at any given moment, some fraction of your services are running different authorization rules.&lt;/p&gt;



&lt;p&gt;This is the core argument in Google&amp;rsquo;s &lt;a href=&quot;https://cloud.google.com/beyondcorp&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;BeyondCorp&lt;/a&gt; model and the &lt;a href=&quot;https://doi.org/10.6028/NIST.SP.800-207&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;Zero Trust Architecture guidance from NIST SP 800-207&lt;/a&gt;: authentication and authorization decisions should be made by a centralized, auditable policy enforcement point &amp;mdash; not distributed across workloads.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;4. Secrets Distribution Is a Problem You Don&amp;rsquo;t Need&lt;/h3&gt;



&lt;p&gt;If every service validates JWTs, every service needs the signing key (for symmetric algorithms) or the public key (for asymmetric). Distributing and rotating signing keys across a fleet of microservices is an operational burden with meaningful blast radius: a leaked symmetric key compromises every service holding it.&lt;/p&gt;



&lt;p&gt;The &lt;a href=&quot;https://spiffe.io/docs/latest/spiffe-about/overview/&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;CNCF SPIFFE/SPIRE project&lt;/a&gt; was built specifically to solve this class of problem: workload identity should be cryptographically attested, not rely on secrets distributed to application code.&lt;/p&gt;



&lt;hr class=&quot;wp-block-separator has-alpha-channel-opacity&quot;&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;The Real-World Incidents&lt;/h2&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;The &lt;code&gt;alg: none&lt;/code&gt; Class&lt;/h3&gt;



&lt;p&gt;In 2015, critical vulnerabilities in JWT libraries from &lt;a href=&quot;https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;Auth0&lt;/a&gt; affecting Python, PHP, Node.js, Ruby, Java, and .NET allowed attackers to forge tokens by setting &lt;code&gt;alg: none&lt;/code&gt;. The signature was not verified. The vulnerability was present in applications that had copied JWT validation code from tutorials or used unpatched libraries &amp;mdash; exactly the pattern that in-service auth produces at scale.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Java&amp;rsquo;s Psychic Signatures (CVE-2022-21449)&lt;/h3&gt;



&lt;p&gt;&lt;a href=&quot;https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-21449&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;CVE-2022-21449&lt;/a&gt; affected ECDSA signature verification in Oracle Java SE and GraalVM, including &lt;code&gt;java.security.Signature&lt;/code&gt; paths used by higher-level libraries. The bug allowed certain malformed ECDSA signatures to verify when they should not. JWT validation was in scope only when the deployment used an affected Java runtime and ECDSA-signed tokens, for example ES256. A gateway would help only if verification happened on a patched or unaffected runtime at the gateway instead of inside every Java service.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;CVE-2023-2728 (Kubernetes Mountable Secrets Bypass)&lt;/h3&gt;



&lt;p&gt;&lt;a href=&quot;https://www.cve.org/CVERecord?id=CVE-2023-2728&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;CVE-2023-2728&lt;/a&gt; was not an ImagePolicyWebhook outage behavior. It was a Kubernetes API server issue where users could use ephemeral containers to bypass the mountable secrets policy enforced by the ServiceAccount admission plugin. Clusters were affected only when the ServiceAccount admission plugin, the &lt;code&gt;kubernetes.io/enforce-mountable-secrets&lt;/code&gt; annotation, and ephemeral containers were used together. The adjacent ImagePolicyWebhook issue was &lt;a href=&quot;https://github.com/kubernetes/kubernetes/issues/118640&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;CVE-2023-2727&lt;/a&gt;, also involving ephemeral containers, but it is a separate CVE.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;The Uber API Gateway Evolution&lt;/h3&gt;



&lt;p&gt;Uber&amp;rsquo;s engineering blog describes its API gateway as a centralized layer for routing, protocol conversion, rate limiting, load shedding, header propagation, security auditing, and user access blocking. That supports the architectural point here: high-volume platforms move cross-cutting controls into shared infrastructure. The public source does not prove that Uber migrated specifically from per-service authentication to gateway authentication, so that stronger claim should not be made.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Netflix Zuul&lt;/h3&gt;



&lt;p&gt;Netflix&amp;rsquo;s &lt;a href=&quot;https://github.com/Netflix/zuul&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;Zuul&lt;/a&gt; is an L7 gateway for dynamic routing, monitoring, resiliency, security, and related edge concerns. Netflix&amp;rsquo;s own Zuul posts list authentication among common edge-service uses, but they do not frame Zuul primarily as a case study in eliminating per-service auth. Treat it as evidence that authentication is a natural edge concern at scale, not as proof of a specific migration story.&lt;/p&gt;



&lt;hr class=&quot;wp-block-separator has-alpha-channel-opacity&quot;&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;The Alternatives&lt;/h2&gt;



&lt;p&gt;Use these as complementary controls, not as a single replacement for all authentication and authorization logic:&lt;/p&gt;



&lt;figure class=&quot;wp-block-table&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Alternative&lt;/th&gt;&lt;th&gt;When to use it&lt;/th&gt;&lt;th&gt;What it solves&lt;/th&gt;&lt;th&gt;Principal trade-off&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;API Gateway / Edge Auth&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;External API clients, public ingress, partner integrations, mixed auth mechanisms at the boundary&lt;/td&gt;&lt;td&gt;Central JWT/API-key/OAuth2 validation, rate limiting, request shaping, and identity header propagation before traffic reaches services&lt;/td&gt;&lt;td&gt;Does not secure east-west service calls by itself; trusted headers require strict network boundaries&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Service Mesh mTLS&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Service-to-service traffic inside the cluster, especially across teams or sensitive domains&lt;/td&gt;&lt;td&gt;Workload identity, automatic mTLS, peer authentication, and proxy-level authorization policy&lt;/td&gt;&lt;td&gt;Adds data-plane/control-plane complexity and operational coupling to sidecars or ambient mesh components&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;OAuth2 Proxy&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Browser-facing internal apps that need OIDC login, redirects, cookies, and session handling&lt;/td&gt;&lt;td&gt;Delegates login/session management to a reverse proxy and forwards authenticated identity headers&lt;/td&gt;&lt;td&gt;Best for HTTP/browser flows; not a general machine-to-machine authorization system&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;OPA&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Complex, auditable, frequently changing authorization rules&lt;/td&gt;&lt;td&gt;Separates policy decisions from application releases and can run as sidecar, service, ext_authz backend, or admission control via Gatekeeper&lt;/td&gt;&lt;td&gt;Policy/data distribution and failure behavior must be designed deliberately&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;SPIFFE/SPIRE&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Multi-cluster, multi-cloud, or meshless environments that need portable workload identity&lt;/td&gt;&lt;td&gt;Issues short-lived workload identities without application-managed shared secrets&lt;/td&gt;&lt;td&gt;Provides identity, not business authorization; needs registration and attestation lifecycle management&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/figure&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Option 1: API Gateway (Edge Auth)&lt;/h3&gt;



&lt;p&gt;An API Gateway sits at the perimeter and handles authentication before a request reaches any downstream service. Services receive pre-validated identity in a trusted header.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; &amp;ndash; Validates JWTs, API keys, OAuth2 tokens &amp;ndash; Enforces rate limiting per identity &amp;ndash; Strips and re-adds Authorization headers as needed &amp;ndash; Routes to upstream services with verified identity headers&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;When to use it:&lt;/strong&gt; &amp;ndash; North-south traffic (external clients &amp;rarr; cluster) &amp;ndash; Mixed authentication mechanisms (JWT + API key + mTLS) at the Ingress layer &amp;ndash; Teams that want to centralize auth policy without rolling out a full service mesh&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Tools:&lt;/strong&gt;&lt;/p&gt;



&lt;p&gt;&lt;a href=&quot;https://documentation.gravitee.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;Gravitee.io API Gateway&lt;/a&gt; can be deployed on Kubernetes via its &lt;a href=&quot;https://documentation.gravitee.io/apim/getting-started/install-on-kubernetes&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;Helm chart&lt;/a&gt; and integrates with the &lt;a href=&quot;https://documentation.gravitee.io/apim/getting-started/configuration/configure-the-kubernetes-gateway-api&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;Kubernetes Gateway API&lt;/a&gt;:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;helm repo add graviteeio &lt;a href=&quot;https://helm.gravitee.io&quot; rel=&quot;nofollow&quot;&gt;https://helm.gravitee.io&lt;/a&gt;
helm install gravitee-apim graviteeio/apim \
  --namespace gravitee \
  --create-namespace \
  --set gateway.replicaCount=2 \
  --set gateway.ingress.enabled=true \
  --set gateway.ingress.hosts[0]=api.example.com&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Once deployed, authentication policies are declared on the &lt;code&gt;ApiV4&lt;/code&gt; CRD &amp;mdash; no application code involved:&lt;/p&gt;



&lt;pre class=&quot;wp-block-code&quot;&gt;&lt;code&gt;apiVersion: gravitee.io/v1alpha1
kind: ApiV4
metadata:
  name: payment-api
  namespace: gravitee
spec:
  name: &quot;Payment API&quot;
  type: PROXY
  listeners:
    - type: HTTP
      paths:
        - path: /v1/payments
      entrypoints:
        - type: http-proxy
  endpointGroups:
    - name: default
      type: http-proxy
      endpoints:
        - name: upstream
          type: http-proxy
          configuration:
            target: &lt;a href=&quot;http://payment-service.production.svc.cluster.local:8080&quot; rel=&quot;nofollow&quot;&gt;http://payment-service.production.svc.cluster.local:8080&lt;/a&gt;
  flows:
    - name: JWT validation
      enabled: true
      request:
        - policy: jwt
          enabled: true
          configuration:
            signature: RSA_RS256
            publicKeyResolver: JWKS_URL
            jwksUrl: &lt;a href=&quot;https://idp.example.com/.well-known/jwks.json&quot; rel=&quot;nofollow&quot;&gt;https://idp.example.com/.well-known/jwks.json&lt;/a&gt;
            checkTokenRevocation: true
            requiredClaims:
              - name: aud
                value: payment-api
        - policy: rate-limit
          enabled: true
          configuration:
            rate:
              limit: 100
              periodTime: 1
              periodTimeUnit: MINUTES&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Gravitee&amp;rsquo;s &lt;a href=&quot;https://documentation.gravitee.io/apim/getting-started/configuration/configure-the-kubernetes-operator&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;Kubernetes operator&lt;/a&gt; reconciles &lt;code&gt;ApiV4&lt;/code&gt; resources against the gateway, making API policy a first-class GitOps object &amp;mdash; versioned, reviewed, and deployed the same way as any other Kubernetes manifest.&lt;/p&gt;



&lt;p&gt;Other Kubernetes-native options: &lt;a href=&quot;https://www.getambassador.io/docs/emissary&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;Emissary-Ingress&lt;/a&gt; (formerly Ambassador) with its &lt;code&gt;AuthService&lt;/code&gt; CRD; &lt;a href=&quot;https://doc.traefik.io/traefik/middlewares/http/forwardauth/&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;Traefik&lt;/a&gt; with &lt;code&gt;ForwardAuth&lt;/code&gt; middleware on &lt;code&gt;IngressRoute&lt;/code&gt; resources.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Limitations:&lt;/strong&gt; API Gateways handle north-south traffic. They don&amp;rsquo;t address east-west (service-to-service) authentication inside the cluster.&lt;/p&gt;



&lt;hr class=&quot;wp-block-separator has-alpha-channel-opacity&quot;&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;Option 2: Service Mesh (East-West mTLS + Auth)&lt;/h3&gt;



&lt;p&gt;A service mesh provides mutual TLS between every service pair and enforces authorization policy at the sidecar proxy, without any application code changes.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; &amp;ndash; Automatic mTLS between all service-to-service calls &amp;ndash; Workload identity via X.509 certificates (SPIFFE SVIDs) &amp;ndash; Fine-grained AuthorizationPolicy at the Envoy sidecar &amp;ndash; JWT validation at the proxy, not the application&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Istio Implementation:&lt;/strong&gt;&lt;/p&gt;



&lt;p&gt;Istio uses &lt;a href=&quot;https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/ext_authz_filter&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;Envoy&amp;rsquo;s ext_authz filter&lt;/a&gt; and native &lt;a rel=&quot;nofollow&quot; href=&quot;https://alexandre-vazquez.com/istio-serviceentry-explained/&quot;&gt;&lt;code&gt;RequestAuthentication&lt;/code&gt; + &lt;code&gt;AuthorizationPolicy&lt;/code&gt; CRDs&lt;/a&gt;:&lt;/p&gt;



&amp;lt;pre class=&quot;wp-block-c
</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/23251/stop-implementing-authentication-inside-containers-on-kubernetes</guid>
<pubDate>Sat, 25 Jul 2026 10:00:00 +0000</pubDate>
</item>
<item>
<title>Build a Todo App with Next.js 16 and Supabase (2026 Guide)</title>
<link>https://coderlegion.com/23100/build-a-todo-app-with-next-js-16-and-supabase-2026-guide</link>
<description>&lt;p&gt;In this guide, we’ll build a production-ready Todo App using:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Next.js 16&lt;/li&gt;
&lt;li&gt;Supabase&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Tailwind CSS&lt;/li&gt;
&lt;li&gt;Server Actions&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Row Level Security (RLS)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This tutorial focuses more on &lt;strong&gt;logic and architecture&lt;/strong&gt; rather than UI design.&lt;/p&gt;
&lt;p&gt;By the end of this guide, you'll have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Authentication with Supabase&lt;/li&gt;
&lt;li&gt;Protected routes&lt;/li&gt;
&lt;li&gt;Todo CRUD operations&lt;/li&gt;
&lt;li&gt;Row Level Security (RLS)&lt;/li&gt;
&lt;li&gt;Form validation with Zod&lt;/li&gt;
&lt;li&gt;Server Actions&lt;/li&gt;
&lt;li&gt;Secure session handling&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-we-re-building-2&quot;&gt;What We’re Building&lt;/h3&gt;
&lt;p&gt;Our Todo App will include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;User Registration&lt;/li&gt;
&lt;li&gt;User Login&lt;/li&gt;
&lt;li&gt;Protected Dashboard&lt;/li&gt;
&lt;li&gt;Create Todo&lt;/li&gt;
&lt;li&gt;Complete Todo&lt;/li&gt;
&lt;li&gt;Delete Todo&lt;/li&gt;
&lt;li&gt;Secure Database Access with RLS&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;prerequisites-2&quot;&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;Before starting, make sure you have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Node.js installed&lt;/li&gt;
&lt;li&gt;A Supabase account&lt;/li&gt;
&lt;li&gt;Basic understanding of React and Next.js&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;folder-structure-1&quot;&gt;Folder Structure&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;proxy.ts
src
├── actions
│   ├── auth.ts
│   └── todo.ts
├── app
│   ├── layout.tsx
│   ├── (auth)
│   │   ├── layout.tsx
│   │   ├── login
│   │   │   └── page.tsx
│   │   ├── register
│   │   │   └── page.tsx
│   ├── (private)
│   |   ├── layout.tsx
│   |   ├── page.tsx
│   |   ├── create
│   │   │   └── page.tsx
├── components
│   ├── form-error.tsx
│   ├── input.tsx
│   ├── loading-button.tsx
│   ├── navbar.tsx
│   └── todo-actions.tsx
├── utils
│   ├── supabase
│   |   ├── client.ts
│   |   ├── proxy.ts
│   |   └── server.ts
│   ├── validators
│   |   ├── auth.ts
│   │   └── todo.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;create-a-next-js-16-project&quot;&gt;Create a Next.js 16 Project&lt;/h3&gt;
&lt;p&gt;Run the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx create-next-app@latest todo-app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Choose options according to your preferences.&lt;/p&gt;
&lt;p&gt;Recommended setup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Tailwind CSS&lt;/li&gt;
&lt;li&gt;App Router&lt;/li&gt;
&lt;li&gt;React Compiler&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/&lt;/code&gt; directory&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now move into the project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd todo-app
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;setup-supabase&quot;&gt;Setup Supabase&lt;/h3&gt;
&lt;p&gt;Go to &lt;a rel=&quot;nofollow&quot; href=&quot;https://supabase.com/dashboard/&quot;&gt;Supabase Dashboard&lt;/a&gt;:&lt;/p&gt;
&lt;h4 id=&quot;create-a-new-project&quot;&gt;Create a New Project&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;New Project&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Enter project name&lt;/li&gt;
&lt;li&gt;Enter database password&lt;/li&gt;
&lt;li&gt;Select region&lt;/li&gt;
&lt;li&gt;Enable automatic RLS&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create Project&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;add-environment-variables&quot;&gt;Add Environment Variables&lt;/h3&gt;
&lt;p&gt;Inside your project Dashboard:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Under &lt;strong&gt;Get Connected&lt;/strong&gt; section&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Frameworks&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Copy the environment variables.&lt;/p&gt;
&lt;p&gt;Create &lt;code&gt;.env.local&lt;/code&gt; in your project root:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-env&quot;&gt;NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;install-supabase-packages&quot;&gt;Install Supabase Packages&lt;/h3&gt;
&lt;p&gt;Run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install @supabase/supabase-js @supabase/ssr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We’ll also install Zod later for validation.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;understanding-supabase-clients&quot;&gt;Understanding Supabase Clients&lt;/h3&gt;
&lt;p&gt;Supabase provides two client types:&lt;/p&gt;
&lt;h4 id=&quot;browser-client&quot;&gt;Browser Client&lt;/h4&gt;
&lt;p&gt;Used inside Client Components.&lt;/p&gt;
&lt;h4 id=&quot;server-client&quot;&gt;Server Client&lt;/h4&gt;
&lt;p&gt;Used inside:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Server Components&lt;/li&gt;
&lt;li&gt;Server Actions&lt;/li&gt;
&lt;li&gt;Route Handlers&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;create-supabase-browser-client&quot;&gt;Create Supabase Browser Client&lt;/h3&gt;
&lt;p&gt;Create:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;/src/utils/supabase/client.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { createBrowserClient } from &quot;@supabase/ssr&quot;;

export const createClient = () =&amp;gt;
  createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
  );
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;create-supabase-server-client&quot;&gt;Create Supabase Server Client&lt;/h3&gt;
&lt;p&gt;Create:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;/src/utils/supabase/server.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { createServerClient } from &quot;@supabase/ssr&quot;;
import { cookies } from &quot;next/headers&quot;;

export async function createClient() {
  const cookieStore = await cookies();

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll();
        },

        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =&amp;gt;
              cookieStore.set(name, value, options)
            );
          } catch {
            // Ignore if called from Server Component
          }
        },
      },
    }
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;setup-supabase-session-middleware&quot;&gt;Setup Supabase Session Middleware&lt;/h3&gt;
&lt;p&gt;Create:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;/src/utils/supabase/proxy.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { createServerClient } from &quot;@supabase/ssr&quot;;
import { NextResponse, type NextRequest } from &quot;next/server&quot;;

export async function updateSession(request: NextRequest) {
  let supabaseResponse = NextResponse.next({
    request,
  });

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll();
        },

        setAll(cookiesToSet, headers) {
          cookiesToSet.forEach(({ name, value }) =&amp;gt;
            request.cookies.set(name, value)
          );

          supabaseResponse = NextResponse.next({
            request,
          });

          cookiesToSet.forEach(({ name, value, options }) =&amp;gt;
            supabaseResponse.cookies.set(name, value, options)
          );

          Object.entries(headers).forEach(([key, value]) =&amp;gt;
            supabaseResponse.headers.set(key, value)
          );
        },
      },
    }
  );

  await supabase.auth.getClaims();

  return supabaseResponse;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;setup-next-js-middleware&quot;&gt;Setup Next.js Middleware&lt;/h3&gt;
&lt;p&gt;Create:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;/src/proxy.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { type NextRequest } from &quot;next/server&quot;;
import { updateSession } from &quot;@/utils/supabase/proxy&quot;;

export async function proxy(request: NextRequest) {
  return await updateSession(request);
}

export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     * Feel free to modify this pattern to include more paths.
    */
    &quot;/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)&quot;,
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;create-route-groups&quot;&gt;Create Route Groups&lt;/h3&gt;
&lt;p&gt;We’ll use route groups for better organization.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;app
 ├── (auth)
 └── (private)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Route groups help organize routes without affecting URLs.&lt;br&gt;
Learn more about &lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/file-conventions/route-groups&quot;&gt;Nextjs Route Groups&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;create-auth-layout&quot;&gt;Create Auth Layout&lt;/h4&gt;
&lt;p&gt;Create:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;/app/(auth)/layout.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export default function AuthLayout({
  children,
}: Readonly&amp;lt;{
  children: React.ReactNode;
}&amp;gt;) {
  return (
    &amp;lt;div className=&quot;bg-gray-900 flex items-center justify-center flex-1&quot;&amp;gt;
      {children}
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;create-login-page&quot;&gt;Create Login Page&lt;/h3&gt;
&lt;p&gt;Create:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;/app/(auth)/login/page.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&quot;use client&quot;;

import { login } from &quot;@/actions/auth&quot;;
import FormError from &quot;@/components/form-error&quot;;
import Input from &quot;@/components/input&quot;;
import LoadingButton from &quot;@/components/loading-button&quot;;
import Image from &quot;next/image&quot;;
import Link from &quot;next/link&quot;;
import { useActionState } from &quot;react&quot;;

export default function Login() {
  const [state, formAction] = useActionState(login, {
    errors: {
      email: [],
      password: [],
      formError: &quot;&quot;,
    },
    data: {
      email: &quot;&quot;,
      password: &quot;&quot;,
    },
  });

  return (
    &amp;lt;div className=&quot;flex flex-col justify-center w-full&quot;&amp;gt;
      &amp;lt;div className=&quot;sm:mx-auto sm:w-full sm:max-w-sm&quot;&amp;gt;
        &amp;lt;Image
          src=&quot;https://tailwindcss.com/plus-assets/img/logos/mark.svg?color=indigo&amp;amp;shade=500&quot;
          alt=&quot;Logo&quot;
          className=&quot;mx-auto&quot;
          width={50}
          height={50}
        /&amp;gt;

        &amp;lt;h2 className=&quot;mt-10 text-center text-2xl/9 font-bold tracking-tight text-white&quot;&amp;gt;
          Sign in to your account
        &amp;lt;/h2&amp;gt;
      &amp;lt;/div&amp;gt;

      &amp;lt;div className=&quot;mt-10 sm:mx-auto sm:w-full sm:max-w-sm&quot;&amp;gt;
        &amp;lt;form action={formAction} className=&quot;space-y-6&quot;&amp;gt;
          &amp;lt;Input
            defaultValue={state?.data?.email || &quot;&quot;}
            name=&quot;email&quot;
            type=&quot;email&quot;
            label=&quot;Email address&quot;
          /&amp;gt;

          &amp;lt;FormError errors={state.errors.email} /&amp;gt;

          &amp;lt;Input
            defaultValue={state?.data?.password || &quot;&quot;}
            name=&quot;password&quot;
            type=&quot;password&quot;
            label=&quot;Password&quot;
          /&amp;gt;

          &amp;lt;FormError errors={state.errors.password} /&amp;gt;

          &amp;lt;LoadingButton title=&quot;Sign in&quot; /&amp;gt;

          &amp;lt;FormError errors={state?.errors?.formError} /&amp;gt;
        &amp;lt;/form&amp;gt;

        &amp;lt;p className=&quot;mt-10 text-center text-sm/6 text-gray-400&quot;&amp;gt;
          Not a member?{&quot; &quot;}
          &amp;lt;Link
            href=&quot;/register&quot;
            className=&quot;font-semibold text-indigo-400 hover:text-indigo-300&quot;
          &amp;gt;
            Register
          &amp;lt;/Link&amp;gt;
        &amp;lt;/p&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;important-note-about-input-names&quot;&gt;Important Note About Input Names&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;name&lt;/code&gt; attribute is extremely important because we later access form values using:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;formData.get(&quot;email&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If names don’t match, your form won’t work correctly.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;create-reusable-input-component&quot;&gt;Create Reusable Input Component&lt;/h3&gt;
&lt;p&gt;Create:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;/src/components/input.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;interface InputProps {
  name: string;
  label?: string;
  type?: string;
  defaultValue?: string;
  required?: boolean;
}

const Input = ({
  name,
  label,
  type = &quot;text&quot;,
  defaultValue,
  required = true,
}: InputProps) =&amp;gt; {
  return (
    &amp;lt;div&amp;gt;
      {label &amp;amp;&amp;amp; (
        &amp;lt;label
          htmlFor={name}
          className=&quot;block text-sm/6 font-medium text-gray-100&quot;
        &amp;gt;
          {label}
        &amp;lt;/label&amp;gt;
      )}

      &amp;lt;div className=&quot;mt-2&quot;&amp;gt;
        &amp;lt;input
          id={name}
          type={type}
          name={name}
          defaultValue={defaultValue}
          required={required}
          autoComplete={type === &quot;email&quot; ? &quot;email&quot; : undefined}
          className=&quot;block w-full rounded-md bg-white/5 px-3 py-1.5 text-base text-white outline-1 outline-white/10&quot;
        /&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default Input;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;create-loading-button&quot;&gt;Create Loading Button&lt;/h3&gt;
&lt;p&gt;Create:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;/src/components/loading-button.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&quot;use client&quot;;

import { useFormStatus } from &quot;react-dom&quot;;

const LoadingButton = ({
  title,
  className = &quot;&quot;,
}: {
  title: string;
  className?: string;
}) =&amp;gt; {
  const { pending } = useFormStatus();

  return (
    &amp;lt;button
      disabled={pending}
      type=&quot;submit&quot;
      className={`cursor-pointer flex w-full justify-center rounded-md bg-indigo-500 px-3 py-1.5 text-white ${className}`}
    &amp;gt;
      {pending ? &quot;Loading...&quot; : title}
    &amp;lt;/button&amp;gt;
  );
};

export default LoadingButton;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-useformstatus-&quot;&gt;What is useFormStatus?&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;useFormStatus()&lt;/code&gt; gives information about the latest form submission state.&lt;/p&gt;
&lt;p&gt;We use it to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Disable button while submitting&lt;/li&gt;
&lt;li&gt;Show loading state&lt;/li&gt;
&lt;li&gt;Prevent duplicate submissions&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;create-form-error-component&quot;&gt;Create Form Error Component&lt;/h3&gt;
&lt;p&gt;Create:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;/src/components/form-error.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export default function FormError({
  errors,
}: {
  errors?: string | string[];
}) {
  if (!errors || (Array.isArray(errors) &amp;amp;&amp;amp; errors.length === 0)) {
    return null;
  }

  return (
    &amp;lt;p aria-live=&quot;polite&quot; className=&quot;text-sm text-red-500&quot;&amp;gt;
      {typeof errors === &quot;string&quot; ? errors : errors.join(&quot;, &quot;)}
    &amp;lt;/p&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;install-zod&quot;&gt;Install Zod&lt;/h3&gt;
&lt;p&gt;Run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install zod
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;create-auth-validation-schema&quot;&gt;Create Auth Validation Schema&lt;/h3&gt;
&lt;p&gt;Create:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;/src/utils/validators/auth.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import z from &quot;zod&quot;;

export const authSchema = z.object({
  email: z.email(),
  password: z
    .string()
    .min(6, &quot;Password must be at least 6 characters long&quot;)
    .max(100, &quot;Password must not exceed 100 characters&quot;),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;create-login-action&quot;&gt;Create Login Action&lt;/h3&gt;
&lt;p&gt;Create:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;/src/actions/auth.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;&quot;use server&quot;;

import { redirect } from &quot;next/navigation&quot;;
import { createClient } from &quot;@/utils/supabase/server&quot;;
import { authSchema } from &quot;@/utils/validators/auth&quot;;

export async function login(prev: unknown, formData: FormData) {
  const data = {
    email: formData.get(&quot;email&quot;) as string,
    password: formData.get(&quot;password&quot;) as string,
  };

  const validatedFields = authSchema.safeParse(data);

  if (!validatedFields.success) {
    return {
      data,
      errors: {
        ...validatedFields.error.flatten().fieldErrors,
        formError: &quot;&quot;,
      },
    };
  }

  const supabase = await createClient();

  const { error } = await supabase.auth.signInWithPassword(
    validatedFields.data
  );

  if (error) {
    return {
      data,
      errors: {
        formError: error.message,
        email: [],
        password: [],
      },
    };
  }

  redirect(&quot;/&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;understanding-the-login-action&quot;&gt;Understanding the Login Action&lt;/h3&gt;
&lt;p&gt;Here’s what happens:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Get form values using &lt;code&gt;formData&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Validate using Zod&lt;/li&gt;
&lt;li&gt;Call Supabase Auth API&lt;/li&gt;
&lt;li&gt;Redirect user after successful login&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;&lt;p&gt;Because this is a &lt;strong&gt;Server Action&lt;/strong&gt;, we must use the server Supabase client.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;create-register-page&quot;&gt;Create Register Page&lt;/h3&gt;
&lt;p&gt;Create:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;/app/(auth)/register/page.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use the same structure as login page but replace:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { signup } from &quot;@/actions/auth&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;LoadingButton title=&quot;Sign up&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;create-signup-action&quot;&gt;Create Signup Action&lt;/h3&gt;
&lt;p&gt;Add this inside:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;/src/actions/auth.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export async function signup(prev: unknown, formData: FormData) {
  const data = {
    email: formData.get(&quot;email&quot;) as string,
    password: formData.get(&quot;password&quot;) as string,
  };

  const validatedFields = authSchema.safeParse(data);

  if (!validatedFields.success) {
    return {
      data,
      errors: {
        ...validatedFields.error.flatten().fieldErrors,
        formError: &quot;&quot;,
      },
    };
  }

  const supabase = await createClient();

  const { error } = await supabase.auth.signUp(validatedFields.data);

  if (error) {
    return {
      data,
      errors: {
        formError: error.message,
        email: [],
        password: [],
      },
    };
  }

  redirect(&quot;/&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;Supabase may require email confirmation depending on project settings after signup.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;create-private-layout&quot;&gt;Create Private Layout&lt;/h3&gt;
&lt;p&gt;Create:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;/app/(private)/layout.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import Navbar from &quot;@/components/navbar&quot;;

export default function PrivateLayout({
  children,
}: Readonly&amp;lt;{
  children: React.ReactNode;
}&amp;gt;) {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;Navbar /&amp;gt;

      &amp;lt;div className=&quot;mt-16&quot;&amp;gt;{children}&amp;lt;/div&amp;gt;
    &amp;lt;/&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;create-navbar&quot;&gt;Create Navbar&lt;/h3&gt;
&lt;p&gt;Create:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;/src/components/navbar.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { logout } from &quot;@/actions/auth&quot;;
import { createClient } from &quot;@/utils/supabase/server&quot;;
import LoadingButton from &quot;./loading-button&quot;;
import Link from &quot;next/link&quot;;

const Navbar = async () =&amp;gt; {
  const supabase = await createClient();

  const {
    data: { user },
  } = await supabase.auth.getUser();

  return (
    &amp;lt;div className=&quot;w-full flex items-center gap-4 p-4 bg-gray-800 justify-between&quot;&amp;gt;
      &amp;lt;div className=&quot;text-lg font-bold&quot;&amp;gt;
        Hi {user?.email}
      &amp;lt;/div&amp;gt;

      &amp;lt;div className=&quot;flex items-center gap-4&quot;&amp;gt;
        &amp;lt;nav className=&quot;flex items-center gap-4&quot;&amp;gt;
          &amp;lt;Link href=&quot;/&quot;&amp;gt;Home&amp;lt;/Link&amp;gt;
          &amp;lt;Link href=&quot;/create&quot;&amp;gt;Create Todo&amp;lt;/Link&amp;gt;
        &amp;lt;/nav&amp;gt;

        &amp;lt;form action={logout}&amp;gt;
          &amp;lt;LoadingButton title=&quot;Logout&quot; /&amp;gt;
        &amp;lt;/form&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default Navbar;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;create-logout-action&quot;&gt;Create Logout Action&lt;/h3&gt;
&lt;p&gt;Add this to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;/src/actions/auth.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export async function logout() {
  const supabase = await createClient();

  const { data: claimsData } = await supabase.auth.getClaims();

  if (claimsData?.claims) {
    await supabase.auth.signOut();
  }

  redirect(&quot;/login&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;protect-private-routes&quot;&gt;Protect Private Routes&lt;/h3&gt;
&lt;p&gt;Right now, users can manually access protected pages.&lt;/p&gt;
&lt;p&gt;We need route protection.&lt;/p&gt;
&lt;p&gt;Update:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;/src/utils/supabase/proxy.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace this &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;await supabase.auth.getClaims()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const { data } = await supabase.auth.getClaims();

const user = data?.claims;
const publicRoutes = [&quot;/login&quot;, &quot;/register&quot;];

if (user &amp;amp;&amp;amp; publicRoutes.includes(request.nextUrl.pathname)) {
  const url = request.nextUrl.clone();
  url.pathname = &quot;/&quot;;
  return NextResponse.redirect(url);
}

if (!user &amp;amp;&amp;amp; !publicRoutes.includes(request.nextUrl.pathname)) {
  const url = request.nextUrl.clone();
  url.pathname = &quot;/login&quot;;
  return NextResponse.redirect(url);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;understanding-logic&quot;&gt;Understanding Logic&lt;/h4&gt;
&lt;p&gt;Here’s what happens:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Get User&lt;/li&gt;
&lt;li&gt;Create Public Routes&lt;/li&gt;
&lt;li&gt;First &lt;code&gt;if&lt;/code&gt; block checks if the user is logged in &amp;amp; trying to access public routes then navigates user to Home&lt;/li&gt;
&lt;li&gt;Second &lt;code&gt;if&lt;/code&gt; block checks if the user is not logged in &amp;amp; trying to access private routes then navigates user to Login&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-route-protection-works&quot;&gt;How Route Protection Works&lt;/h3&gt;
&lt;h4 id=&quot;public-routes&quot;&gt;Public Routes&lt;/h4&gt;
&lt;p&gt;Anyone can access:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/login&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/register&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;private-routes&quot;&gt;Private Routes&lt;/h4&gt;
&lt;p&gt;Only authenticated users can access them.&lt;/p&gt;
&lt;h4 id=&quot;additional-protection&quot;&gt;Additional Protection&lt;/h4&gt;
&lt;p&gt;Logged-in users cannot revisit auth pages.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;create-todos-table&quot;&gt;Create Todos Table&lt;/h3&gt;
&lt;p&gt;Open:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Supabase Project Dashboard&lt;/li&gt;
&lt;li&gt;From sidebar click SQL Editor&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;create table todos (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null,
  title text not null,
  completed boolean default false,
  created_at timestamptz default now()
);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;add-row-level-security-policies&quot;&gt;Add Row Level Security Policies&lt;/h3&gt;
&lt;p&gt;Run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;create policy &quot;Users can view own todos&quot;
on todos
for select
using (auth.uid() = user_id);

create policy &quot;Users can insert own todos&quot;
on todos
for insert
with check (auth.uid() = user_id);

create policy &quot;Users can update own todos&quot;
on todos
for update
using (auth.uid() = user_id);

create policy &quot;Users can delete own todos&quot;
on todos
for delete
using (auth.uid() = user_id);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-rls-is-important&quot;&gt;Why RLS is Important&lt;/h3&gt;
&lt;p&gt;Without RLS:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Any user could access all data.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With RLS:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Users can only access their own data.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is one of the most important security features in Supabase.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;create-todo-validation-schema&quot;&gt;Create Todo Validation Schema&lt;/h3&gt;
&lt;p&gt;Create:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;/src/utils/validators/todo.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import z from &quot;zod&quot;;

export const todoSchema = z.object({
  title: z
    .string()
    .min(2, &quot;Title must be at least 2 characters long&quot;)
    .max(100, &quot;Title must not exceed 100 characters&quot;),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;create-todo-form&quot;&gt;Create Todo Form&lt;/h3&gt;
&lt;p&gt;Create:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;/app/(private)/create/page.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&quot;use client&quot;;

import { createTodo } from &quot;@/actions/todo&quot;;
import FormError from &quot;@/components/form-error&quot;;
import Input from &quot;@/components/input&quot;;
import LoadingButton from &quot;@/components/loading-button&quot;;
import { useActionState } from &quot;react&quot;;

const CreateTodo = () =&amp;gt; {
  const [state, formAction] = useActionState(createTodo, {
    error: &quot;&quot;,
    data: {
      title: &quot;&quot;,
    },
  });

  return (
    &amp;lt;div className=&quot;mt-10 sm:mx-auto sm:w-full sm:max-w-sm&quot;&amp;gt;
      &amp;lt;form action={formAction} className=&quot;space-y-6&quot;&amp;gt;
        &amp;lt;Input
          defaultValue={state?.data?.title || &quot;&quot;}
          name=&quot;title&quot;
          label=&quot;Todo Title&quot;
        /&amp;gt;

        &amp;lt;FormError errors={state?.error} /&amp;gt;

        &amp;lt;LoadingButton title=&quot;Create Todo&quot; /&amp;gt;
      &amp;lt;/form&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default CreateTodo;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;create-todo-action&quot;&gt;Create Todo Action&lt;/h3&gt;
&lt;p&gt;Create:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;/src/actions/todo.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;&quot;use server&quot;;

import { todoSchema } from &quot;@/utils/validators/todo&quot;;
import { createClient } from &quot;@/utils/supabase/server&quot;;
import { redirect } from &quot;next/navigation&quot;;

export async function createTodo(
  prev: unknown,
  formData: FormData
) {
  const supabase = await createClient();

  const data = {
    title: formData.get(&quot;title&quot;) as string,
  };

  const validatedFields = todoSchema.safeParse(data);

  if (!validatedFields.success) {
    return {
      data,
      error:
        validatedFields.error.flatten().fieldErrors.title?.[0],
    };
  }

  const {
    data: { user },
  } = await supabase.auth.getUser();

  if (!user) {
    return {
      data,
      error: &quot;Please login first&quot;,
    };
  }

  const { error } = await supabase.from(&quot;todos&quot;).insert({
    title: validatedFields.data.title,
    user_id: user.id,
  });

  if (error) {
    return {
      data,
      error: error.message,
    };
  }

  redirect(&quot;/&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;understanding-the-create-todo-action&quot;&gt;Understanding the Create Todo Action&lt;/h3&gt;
&lt;p&gt;Here’s what happens:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Get form values using &lt;code&gt;formData&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Validate using Zod&lt;/li&gt;
&lt;li&gt;Get logged in user&lt;/li&gt;
&lt;li&gt;Call Supabase Auth API&lt;/li&gt;
&lt;li&gt;Redirect user after successful insert&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;create-home-page&quot;&gt;Create Home Page&lt;/h3&gt;
&lt;p&gt;Move:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;/app/page.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;/app/(private)/page.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now update it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import TodoActions from &quot;@/components/todo-actions&quot;;
import { createClient } from &quot;@/utils/supabase/server&quot;;

export default async function Home() {
  const supabase = await createClient();

  const { data } = await supabase
    .from(&quot;todos&quot;)
    .select(&quot;*&quot;)
    .order(&quot;created_at&quot;, {
      ascending: false,
    });

  return (
    &amp;lt;div className=&quot;overflow-x-auto rounded-2xl border border-gray-200 bg-white shadow-sm max-w-xl mx-auto&quot;&amp;gt;
      &amp;lt;table className=&quot;divide-y divide-gray-200 w-full&quot;&amp;gt;
        &amp;lt;thead className=&quot;bg-gray-50&quot;&amp;gt;
          &amp;lt;tr&amp;gt;
            {['Todo', 'Status', 'Actions'].map((header) =&amp;gt; (
              &amp;lt;th
                key={header}
                className=&quot;px-6 py-4 text-left text-sm font-semibold text-gray-700&quot;&amp;gt;
                {header}
              &amp;lt;/th&amp;gt;
            ))}
          &amp;lt;/tr&amp;gt;
        &amp;lt;/thead&amp;gt;

        &amp;lt;tbody className=&quot;divide-y divide-gray-100 bg-white&quot;&amp;gt;
          {data?.map((todo) =&amp;gt; (
            &amp;lt;tr
              key={todo.id}
              className=&quot;transition-colors hover:bg-gray-50&quot;
            &amp;gt;
              &amp;lt;td className=&quot;px-6 py-4&quot;&amp;gt;
                &amp;lt;p className=&quot;font-medium text-gray-900&quot;&amp;gt;
                  {todo.title}
                &amp;lt;/p&amp;gt;
              &amp;lt;/td&amp;gt;

              &amp;lt;td className=&quot;px-6 py-4&quot;&amp;gt;
                &amp;lt;span
                  className={`inline-flex rounded-full px-3 py-1 text-xs font-medium ${todo.completed
                    ? &quot;bg-green-100 text-green-700&quot;
                    : &quot;bg-yellow-100 text-yellow-700&quot;
                    }`}
                &amp;gt;
                  {todo.completed ? &quot;Completed&quot; : &quot;Pending&quot;}
                &amp;lt;/span&amp;gt;
              &amp;lt;/td&amp;gt;

              &amp;lt;td className=&quot;px-6 py-4&quot;&amp;gt;
                &amp;lt;TodoActions todoId={todo.id} isCompleted={todo.completed} /&amp;gt;
              &amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
          ))}
        &amp;lt;/tbody&amp;gt;
      &amp;lt;/table&amp;gt;

      {data?.length === 0 &amp;amp;&amp;amp; (
        &amp;lt;div className=&quot;py-10 text-center text-sm text-gray-500&quot;&amp;gt;
          No todos found
        &amp;lt;/div&amp;gt;
      )}
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We’re fetching todos in descending order so that the latest todo comes at top.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devstacked.tech/supabase-todo-app/image1.png&quot; alt=&quot;Todo App Home Page&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;create-todo-actions-component&quot;&gt;Create Todo Actions Component&lt;/h3&gt;
&lt;p&gt;Create:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;/src/components/todo-actions.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;'use client';

import { handleTodoAction } from &quot;@/actions/todo&quot;;
import { useActionState } from &quot;react&quot;;

const TodoActions = ({ todoId, isCompleted }: { todoId: string; isCompleted: boolean; }) =&amp;gt; {
    const [_, action, isPending] = useActionState(handleTodoAction, undefined);

    const completeTodo = () =&amp;gt; action({ todoId, action: &quot;complete&quot; });
    const deleteTodo = () =&amp;gt; action({ todoId, action: &quot;delete&quot; });

    return (
        &amp;lt;form className=&quot;flex justify-end gap-3&quot;&amp;gt;
            {!isCompleted &amp;amp;&amp;amp;
                &amp;lt;button
                    formAction={completeTodo}
                    disabled={isPending}
                    className=&quot;rounded-lg disabled:opacity-50 bg-blue-500 px-4 py-2 text-sm font-medium text-white transition hover:bg-blue-600&quot;
                &amp;gt;
                    Complete
                &amp;lt;/button&amp;gt;
            }
            &amp;lt;button
                formAction={deleteTodo}
                disabled={isPending}
                className=&quot;rounded-lg disabled:opacity-50 bg-red-500 px-4 py-2 text-sm font-medium text-white transition hover:bg-red-600&quot;
            &amp;gt;
                Delete
            &amp;lt;/button&amp;gt;
        &amp;lt;/form&amp;gt;
    )
}

export default TodoActions
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;complete-and-delete-todo-action&quot;&gt;Complete and Delete Todo Action&lt;/h3&gt;
&lt;p&gt;Add inside:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;/src/actions/todo.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { revalidatePath } from &quot;next/cache&quot;;

export async function handleTodoAction(prev: unknown, data: { todoId: string; action: &quot;delete&quot; | &quot;complete&quot; }) {

    const { todoId, action } = data;

    if (!todoId) return;

    const supabase = await createClient()
    const { data: { user } } = await supabase.auth.getUser();

    if (!user) return;

    if (action === 'delete') {
        await supabase
            .from(&quot;todos&quot;)
            .delete()
            .eq(&quot;id&quot;, todoId)
            .eq(&quot;user_id&quot;, user.id);
    } else {
        await supabase
            .from(&quot;todos&quot;)
            .update({
                completed: true,
            })
            .eq(&quot;id&quot;, todoId)
            .eq(&quot;user_id&quot;, user.id);
    }
    revalidatePath('/');
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-result&quot;&gt;Final Result&lt;/h3&gt;
&lt;p&gt;You now have a fully functional Todo App with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Next.js 16&lt;/li&gt;
&lt;li&gt;Supabase Authentication&lt;/li&gt;
&lt;li&gt;Protected Routes&lt;/li&gt;
&lt;li&gt;Server Actions&lt;/li&gt;
&lt;li&gt;Zod Validation&lt;/li&gt;
&lt;li&gt;Secure RLS Policies&lt;/li&gt;
&lt;li&gt;CRUD Operations&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion-2&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;This project demonstrates a modern full-stack architecture using:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Next.js 16&lt;/li&gt;
&lt;li&gt;Server Actions&lt;/li&gt;
&lt;li&gt;Supabase&lt;/li&gt;
&lt;li&gt;RLS&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even though it’s a simple Todo App, the structure is production-ready and scalable.&lt;/p&gt;
&lt;p&gt;You can now build larger applications using the same architecture and patterns.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;useful-resources-2&quot;&gt;Useful Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/getting-started/mutating-data#what-are-server-functions&quot;&gt;Next.js Server Actions Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://supabase.com/docs/guides/database/postgres/row-level-security&quot;&gt;Row Level Security RLS Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://react.dev/reference/react/useActionState&quot;&gt;React useActionState Hook Docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;Source Code: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Muhammad-Ali-sma/Todo-App&quot;&gt;View on GitHub&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23100/build-a-todo-app-with-next-js-16-and-supabase-2026-guide</guid>
<pubDate>Tue, 21 Jul 2026 04:06:26 +0000</pubDate>
</item>
<item>
<title>How to Add Clerk Authentication in Next.js 16 (2026 Guide)</title>
<link>https://coderlegion.com/23047/how-to-add-clerk-authentication-in-next-js-16-2026-guide</link>
<description>&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;Authentication is one of the most important parts of modern web applications. Whether you're building a SaaS product, admin dashboard, marketplace, or social platform, you need a secure way to manage users, sessions, and protected routes.&lt;/p&gt;
&lt;p&gt;In this guide, you'll learn how to add authentication to your Next.js 16 app using Clerk. We’ll cover:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Setting up Clerk in Next.js 16&lt;/li&gt;
&lt;li&gt;Creating sign-in and sign-up pages&lt;/li&gt;
&lt;li&gt;Protecting private routes&lt;/li&gt;
&lt;li&gt;Accessing authenticated users in server components&lt;/li&gt;
&lt;li&gt;Protecting server actions&lt;/li&gt;
&lt;li&gt;Adding social login&lt;/li&gt;
&lt;li&gt;Understanding route groups and dynamic routes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By the end, you’ll have a complete authentication system running in your app.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-use-clerk-for-authentication-&quot;&gt;Why Use Clerk for Authentication?&lt;/h3&gt;
&lt;p&gt;Building authentication from scratch is time-consuming and error-prone. You need to handle:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Password hashing&lt;/li&gt;
&lt;li&gt;Sessions&lt;/li&gt;
&lt;li&gt;Social login&lt;/li&gt;
&lt;li&gt;Email verification&lt;/li&gt;
&lt;li&gt;Security best practices&lt;/li&gt;
&lt;li&gt;Protected routes&lt;/li&gt;
&lt;li&gt;User management&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instead of reinventing all of this, Clerk provides a production-ready authentication system with prebuilt UI components and secure session handling.&lt;/p&gt;
&lt;h4 id=&quot;key-features-of-clerk&quot;&gt;Key Features of Clerk&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Prebuilt authentication UI&lt;/li&gt;
&lt;li&gt;User management dashboard&lt;/li&gt;
&lt;li&gt;Social login providers&lt;/li&gt;
&lt;li&gt;Session management&lt;/li&gt;
&lt;li&gt;Route protection&lt;/li&gt;
&lt;li&gt;Server-side authentication helpers&lt;/li&gt;
&lt;li&gt;Easy integration with Next.js&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Clerk works extremely well with the App Router in Next.js 16 and makes authentication much simpler.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-you-ll-build&quot;&gt;What You’ll Build&lt;/h3&gt;
&lt;p&gt;In this tutorial, we’ll create:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sign-in page&lt;/li&gt;
&lt;li&gt;Sign-up page&lt;/li&gt;
&lt;li&gt;Protected private routes&lt;/li&gt;
&lt;li&gt;Navbar with auth state&lt;/li&gt;
&lt;li&gt;Server-side user authentication&lt;/li&gt;
&lt;li&gt;GitHub OAuth login&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;prerequisites-3&quot;&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;Before starting, make sure you have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Node.js installed&lt;/li&gt;
&lt;li&gt;Basic knowledge of React and Next.js&lt;/li&gt;
&lt;li&gt;A Clerk account&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;create-a-next-js-project&quot;&gt;Create a Next.js Project&lt;/h3&gt;
&lt;p&gt;Create a new Next.js app using:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx create-next-app@latest clerk-auth
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Choose the options according to your preferences.&lt;/p&gt;
&lt;p&gt;For this tutorial, we’re using:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Tailwind CSS&lt;/li&gt;
&lt;li&gt;React Compiler&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/&lt;/code&gt; directory&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;install-clerk&quot;&gt;Install Clerk&lt;/h3&gt;
&lt;p&gt;Install Clerk in your project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install @clerk/nextjs
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;project-folder-structure&quot;&gt;Project Folder Structure&lt;/h3&gt;
&lt;p&gt;Your project structure will look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;root/
    .env.local

src/
  proxy.ts

  app/
    layout.tsx
    globals.css

    (private)/
      layout.tsx
      page.tsx

    (public)/
      layout.tsx

        sign-in/[[...sign-in]]/
          page.tsx

        sign-up/[[...sign-up]]/
          page.tsx

  components/
    navbar.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;add-clerk-middleware&quot;&gt;Add Clerk Middleware&lt;/h3&gt;
&lt;p&gt;Create a &lt;code&gt;proxy.ts&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;If you’re using the &lt;code&gt;src&lt;/code&gt; directory, place it inside &lt;code&gt;src/proxy.ts&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;src/proxy.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { clerkMiddleware } from '@clerk/nextjs/server'

export default clerkMiddleware();

export const config = {
  matcher: [
    // Skip Next.js internals and static files
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',

    // Always run for API routes
    '/(api|trpc)(.*)',

    // Clerk frontend API routes
    '/__clerk/(.*)',
  ],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;what-does-this-middleware-do-&quot;&gt;What Does This Middleware Do?&lt;/h4&gt;
&lt;p&gt;The Clerk middleware:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Handles authentication sessions&lt;/li&gt;
&lt;li&gt;Detects signed-in users&lt;/li&gt;
&lt;li&gt;Protects routes&lt;/li&gt;
&lt;li&gt;Works with both client and server components&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The matcher ensures middleware runs only where necessary.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;wrap-your-app-with-clerkprovider&quot;&gt;Wrap Your App with ClerkProvider&lt;/h3&gt;
&lt;p&gt;Now wrap your application with &lt;code&gt;ClerkProvider&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;src/app/layout.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { ClerkProvider } from '@clerk/nextjs'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    &amp;lt;html lang=&quot;en&quot;&amp;gt;
      &amp;lt;body className=&quot;min-h-full flex flex-col&quot;&amp;gt;
        &amp;lt;ClerkProvider&amp;gt;
          {children}
        &amp;lt;/ClerkProvider&amp;gt;
      &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;why-use-clerkprovider-&quot;&gt;Why Use ClerkProvider?&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;ClerkProvider&lt;/code&gt; gives your entire application access to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Authentication state&lt;/li&gt;
&lt;li&gt;User sessions&lt;/li&gt;
&lt;li&gt;Clerk UI components&lt;/li&gt;
&lt;li&gt;Server helpers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without it, Clerk components won’t work.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;create-a-navbar-component&quot;&gt;Create a Navbar Component&lt;/h3&gt;
&lt;p&gt;Now let’s create a navigation bar that changes depending on whether the user is logged in.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;src/components/navbar.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import {
  Show,
  SignInButton,
  SignUpButton,
  UserButton,
} from '@clerk/nextjs'

const Navbar = () =&amp;gt; {
  return (
    &amp;lt;header className=&quot;flex justify-end items-center p-4 gap-4 h-16 shadow-md&quot;&amp;gt;
      &amp;lt;Show when=&quot;signed-out&quot;&amp;gt;
        &amp;lt;SignInButton /&amp;gt;

        &amp;lt;SignUpButton&amp;gt;
          &amp;lt;button className=&quot;bg-[#6c47ff] text-white rounded-full font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 cursor-pointer&quot;&amp;gt;
            Sign Up
          &amp;lt;/button&amp;gt;
        &amp;lt;/SignUpButton&amp;gt;
      &amp;lt;/Show&amp;gt;

      &amp;lt;Show when=&quot;signed-in&quot;&amp;gt;
        &amp;lt;UserButton /&amp;gt;
      &amp;lt;/Show&amp;gt;
    &amp;lt;/header&amp;gt;
  )
}

export default Navbar
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;understanding-the-clerk-components&quot;&gt;Understanding the Clerk Components&lt;/h3&gt;
&lt;h4 id=&quot;show&quot;&gt;&lt;code&gt;Show&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;Show&lt;/code&gt; component conditionally renders content based on authentication state.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;Show when=&quot;signed-in&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;Show when=&quot;signed-out&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;signinbutton&quot;&gt;&lt;code&gt;SignInButton&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Opens the Clerk sign-in flow.&lt;/p&gt;
&lt;h4 id=&quot;signupbutton&quot;&gt;&lt;code&gt;SignUpButton&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Opens the Clerk sign-up flow.&lt;/p&gt;
&lt;h4 id=&quot;userbutton&quot;&gt;&lt;code&gt;UserButton&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Displays the authenticated user avatar and account menu.&lt;/p&gt;
&lt;p&gt;Clerk has a lots of built in UI components that’s already do the heavy lifting, learn more &lt;a rel=&quot;nofollow&quot; href=&quot;https://clerk.com/docs/nextjs/reference/components/overview&quot;&gt;Clerk’s UI Components&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;add-navbar-to-root-layout&quot;&gt;Add Navbar to Root Layout&lt;/h3&gt;
&lt;p&gt;Update your root layout:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;src/app/layout.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { ClerkProvider } from '@clerk/nextjs'
import Navbar from '@/components/navbar'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    &amp;lt;html lang=&quot;en&quot;&amp;gt;
      &amp;lt;body&amp;gt;
        &amp;lt;ClerkProvider&amp;gt;
          &amp;lt;Navbar /&amp;gt;
          {children}
        &amp;lt;/ClerkProvider&amp;gt;
      &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;start-the-development-server&quot;&gt;Start the Development Server&lt;/h3&gt;
&lt;p&gt;Run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You’ll see a Clerk popup in the bottom-left corner.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://devstacked.tech/add-clerk-authentication/image1.png&quot; alt=&quot;App Home Page&quot;&gt;&lt;/p&gt;
&lt;p&gt;Click:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;Configure your application
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You’ll be redirected to the Clerk dashboard.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;create-a-clerk-application&quot;&gt;Create a Clerk Application&lt;/h3&gt;
&lt;p&gt;After logging into Clerk:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Enter your application name&lt;/li&gt;
&lt;li&gt;Create the application&lt;/li&gt;
&lt;li&gt;Copy your environment variables&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;add-environment-variables-1&quot;&gt;Add Environment Variables&lt;/h3&gt;
&lt;p&gt;Create a &lt;code&gt;.env.local&lt;/code&gt; file.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;.env.local
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-env&quot;&gt;NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_****
CLERK_SECRET_KEY=***
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After adding them, restart your development server.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;create-a-custom-sign-in-page&quot;&gt;Create a Custom Sign-In Page&lt;/h3&gt;
&lt;p&gt;Now let’s create our own sign-in route.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;src/app/(public)/sign-in/[[...sign-in]]/page.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { SignIn } from '@clerk/nextjs'

const Login = () =&amp;gt; {
  return &amp;lt;SignIn /&amp;gt;
}

export default Login
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;understanding-the-route-structure&quot;&gt;Understanding the Route Structure&lt;/h3&gt;
&lt;h4 id=&quot;route-groups&quot;&gt;Route Groups&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;(public)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a Route Group in Next.js.&lt;/p&gt;
&lt;p&gt;Route groups help organize routes without affecting the URL.&lt;br&gt;
Learn more about &lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/file-conventions/route-groups&quot;&gt;Nextjs Route Groups&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id=&quot;dynamic-catch-all-routes&quot;&gt;Dynamic Catch-All Routes&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;[[...sign-in]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a dynamic catch-all route.&lt;/p&gt;
&lt;p&gt;It allows Clerk to handle multiple auth-related routes internally.&lt;br&gt;
Learn more about &lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/api-reference/file-conventions/dynamic-routes#catch-all-segments&quot;&gt;Nextjs Dynamic Routes&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;create-a-custom-sign-up-page&quot;&gt;Create a Custom Sign-Up Page&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;src/app/sign-up/[[...sign-up]]/page.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { SignUp } from '@clerk/nextjs'

export default function Page() {
  return &amp;lt;SignUp /&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;redirect-clerk-buttons-to-custom-pages&quot;&gt;Redirect Clerk Buttons to Custom Pages&lt;/h3&gt;
&lt;p&gt;Right now, the buttons still redirect to Clerk-hosted pages.&lt;/p&gt;
&lt;p&gt;To fix this, update &lt;code&gt;.env.local&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;.env.local
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-env&quot;&gt;NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the buttons will redirect to your custom routes.&lt;/p&gt;
&lt;p&gt;Learn more about these environment variables and how to customize Clerk's redirect behavior in this dedicated &lt;a rel=&quot;nofollow&quot; href=&quot;https://clerk.com/docs/guides/development/customize-redirect-urls&quot;&gt;clerk redirect guide&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;render-user-information-in-a-server-component&quot;&gt;Render User Information in a Server Component&lt;/h3&gt;
&lt;p&gt;Now let’s display the logged-in user’s name.&lt;/p&gt;
&lt;p&gt;Move your homepage into a private route group:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;app/(private)/page.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And add&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { currentUser } from '@clerk/nextjs/server'

const Home = async () =&amp;gt; {
  const user = await currentUser()

  return (
    &amp;lt;div className='mt-3 text-2xl'&amp;gt;
      Welcome {user?.fullName}
    &amp;lt;/div&amp;gt;
  )
}

export default Home
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;why-use-currentuser-&quot;&gt;Why Use &lt;code&gt;currentUser()&lt;/code&gt;?&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;currentUser()&lt;/code&gt; works inside Server Components and allows you to securely access authenticated user data.&lt;/p&gt;
&lt;p&gt;This is one of the best parts of using Clerk with Next.js App Router.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;protect-private-routes-1&quot;&gt;Protect Private Routes&lt;/h3&gt;
&lt;p&gt;Right now, unauthenticated users can still access your pages.&lt;/p&gt;
&lt;p&gt;Let’s fix that.&lt;/p&gt;
&lt;p&gt;Update your proxy.ts.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;src/proxy.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import {
  clerkMiddleware,
  createRouteMatcher,
} from '@clerk/nextjs/server'

const isPublicRoute = createRouteMatcher([
  '/sign-in(.*)',
  '/sign-up(.*)',
])

export default clerkMiddleware(async (auth, req) =&amp;gt; {
  if (!isPublicRoute(req)) {
    await auth.protect()
  }
})

export const config = {
  matcher: [
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
    '/(api|trpc)(.*)',
    '/__clerk/(.*)',
  ],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;how-route-protection-works-1&quot;&gt;How Route Protection Works&lt;/h4&gt;
&lt;p&gt;We created a public route matcher:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const isPublicRoute = createRouteMatcher([
  '/sign-in(.*)',
  '/sign-up(.*)',
])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Everything except those routes becomes protected.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;await auth.protect()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the user is not authenticated, Clerk automatically redirects them to the sign-in page.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;protect-server-actions&quot;&gt;Protect Server Actions&lt;/h3&gt;
&lt;p&gt;You can also secure server actions using Clerk’s &lt;code&gt;auth()&lt;/code&gt; helper.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;dummy-server-action.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;'use server'

import { auth } from '@clerk/nextjs/server'

export const dummyAction = async () =&amp;gt; {
  // Check if user is authenticated
  const { isAuthenticated } = await auth()

  if (!isAuthenticated) {
    return {
      error: 'Please login',
    }
  }

  // Database logic here
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;why-protect-server-actions-&quot;&gt;Why Protect Server Actions?&lt;/h4&gt;
&lt;p&gt;Even if your UI is protected, server actions should still verify authentication because users can manually send requests.&lt;/p&gt;
&lt;p&gt;This adds an additional security layer.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;add-github-authentication&quot;&gt;Add GitHub Authentication&lt;/h3&gt;
&lt;p&gt;Clerk also supports social authentication providers like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GitHub&lt;/li&gt;
&lt;li&gt;Google&lt;/li&gt;
&lt;li&gt;Discord&lt;/li&gt;
&lt;li&gt;Microsoft&lt;/li&gt;
&lt;li&gt;Apple&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let’s enable GitHub login.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;enable-github-login-in-clerk&quot;&gt;Enable GitHub Login in Clerk&lt;/h3&gt;
&lt;p&gt;Go to your &lt;a rel=&quot;nofollow&quot; href=&quot;https://dashboard.clerk.com/&quot;&gt;Clerk dashboard&lt;/a&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Select your application&lt;/li&gt;
&lt;li&gt;Open the &lt;strong&gt;Configure&lt;/strong&gt; tab&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;SSO Connections&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add Connection&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Select GitHub&lt;/li&gt;
&lt;li&gt;Enable:&lt;ul&gt;
&lt;li&gt;Sign-in&lt;/li&gt;
&lt;li&gt;Sign-up&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Save the connection&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://devstacked.tech/add-clerk-authentication/image2.png&quot; alt=&quot;Clerk Dashboard Add Connection Menu&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now users can authenticate using GitHub.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;important-production-note&quot;&gt;Important Production Note&lt;/h3&gt;
&lt;p&gt;When deploying to production, you should also enable:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;Use Custom Credentials
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This requires configuring your own GitHub OAuth credentials.&lt;br&gt;
Follow this clerk doc on &lt;a rel=&quot;nofollow&quot; href=&quot;https://clerk.com/docs/guides/configure/auth-strategies/social-connections/github&quot;&gt;How to setup Github Custom Credentials&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;tip&quot;&gt;Tip&lt;/h4&gt;
&lt;blockquote&gt;&lt;p&gt;If the built-in Clerk UI components don’t fully match your design requirements, you can create fully custom authentication flows using Clerk APIs. For more information, see the &lt;a rel=&quot;nofollow&quot; href=&quot;https://clerk.com/docs/guides/development/custom-flows/overview&quot;&gt;custom flow guide&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;common-mistakes-to-avoid&quot;&gt;Common Mistakes to Avoid&lt;/h3&gt;
&lt;h4 id=&quot;forgetting-clerkprovider&quot;&gt;Forgetting ClerkProvider&lt;/h4&gt;
&lt;p&gt;Without &lt;code&gt;ClerkProvider&lt;/code&gt;, Clerk components won’t work.&lt;/p&gt;
&lt;h4 id=&quot;incorrect-middleware-location&quot;&gt;Incorrect Middleware Location&lt;/h4&gt;
&lt;p&gt;If using &lt;code&gt;src/&lt;/code&gt;, place middleware inside:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;src/proxy.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;missing-environment-variables&quot;&gt;Missing Environment Variables&lt;/h4&gt;
&lt;p&gt;Always restart your development server after changing &lt;code&gt;.env.local&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&quot;not-protecting-server-actions&quot;&gt;Not Protecting Server Actions&lt;/h4&gt;
&lt;p&gt;Client-side protection alone is not enough.&lt;/p&gt;
&lt;p&gt;Always validate authentication on the server.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;You now have a complete authentication system in your Next.js 16 application using Clerk.&lt;/p&gt;
&lt;p&gt;You learned how to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Install Clerk&lt;/li&gt;
&lt;li&gt;Create sign-in and sign-up pages&lt;/li&gt;
&lt;li&gt;Protect routes&lt;/li&gt;
&lt;li&gt;Access users in server components&lt;/li&gt;
&lt;li&gt;Secure server actions&lt;/li&gt;
&lt;li&gt;Add GitHub authentication&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Clerk removes a huge amount of complexity from authentication and integrates extremely well with modern Next.js applications.&lt;/p&gt;
&lt;p&gt;If you're building production-grade applications, Clerk is one of the fastest and safest ways to add authentication.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;next-steps&quot;&gt;Next Steps&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/stripe-subscription-billing-nextjs-16&quot;&gt;Add Stripe Subscription&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/stripe-payment-element-nextjs-16-guide&quot;&gt;Add Stripe Billing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/nextjs-16-supabase-todo-app&quot;&gt;Add Database with Supabase&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/resend-nextjs-16-guide&quot;&gt;Add Transactional Emails with Resend&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;helpful-resources&quot;&gt;Helpful Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://clerk.com/docs&quot;&gt;Clerk Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs&quot;&gt;Nextjs Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23047/how-to-add-clerk-authentication-in-next-js-16-2026-guide</guid>
<pubDate>Mon, 20 Jul 2026 03:23:46 +0000</pubDate>
</item>
<item>
<title>Access vs Refresh Tokens: I Was Missing Two Ideas</title>
<link>https://coderlegion.com/22752/access-vs-refresh-tokens-i-was-missing-two-ideas</link>
<description>&lt;p&gt;Years ago I was building a login API, and a colleague looked at what I was returning and asked why I only had one token. He said the newer APIs hand back two, an access token and a refresh token. Mine returned one. So I asked him the obvious thing: why two? What does the second one buy you?&lt;/p&gt;
&lt;p&gt;He didn't really know either. He was a front-end dev, and he'd only ever seen the two tokens come back from the backend on a project he'd worked on. When he asked the person who built that side why there were two, all he got was &quot;it's industry standard.&quot; That was the whole explanation. So he was passing that straight down to me: two tokens, that's just how it's done. He said he'd dig into the real reason and get back to me. Two developers standing there, and the actual why had already gone missing one hop up the chain, buried under &quot;it's industry standard.&quot;&lt;/p&gt;
&lt;p&gt;So I did what a confused junior does when the fancy version doesn't make sense. I called it overhead and shipped the simple one. One token, a JWT, and because I didn't want to think about expiry I gave it a life of something like seven years. No revocation. Once it was signed and handed out, it was valid until roughly the next World Cup, and there was nothing I could do to kill it. The only reason that never blew up in my face is that the project never went to production. I didn't get smart, I got lucky.&lt;/p&gt;
&lt;h3 id=&quot;why-two-tokens-made-no-sense-to-me&quot;&gt;Why two tokens made no sense to me&lt;/h3&gt;
&lt;p&gt;The thing worth being honest about is exactly why I thought it was dumb, because the wrong mental model is the interesting part, not the right one.&lt;/p&gt;
&lt;p&gt;First, I thought the refresh token was just a longer-lived copy of the access token. Same thing, one lives longer. If they're the same thing, why carry two? Just use the long one.&lt;/p&gt;
&lt;p&gt;Then my colleague came back with the first real clue: the refresh token lives longer, and it's used rarely, only the access token gets sent on every request, so there's less chance of the refresh one getting stolen. And I remember pushing back on that, because &quot;less chance of being stolen&quot; sounded like a hand-wave. There's still a chance. If it can be stolen at all, and it's the long-lived one, then leaking it is worse, not better. That didn't sound like security, it sounded like hoping.&lt;/p&gt;
&lt;p&gt;And underneath both of those was a bigger assumption I didn't even know I was making: tokens are stateless. That's the whole selling point, right, the server doesn't store them, it just checks the signature. So the idea of revoking one made no sense to me. You can't reach out and kill a stateless token, there's nothing to kill. And if you did want that, we already have sessions for exactly that job, so why reinvent it worse.&lt;/p&gt;
&lt;p&gt;I also had never heard of scopes. It genuinely never occurred to me that a token could be restricted to one specific job. In my head a token meant &quot;this is me, let me do things,&quot; full stop. So when I stacked all of that up, two tokens looked like ceremony. Two things that do the same job, one just lives longer, neither one killable, for a benefit I couldn't measure.&lt;/p&gt;
&lt;h3 id=&quot;the-two-ideas-i-was-actually-missing&quot;&gt;The two ideas I was actually missing&lt;/h3&gt;
&lt;p&gt;None of this resolved quickly. For months after that conversation we just kept shipping the single-token approach, because it worked and we didn't know any better. We were a handful of junior devs with nobody above us to ask. We knew JWT was the standard tool for auth, so we used it the way we understood it, one token, and moved on. The question just sat there, unanswered, for months.&lt;/p&gt;
&lt;p&gt;It didn't click on the job. It clicked on YouTube, which is where I go when a thing refuses to make sense and I want to hear someone who actually gets it explain it.&lt;/p&gt;
&lt;p&gt;Two concepts landed in the same video and the whole thing fell over at once.&lt;/p&gt;
&lt;p&gt;The first was revocation. A token doesn't have to be a fire-and-forget thing you can never call back. If you store it, you can keep a list of the ones you've killed and refuse them. I'd been treating &quot;stateless&quot; as a law instead of a choice, and the moment it's a choice, killing a leaked token is just something you get to decide.&lt;/p&gt;
&lt;p&gt;The second was scopes. A token can be restricted to certain actions and nothing else. So the two tokens weren't two copies of the same authority after all. They could have different powers on purpose.&lt;/p&gt;
&lt;p&gt;Once I had those two pieces I ran every scenario in my head and it all just resolved, and honestly I started laughing at myself, because it was so easy. This was the same access-and-refresh split I'd bounced off when I first went reading about auth, back when none of the articles made sense. Nothing had changed. I'd just been missing two words. I went to my computer and built a small login, signup, and refresh flow as a demo, and I understood the entire thing end to end in under an hour.&lt;/p&gt;
&lt;h3 id=&quot;so-what-are-the-two-tokens-actually-for&quot;&gt;So what are the two tokens actually for&lt;/h3&gt;
&lt;p&gt;Here's the version I wish someone had handed me at the start.&lt;/p&gt;
&lt;p&gt;The access token is the one you send on every single request. Because it's everywhere, all the time, in headers, in logs if you're careless, on every hop, it's the one most likely to leak. So you make it cheap to lose. You give it a short life, minutes to an hour, and you scope it to just what it needs. It's a stateless JWT, so the server never looks it up, it just checks the signature and trusts it until it expires. That's what keeps it fast, and it's also why you don't try to kill an access token individually: you can't, so you lean on the short life instead. If a copy leaks, the damage is small and it ages out on its own fast.&lt;/p&gt;
&lt;p&gt;The refresh token has exactly one job: get a new access token. That's it. It can't read your data, it can't hit your endpoints, it does one thing. And here's the part I'd had backwards: the refresh token usually isn't a JWT at all. It's an opaque random string, a row in your database, and that's on purpose, because a row is a thing you can delete. That's what makes it revocable. It also only ever travels to one place, the single refresh endpoint, never your other routes, so its exposure is a sliver of what the access token's is. You send it rarely, only when the access token is about to run out, so there are fewer chances to leak it in the first place, and the moment you suspect it, you delete the row and it's dead on the next use.&lt;/p&gt;
&lt;p&gt;That finally answers the thing past-me couldn't get over: yes, the long-lived one can still be stolen, but it only works against that one refresh route, it's useless anywhere else, and it dies the instant you kill the row. The long life stops being scary once the thing is killable and can barely go anywhere.&lt;/p&gt;
&lt;p&gt;Where you keep it matters as much as any of this. The refresh token lives in a cryptographically signed, httpOnly cookie, which the browser will hand back only to your server and which JavaScript on the page can't read. What you never do is drop it in localStorage. Anything running on your page can read localStorage, and on a modern app that's a pile of npm packages you didn't write. One compromised dependency reads your token straight out of storage and walks off with it, and you won't get so much as a log line. A signed cookie closes that door.&lt;/p&gt;
&lt;p&gt;That's the piece I'd been missing. The refresh token is not a longer copy of the access token. It has a job the access token can't safely do. It keeps you logged in without making you re-type your password every hour, and it does that while staying killable. Short-lived thing you use constantly, long-lived thing you use almost never and can revoke on demand. Two different jobs, so two different tokens.&lt;/p&gt;
&lt;p&gt;And that split is the best-of-both trick I never saw coming. A session checks the database on every single request: safe, always current, but a lookup every time. A pure JWT checks nothing, ever: fast, but you can't call it back. Two tokens sit right between them. The access token is the no-lookup JWT on the hot path, so the requests that happen constantly stay fast, and the refresh token is the database-backed, revocable thing on the cold path that fires rarely. You get the speed of stateless where it's hot and the killability of sessions where it counts.&lt;/p&gt;
&lt;h3 id=&quot;the-honest-part-you-probably-don-t-need-it&quot;&gt;The honest part: you probably don't need it&lt;/h3&gt;
&lt;p&gt;Now that I get it, I'm not going to pretend everyone should reach for two tokens. Most apps don't need this.&lt;/p&gt;
&lt;p&gt;For a normal app with sessions, one token is fine. Give it a sane life, seven or thirty days, and when it expires, ask the user to log in again. That's not a security hole, that's just a login. The two-token dance has a real cost: you're now refreshing tokens on a timer, maintaining a blacklist, handling the refresh failing, handling the race where two requests refresh at once. That's more moving parts, and every moving part is a new place for it to break.&lt;/p&gt;
&lt;p&gt;Notice that my seven-year JWT wasn't dumb because it was one token. It was dumb because it was unbounded and I couldn't kill it. A single token with a bounded life and a re-login is a perfectly reasonable design for most things I'd build today. Two tokens is what you reach for when the blast radius is big enough to earn the extra machinery: a big multi-tenant system, anything moving real money, anything where a leaked credential is expensive to clean up.&lt;/p&gt;
&lt;p&gt;If I could hand one sentence back to the guy shipping that seven-year token, it'd be this. Short access, long refresh, only the refresh can mint new ones. If the small one leaks, the damage window is tiny. If the long one leaks, you blacklist it. That's the whole thing, and it took me two words, revocation and scopes, to see it.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22752/access-vs-refresh-tokens-i-was-missing-two-ideas</guid>
<pubDate>Wed, 15 Jul 2026 12:11:13 +0000</pubDate>
</item>
<item>
<title>The Other Half of Authentication Is 345 Years Old</title>
<link>https://coderlegion.com/21241/the-other-half-of-authentication-is-345-years-old</link>
<description>&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;Canonical: originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/blog/the-other-half-of-authentication/&quot;&gt;vibeagentmaking.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;In 1675 a scholar declared the old charters were forgeries. A monk answered with a method, diplomatics, the half of document authentication PKI quietly forgot to build.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In 1675, a Jesuit scholar named Daniel van Papenbroeck did something that would feel very familiar today: he announced that you couldn't trust the documents. Writing in the preface to a volume of saints' lives, Papenbroeck argued that a large class of supposedly seventh-century royal charters, Merovingian land grants, the kind of parchment on which monasteries based their legal claims to property and privilege, were forgeries. Not a few of them. Essentially all of them, anything older than around AD 700. The historical record before a certain date, he suggested, was mostly fake.&lt;/p&gt;
&lt;p&gt;This was not an idle academic spat. The Benedictine monks of Saint-Denis held exactly those charters, and their prestige, and their property rights, rode on the documents being real. So the order did the sensible thing and handed the problem to its sharpest young scholar, a monk named Jean Mabillon, with what amounted to a brief: &lt;em&gt;prove the documents are authentic, or prove we have a method for telling.&lt;/em&gt; Mabillon spent six years on it. What he produced in 1681, a six-volume Latin treatise called &lt;em&gt;De re diplomatica&lt;/em&gt;, did not just defend the charters. It founded a science.&lt;/p&gt;
&lt;p&gt;We are living through Papenbroeck's panic again. The 2020s version is &quot;nothing online is real&quot;: deepfaked memos, AI-forged letters, synthetic leaked documents, screenshots of conversations that never happened. And our instinct, like the seventeenth century's, swings between two bad options: credulously trust everything, or cynically trust nothing. Mabillon's contribution was to refuse both. He replaced the panic with a method, and that method, a discipline called &lt;strong&gt;diplomatics&lt;/strong&gt;, is the half of document authentication that modern security engineering quietly forgot to build.&lt;/p&gt;
&lt;h3 id=&quot;a-science-for-telling-the-true-from-the-false&quot;&gt;A science for telling the true from the false&lt;/h3&gt;
&lt;p&gt;Mabillon had a phrase for what his discipline was for: &lt;em&gt;discrimen veri ac falsi&lt;/em&gt;, &quot;the distinguishing of the true from the false.&quot; And his central move, the one that made it a science rather than a series of educated guesses, was this: &lt;strong&gt;you authenticate a document from its form, not from its custody.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That distinction is the whole game, so it's worth being precise about it. There are two ways to decide whether a document is genuine. The first is to trace where it came from, who held it, who passed it to whom, an unbroken line back to its creation. Call that the chain. The second is to interrogate the artifact itself, its structure, its conventions, the thousand small choices a genuine document of its kind would make and a forgery gets subtly wrong. Mabillon's insight was that the second method works even when the first is unavailable, which, for medieval charters scattered and recopied across five centuries, it almost always was.&lt;/p&gt;
&lt;p&gt;To make this systematic, diplomatics treats every formal document as having a predictable anatomy. The opening is the &lt;strong&gt;protocol&lt;/strong&gt;, the invocation, the issuer's title, the addressee, the greeting. The middle is the &lt;strong&gt;text&lt;/strong&gt;, including the &lt;em&gt;dispositio&lt;/em&gt;, the operative clause that actually does the thing (grants the land, confirms the privilege), wrapped in rhetorical preamble and penalty clauses. The close is the &lt;strong&gt;eschatocol&lt;/strong&gt;, the signatures and cross-marks, the witness list, the date and place, the closing benediction. None of this is decoration. In a world where most people couldn't read, the &lt;em&gt;form&lt;/em&gt; was the mechanism by which a document claimed legal force: the right formula, in the right place, sealed the right way, &lt;em&gt;was&lt;/em&gt; the authority.&lt;/p&gt;
&lt;p&gt;And here is the part that should make any security engineer sit up, because it is a forensic principle of startling generality: &lt;strong&gt;forgeries almost always fail at the transitions between the parts.&lt;/strong&gt; A forger can copy a salutation perfectly and then attach it to a dating clause from the wrong century. They can reproduce an authentic seal and bolt it onto a protocol whose formulae no real chancery of that era would have paired with it. The forger learns the &lt;em&gt;surface&lt;/em&gt; of the document type, what it looks like, but not its internal grammar, the way the parts have to cohere. The seams are where the lie shows.&lt;/p&gt;
&lt;p&gt;The nineteenth-century German scholar Theodor von Sickel sharpened this into something explicitly forensic with a concept he called &lt;em&gt;Kanzleimäßigkeit&lt;/em&gt;, &quot;chancery-conformity.&quot; The point was that you don't compare a suspect charter to a vague sense of &quot;how thirteenth-century documents feel.&quot; You compare it to the specific &lt;em&gt;office&lt;/em&gt; that supposedly produced it: which scribe's hand, the clerk who habitually drafted these formulae, the officials who co-signed, what this one chancery's working habits actually were. The target of comparison stopped being a period and became an institution. That is a fingerprinting discipline.&lt;/p&gt;
&lt;p&gt;It worked, and not just on charters. The most famous case predates Mabillon: in 1440 the humanist Lorenzo Valla demolished the &lt;em&gt;Donation of Constantine&lt;/em&gt;, the document by which the fourth-century emperor had supposedly handed temporal authority over the Western Empire to the Pope, by showing its Latin vocabulary and institutional terminology were anachronistic, language a fourth-century chancery could not have produced. The forgery had fooled people for centuries because nobody had read its &lt;em&gt;form&lt;/em&gt; closely enough. Modern scholarship dates the actual composition to the eighth or ninth century. A political fraud that shaped the map of medieval Europe was undone not by finding a smoking-gun source but by reading the document against itself.&lt;/p&gt;
&lt;h3 id=&quot;what-pki-does-and-the-cases-it-can-t-reach&quot;&gt;What PKI does, and the cases it can't reach&lt;/h3&gt;
&lt;p&gt;Now jump 345 years, to how we authenticate digital documents today. The dominant answer is public-key infrastructure: a digital signature, validated through a chain of certificates up to a trusted authority, proving two things, &lt;em&gt;who&lt;/em&gt; signed the document, and that the bytes haven't changed since. When it works, it's beautiful, and far stronger than anything Mabillon had. I want to be clear that PKI is not broken and this is not an argument against it.&lt;/p&gt;
&lt;p&gt;It is an argument about &lt;em&gt;scope.&lt;/em&gt; Because PKI authenticates the chain, and the chain has failure modes, and they are, almost exactly, the cases diplomatics was invented for.&lt;/p&gt;
&lt;p&gt;Start with the one PKI practitioners know best: revocation. Certificates expire, and a certificate authority can revoke one early, publishing the fact through a Certificate Revocation List or an OCSP responder. Here's the catch that's underappreciated outside the field: this quietly undoes PKI's most-advertised selling point. A certificate is supposed to be &lt;em&gt;self-authenticating&lt;/em&gt;, you can check it without phoning home. But once revocation is in the picture, you &lt;em&gt;must&lt;/em&gt; fetch the current revocation data online, and CRLs lag by their publication interval, leaving a window in which a revoked certificate still validates. The &quot;self-authenticating&quot; certificate isn't actually self-contained. Sit with the irony: Mabillon's analysis of a document's internal form is &lt;em&gt;more&lt;/em&gt; self-contained than a modern certificate, because the form is all there, in the artifact, with nothing to go fetch.&lt;/p&gt;
&lt;p&gt;Then the cases that get worse. &lt;strong&gt;Dead keys.&lt;/strong&gt; A digital record signed thirty years ago, with algorithms now obsolete and signing keys long gone, you cannot re-validate that chain, ever. This is precisely the problem the archival world has wrestled with for decades: how do you trust the authenticity of a digital record over the long term when the entire cryptographic context that vouched for it has crumbled?&lt;/p&gt;
&lt;p&gt;And the case that's exploding right now: &lt;strong&gt;artifacts that were never signed at all.&lt;/strong&gt; A leaked file. An internal draft. A screenshot. A forwarded copy stripped of its headers. An AI-generated document. For all of these, there is no chain to check, PKI has nothing to say, because nothing in its model ever happened. The signature it's looking for was never there.&lt;/p&gt;
&lt;p&gt;The clean way to hold the two methods is this: &lt;strong&gt;PKI verifies provenance you have; diplomatics reconstructs trust when provenance is broken or absent.&lt;/strong&gt; They aren't rivals. They're the two halves of the authentication problem, and the security world, reaching naturally for cryptography, built one half and left the other in the archives.&lt;/p&gt;
&lt;h3 id=&quot;the-bridge-already-exists-it-just-never-crossed-the-wall&quot;&gt;The bridge already exists, it just never crossed the wall&lt;/h3&gt;
&lt;p&gt;I want to be careful here, because there's a tempting and false version of this essay that says &quot;nobody has ever connected medieval document science to digital authentication.&quot; That's wrong, and the truth is more interesting.&lt;/p&gt;
&lt;p&gt;The archivist &lt;strong&gt;Luciana Duranti&lt;/strong&gt; and the international &lt;strong&gt;InterPARES&lt;/strong&gt; project have been doing exactly this since 1999, building what they call the &lt;em&gt;archival diplomatics of digital records&lt;/em&gt;, and, pointedly, &lt;em&gt;digital records forensics&lt;/em&gt;. For a quarter century, a serious research program has been carrying Mabillon's principles from parchment to pixels: treating a born-digital record's structure, its metadata, its formal elements as the evidence, asking whether a digital object conforms to the documentary form its claimed origin would produce. The parchment-to-pixels bridge is not a thought experiment. It is a mature field.&lt;/p&gt;
&lt;p&gt;The strange thing, the actual gap worth naming, is that this mature field lives in &lt;em&gt;archival science&lt;/em&gt; and has almost never crossed the disciplinary wall into the &lt;em&gt;cryptographic, PKI, and security-engineering&lt;/em&gt; literature. Two communities have been working the same problem, authenticating documents, with almost no traffic between them. The cryptographers reached for the chain; the archivists kept reading the form; and they rarely cite each other. The opportunity isn't to invent the connection. It's to &lt;em&gt;import&lt;/em&gt; it, to put the form-reading toolkit on the same workbench as the signature-checking one.&lt;/p&gt;
&lt;p&gt;Which brings us to the sharpest modern case, and the one where the two halves most need each other. An AI-generated document is the ultimate chain-broken artifact: a deepfake contract, an LLM-forged resignation letter, a synthetic &quot;leaked memo&quot; can &lt;em&gt;never&lt;/em&gt; be caught by PKI, because nothing valid ever signed them. But they are a textbook diplomatics problem. A language model learns the &lt;em&gt;surface&lt;/em&gt; of a document type, the look and cadence of a contract, a memo, an official letter, from its training distribution. What it does not reliably learn is the institutional &lt;em&gt;grammar&lt;/em&gt; underneath: which formula has to pair with which, which validation marks a specific office actually uses, whether the opening and the operative clause are internally consistent, whether the dating convention matches the claimed issuer. It will, in other words, tend to get the form subtly wrong &lt;em&gt;at the transitions&lt;/em&gt;, which is the exact failure signature Mabillon used to catch the Merovingian forgers. I'm offering that as a natural application, not a finished detector. The lens is the right one. A model that has absorbed the appearance of a document type without its institutional grammar is, structurally, a very fast forger, and forgers have a known weakness.&lt;/p&gt;
&lt;h3 id=&quot;what-to-build-with-this&quot;&gt;What to build with this&lt;/h3&gt;
&lt;p&gt;The practical takeaway for anyone building or trusting verification systems is to stop treating authentication as one question and start asking two.&lt;/p&gt;
&lt;p&gt;PKI's question, &lt;em&gt;can I trust the chain of custody?&lt;/em&gt;, is the right one whenever the chain is intact, and you should keep leaning on it hard. Diplomatics' question, &lt;em&gt;does the document's own form betray it?&lt;/em&gt;, is the one you need whenever the chain is broken, which in the AI era is more and more of the time. The mature engineering posture is to build both, and to know which you're relying on.&lt;/p&gt;
&lt;p&gt;Three concrete things follow. First, &lt;strong&gt;authentication is not binary&lt;/strong&gt;, and diplomatics knew this three centuries before we did: a document can be genuine in substance but altered in form, a faithful copy of a lost original, or a real act recorded in a forged instrument. &quot;Real&quot; and &quot;fake&quot; are too coarse; the useful output is a structured judgment about &lt;em&gt;which&lt;/em&gt; elements conform and which don't. Second, &lt;strong&gt;internal consistency is a verification primitive you can actually implement&lt;/strong&gt;, does this artifact cohere the way a genuine document of its claimed type and origin would?, and it has the rare property of working precisely where signatures don't. Third, and most pointed for anyone shipping AI systems, including those of us building them: &lt;strong&gt;where there is no signing chain, internal-evidence authentication is the only verification left.&lt;/strong&gt; For synthetic content, the form &lt;em&gt;is&lt;/em&gt; the evidence.&lt;/p&gt;
&lt;p&gt;Mabillon's real discovery, the thing that let one monk answer a continent's worth of forgery panic, was that you do not always need to know where a document has been. If you can read its form closely enough, the document carries its own testimony, the seams, the formulae, the conformity or its absence. Three and a half centuries later, as the chain of custody breaks more often than it holds, that is not a quaint medieval technique waiting to be rediscovered. It is the half of authentication we already had, in another building, the whole time.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;sources&quot;&gt;Sources&lt;/h4&gt;
&lt;p&gt;Jean Mabillon, &lt;em&gt;De re diplomatica libri VI&lt;/em&gt; (Paris, 1681), and the Papenbroeck–Mabillon &lt;em&gt;bella diplomatica&lt;/em&gt; over the Saint-Denis Merovingian charters, via Encyclopædia Britannica (&quot;De Re Diplomatica,&quot; &quot;Jean Mabillon&quot;) and Wikipedia (&quot;Diplomatics,&quot; &quot;Jean Mabillon&quot;). Lorenzo Valla, &lt;em&gt;De falso credita et ementita Constantini Donatione&lt;/em&gt; (1440), on the Donation of Constantine; the document anatomy (protocolum / textus / eschatocolum) and Theodor von Sickel's &lt;em&gt;Kanzleimäßigkeit&lt;/em&gt; are standard diplomatics, codified by the 19th-century German and French schools. Luciana Duranti and the InterPARES Project (1999– ): &quot;Archival Diplomatics of Digital Records,&quot; &quot;From Digital Diplomatics to Digital Records Forensics&quot; (&lt;em&gt;Archivaria&lt;/em&gt;), &quot;The Return of Diplomatics as a Forensic Discipline,&quot; and &quot;The Authenticity of Electronic Records: the InterPARES Approach&quot;; Wikipedia, &quot;InterPARES Project.&quot; On PKI revocation and the self-authenticating caveat: Wikipedia, &quot;Certificate revocation list&quot;; TechTarget, &quot;Certificate Revocation List (CRL)&quot;; eMudhra, &quot;How to Verify a PKI-Based Digital Signature.&quot; The application of diplomatics to AI-generated documents is offered here as a proposed forensic approach, not a demonstrated result.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;When the chain of custody breaks, the form is the evidence.&lt;/strong&gt; PKI verifies the provenance you have; it has nothing to say about a leaked draft, a dead-key archive, or an AI-forged memo that nothing valid ever signed. The other half is reading the artifact's own form. Chain-of-consciousness records an agent's reasoning as it works, so the work carries its own internal testimony, a trail you can read even when there is no signature to check.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pip install chain-of-consciousness&lt;/code&gt; · &lt;code&gt;npm install chain-of-consciousness&lt;/code&gt; · &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/hosted-coc/&quot;&gt;Hosted Chain-of-Consciousness&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21241/the-other-half-of-authentication-is-345-years-old</guid>
<pubDate>Wed, 24 Jun 2026 04:57:16 +0000</pubDate>
</item>
<item>
<title>Building Authentication From Scratch in Go — No Libraries, No Magic</title>
<link>https://coderlegion.com/18322/building-authentication-from-scratch-in-go-no-libraries-no-magic</link>
<description>&lt;p&gt;In &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/abhishek_sharma_a9792aee8/i-stopped-watching-tutorials-and-started-building-a-rest-api-in-go-51h5&quot;&gt;Part 2&lt;/a&gt;, I had a working REST API with two endpoints. You could create entries and query them. But anyone could hit the API — no login, no tokens, no protection.&lt;/p&gt;
&lt;p&gt;This post is about the day I decided to build authentication from scratch, and the Go pattern that changed how I think about HTTP servers.&lt;/p&gt;
&lt;h3 id=&quot;the-plan&quot;&gt;The Plan&lt;/h3&gt;
&lt;p&gt;I needed three things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Register&lt;/strong&gt; — hash a password, save the user&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Login&lt;/strong&gt; — verify credentials, return a JWT token&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Middleware&lt;/strong&gt; — block unauthenticated requests before they reach any handler&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;No auth libraries. No Passport.js equivalent. Just &lt;code&gt;golang-jwt/jwt&lt;/code&gt; for token creation and &lt;code&gt;golang.org/x/crypto/bcrypt&lt;/code&gt; for password hashing.&lt;/p&gt;
&lt;h3 id=&quot;registration-my-first-time-hashing-a-password&quot;&gt;Registration: My First Time Hashing a Password&lt;/h3&gt;
&lt;p&gt;Here's what the register handler looked like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func Register(w http.ResponseWriter, r *http.Request) {
    var req RegisterRequest
    json.NewDecoder(r.Body).Decode(&amp;amp;req)

    // Validate
    if req.Email == &quot;&quot; { /* 400 */ }
    if !strings.Contains(req.Email, &quot;@&quot;) { /* 400 */ }
    if len(req.Password) &amp;lt; 6 { /* 400 */ }

    // Hash password — NEVER store plain text
    passwordHash, err := bcrypt.GenerateFromPassword(
        []byte(req.Password), bcrypt.DefaultCost,
    )

    // Save to database
    userID, err := db.CreateUser(req.Email, string(passwordHash))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two things I learned here:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;bcrypt.DefaultCost&lt;/code&gt; is 10.&lt;/strong&gt; That means it runs the hashing algorithm 2^10 = 1,024 times. Slow on purpose — makes brute-force attacks impractical. I spent 20 minutes reading about why a slower hash function is a &lt;em&gt;feature&lt;/em&gt;, not a bug.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;[]byte()&lt;/code&gt; everywhere.&lt;/strong&gt; bcrypt's functions take byte slices, not strings. Coming from JavaScript where everything is just a string, I kept forgetting to convert. Go makes you think about data types at every step.&lt;/p&gt;
&lt;h3 id=&quot;login-creating-my-first-jwt&quot;&gt;Login: Creating My First JWT&lt;/h3&gt;
&lt;p&gt;The login flow taught me more about Go than any tutorial:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// 1. Get user from database
userID, passwordHash, err := db.GetUserByEmail(req.Email)

// 2. Compare password with stored hash
err = bcrypt.CompareHashAndPassword(
    []byte(passwordHash), []byte(req.Password),
)

// 3. Create JWT claims
claims := jwt.MapClaims{
    &quot;user_id&quot;: userID,
    &quot;exp&quot;:     time.Now().Add(24 * time.Hour).Unix(),
}

// 4. Sign the token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(secret))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I left this comment in my code at the time:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Why Unix()? JWT needs simple numbers, not complex Go time objects
// Example: time.Now().Unix() = 1736359530 (just a number)
// Add 24 hours = add 86400 seconds (24 * 60 * 60 = 86400)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Again — verbose, maybe obvious to experienced devs. But I was explaining JWT to &lt;em&gt;myself&lt;/em&gt; while building it. That's the whole point of learning in public.&lt;/p&gt;
&lt;p&gt;One security detail I got right: the error message for a wrong password is &lt;code&gt;&quot;Invalid email or password&quot;&lt;/code&gt; — same as for a non-existent email. You never want to reveal whether an account exists.&lt;/p&gt;
&lt;h3 id=&quot;the-middleware-pattern-that-changed-everything&quot;&gt;The Middleware Pattern That Changed Everything&lt;/h3&gt;
&lt;p&gt;This is the part I'm most proud of from this phase. Go doesn't have middleware built in like Express. You build it yourself:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // 1. Get token from &quot;Authorization: Bearer &amp;lt;token&amp;gt;&quot;
        authHeader := r.Header.Get(&quot;Authorization&quot;)
        tokenString := strings.TrimPrefix(authHeader, &quot;Bearer &quot;)

        // 2. Validate token
        claims, err := validateToken(tokenString)
        if err != nil {
            errorResponseAuth(w, http.StatusUnauthorized, &quot;Invalid token&quot;)
            return
        }

        // 3. Extract user_id and add to request context
        userID := claims[&quot;user_id&quot;].(float64)
        ctx := context.WithValue(r.Context(), &quot;user_id&quot;, int64(userID))

        // 4. Call next handler with updated context
        next(w, r.WithContext(ctx))
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read that again. &lt;strong&gt;A function that takes a handler and returns a new handler.&lt;/strong&gt; The returned handler does the auth check &lt;em&gt;first&lt;/em&gt;, and only calls the original handler if the token is valid.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;main.go&lt;/code&gt;, using it looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Public routes — no protection
http.HandleFunc(&quot;/register&quot;, handlers.Register)
http.HandleFunc(&quot;/login&quot;, handlers.Login)

// Protected route — wrapped with AuthMiddleware
http.HandleFunc(&quot;/entries&quot;, handlers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
    if r.Method == http.MethodPost {
        handlers.CreateEntry(w, r)
    } else if r.Method == http.MethodGet {
        handlers.GetEntries(w, r)
    }
}))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;AuthMiddleware(handler)&lt;/code&gt; — that's it. One wrapper function. No framework, no plugin system, no decorator syntax. Just a function wrapping another function.&lt;/p&gt;
&lt;p&gt;This is the moment Go clicked for me at a deeper level. Functions are values. You can pass them around, wrap them, chain them. Middleware isn't a framework feature — it's just &lt;em&gt;how functions work&lt;/em&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-context-withvalue-trick&quot;&gt;The &lt;code&gt;context.WithValue&lt;/code&gt; Trick&lt;/h3&gt;
&lt;p&gt;The most clever part of Go's auth pattern: how you pass the user ID from middleware to the handler.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Middleware sets it:
ctx := context.WithValue(r.Context(), &quot;user_id&quot;, int64(userID))
next(w, r.WithContext(ctx))

// Handler reads it:
userID := r.Context().Value(&quot;user_id&quot;).(int64)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No global variables. No session objects. The user ID travels &lt;em&gt;with the request itself&lt;/em&gt;. Every handler that needs to know who's making the request just reads it from the context.&lt;/p&gt;
&lt;p&gt;Coming from Express where you'd do &lt;code&gt;req.user = decoded&lt;/code&gt;, this felt more explicit and safer. The data is scoped to the request, not bolted onto a mutable object.&lt;/p&gt;
&lt;h3 id=&quot;one-thing-that-tripped-me-up&quot;&gt;One Thing That Tripped Me Up&lt;/h3&gt;
&lt;p&gt;JWT numbers come back as &lt;code&gt;float64&lt;/code&gt;, not &lt;code&gt;int64&lt;/code&gt;. This line:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;userID, ok := claims[&quot;user_id&quot;].(float64)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I originally wrote &lt;code&gt;.(int64)&lt;/code&gt; and couldn't figure out why it kept failing. Turns out JSON (which JWT uses internally) doesn't distinguish between integers and floats — everything is a number. Go's JSON decoder maps all numbers to &lt;code&gt;float64&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Took me an hour of debugging. Now I'll never forget it.&lt;/p&gt;
&lt;h3 id=&quot;what-the-api-looked-like-after-this&quot;&gt;What the API Looked Like After This&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;POST /register  → Create account (public)
POST /login     → Get JWT token (public)
POST /entries   → Create entry (protected)
GET  /entries   → List entries (protected)
GET  /health    → Health check (public)
GET  /ping      → Ping (public)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Four real endpoints. Authentication. Authorization. No frameworks. Just Go's standard library plus two packages for JWT and bcrypt.&lt;/p&gt;
&lt;h3 id=&quot;what-s-next&quot;&gt;What's Next&lt;/h3&gt;
&lt;p&gt;In Part 4, I'll cover the week I added &lt;strong&gt;testing, UPDATE, and DELETE&lt;/strong&gt; — and why writing tests &lt;em&gt;after&lt;/em&gt; the code changed how I thought about designing handlers.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This is Part 3 of the &quot;Learning Go in Public&quot; series. &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/abhishek_sharma_a9792aee8/why-i-decided-to-learn-go-and-what-my-first-commit-looked-like-38ii&quot;&gt;Part 1&lt;/a&gt; | &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/abhishek_sharma_a9792aee8/i-stopped-watching-tutorials-and-started-building-a-rest-api-in-go-51h5&quot;&gt;Part 2&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18322/building-authentication-from-scratch-in-go-no-libraries-no-magic</guid>
<pubDate>Tue, 19 May 2026 14:02:04 +0000</pubDate>
</item>
<item>
<title>B2B Authentication Provider Comparison: Features, Pricing &amp; SSO Support (2026)</title>
<link>https://coderlegion.com/15553/b2b-authentication-provider-comparison-features-pricing-sso-support-2026</link>
<description>
&lt;p&gt;Enterprise SaaS buyers now expect SSO and SCIM support by default.&lt;/p&gt;
&lt;p&gt;Most enterprise security teams will reject SaaS products that do not support SSO. Identity infrastructure has become a core requirement for B2B SaaS platforms.&lt;/p&gt;
&lt;p&gt;Modern authentication providers help SaaS companies integrate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Enterprise Single Sign-On (SSO)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Directory synchronization&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SCIM user provisioning&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Multi-factor authentication&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Passwordless login&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instead of building authentication infrastructure internally, most SaaS companies rely on specialized identity platforms.&lt;/p&gt;
&lt;p&gt;Below is a quick comparison of the leading B2B authentication providers in 2026.&lt;/p&gt;
&lt;h2&gt;Quick Comparison of Authentication Providers&lt;/h2&gt;
&lt;table class=&quot;border-collapse border border-muted&quot; style=&quot;min-width: 333px;&quot;&gt;&lt;colgroup&gt;&lt;col style=&quot;width: 116px;&quot;&gt;&lt;col style=&quot;min-width: 25px;&quot;&gt;&lt;col style=&quot;min-width: 25px;&quot;&gt;&lt;col style=&quot;min-width: 25px;&quot;&gt;&lt;col style=&quot;width: 142px;&quot;&gt;&lt;/colgroup&gt;&lt;tbody&gt;&lt;tr class=&quot;border-b border-muted&quot;&gt;&lt;th class=&quot;border border-muted bg-muted px-2 py-1 text-left font-medium&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Provider&lt;/p&gt;&lt;/th&gt;&lt;th class=&quot;border border-muted bg-muted px-2 py-1 text-left font-medium&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Best For&lt;/p&gt;&lt;/th&gt;&lt;th class=&quot;border border-muted bg-muted px-2 py-1 text-left font-medium&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Pricing Model&lt;/p&gt;&lt;/th&gt;&lt;th class=&quot;border border-muted bg-muted px-2 py-1 text-left font-medium&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Enterprise SSO&lt;/p&gt;&lt;/th&gt;&lt;th class=&quot;border border-muted bg-muted px-2 py-1 text-left font-medium&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;SCIM&lt;/p&gt;&lt;/th&gt;&lt;/tr&gt;&lt;tr class=&quot;border-b border-muted&quot;&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Auth0&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Full CIAM platform&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;MAU&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;&amp;#10003;&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;&amp;#10003;&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr class=&quot;border-b border-muted&quot;&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Okta&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Enterprise IAM&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Per-user&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;&amp;#10003;&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;&amp;#10003;&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr class=&quot;border-b border-muted&quot;&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;WorkOS&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;B2B SaaS SSO integrations&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Per connection&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;&amp;#10003;&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;&amp;#10003;&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr class=&quot;border-b border-muted&quot;&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;SSOJet&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Enterprise SSO for SaaS&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Per connection&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;&amp;#10003;&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;&amp;#10003;&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr class=&quot;border-b border-muted&quot;&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;MojoAuth&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Passwordless authentication&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;API / MAU&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Partial&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Limited&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr class=&quot;border-b border-muted&quot;&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;FusionAuth&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Self-hosted CIAM&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;License / self-hosted&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;&amp;#10003;&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;&amp;#10003;&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr class=&quot;border-b border-muted&quot;&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Keycloak&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Open-source IAM&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Self-hosted&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;&amp;#10003;&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Limited&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;

&lt;p&gt;Enterprise authentication platforms differ significantly in architecture and pricing models.&lt;/p&gt;
&lt;p&gt;Some providers offer complete identity platforms, while others focus on enterprise SSO infrastructure.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Auth0 provides a full CIAM platform covering authentication, MFA, and user management.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Okta dominates enterprise workforce identity.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SSOJet focuses on enterprise SSO integrations for SaaS apps.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, many modern B2B SaaS companies prefer dedicated enterprise SSO platforms designed specifically for multi-tenant SaaS architectures.&lt;/p&gt;
&lt;p&gt;This is where platforms like SSOJet stand out.&lt;/p&gt;
&lt;p&gt;Instead of replacing your authentication system, SSOJet allows SaaS companies to add enterprise SSO and SCIM provisioning quickly without rebuilding their identity stack.&lt;/p&gt;
&lt;p&gt;This approach is particularly valuable for SaaS startups and mid-market platforms that want to support enterprise customers without complex identity infrastructure.&lt;/p&gt;
&lt;h2&gt;Key Authentication Trends in 2026&lt;/h2&gt;
&lt;p&gt;The authentication landscape is evolving rapidly as organizations prioritize stronger identity security and seamless access management. Several major trends are shaping the identity and authentication market in 2026.&lt;/p&gt;
&lt;h3&gt;1. Enterprise SSO Becoming a Standard Requirement&lt;/h3&gt;
&lt;p&gt;Enterprise Single Sign-On (SSO) is no longer considered an optional feature. Most enterprise customers now require SSO support as part of their vendor procurement process.&lt;/p&gt;
&lt;p&gt;Organizations expect SaaS platforms to integrate with existing identity systems to simplify user management and improve security. Supporting standards like &lt;a rel=&quot;nofollow&quot; href=&quot;https://ssojet.com/blog/what-is-saml-assertion-in-sso&quot;&gt;SAML&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://ssojet.com/blog/openid-connect-explained#what-is-openid-connect-oidc&quot;&gt;&lt;strong&gt;OpenID Connect&lt;/strong&gt;&lt;/a&gt; has become essential for B2B applications.&lt;/p&gt;
&lt;h3&gt;2. Rapid Growth of Passwordless Authentication&lt;/h3&gt;
&lt;p&gt;Traditional password-based authentication is gradually being replaced by passwordless methods.&lt;/p&gt;
&lt;p&gt;Technologies such as &lt;a rel=&quot;nofollow&quot; href=&quot;https://mojoauth.com/blog/what-are-passkeys-and-how-they-work&quot;&gt;&lt;strong&gt;Passkeys&lt;/strong&gt;&lt;/a&gt; and WebAuthn are gaining adoption because they:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;eliminate password-related security risks&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;reduce phishing vulnerabilities&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;improve user experience&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Many modern authentication platforms now prioritize passwordless login as a core capability.&lt;/p&gt;
&lt;h3&gt;3. Identity Security Moving Toward Zero-Trust&lt;/h3&gt;
&lt;p&gt;Security architectures are shifting toward &lt;a rel=&quot;nofollow&quot; href=&quot;https://ssojet.com/blog/safeguard-your-apps-embracing-zero-trust-security&quot;&gt;Zero Trust Architecture&lt;/a&gt;, where access decisions are based on verified identity signals rather than network location.&lt;/p&gt;
&lt;p&gt;In this model:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;every access request is verified&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;user identity and device signals are continuously evaluated&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;trust is never assumed&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Identity providers and authentication platforms play a central role in enabling these zero-trust security models.&lt;/p&gt;
&lt;h3&gt;4. Expansion of Enterprise Identity Integrations&lt;/h3&gt;
&lt;p&gt;Modern organizations rely on multiple identity providers and enterprise platforms to manage user access across their technology stack.&lt;/p&gt;
&lt;p&gt;Common identity providers used by enterprises include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Okta&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Microsoft Entra ID&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Google Workspace&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ping Identity&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As a result, SaaS companies must support seamless identity integrations to enable enterprise customers to authenticate users using their existing identity systems.&lt;/p&gt;
&lt;h2&gt;Why This Comparison Matters&lt;/h2&gt;
&lt;p&gt;Choosing the wrong authentication platform can create major technical and financial problems later.&lt;/p&gt;
&lt;p&gt;Identity infrastructure directly impacts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Security&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enterprise Sales&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Developer Productivity&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Platform Scalability&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A poorly chosen identity provider can make it difficult to support enterprise customers.&lt;/p&gt;
&lt;p&gt;On the other hand, the right provider can dramatically accelerate enterprise onboarding and reduce engineering complexity.&lt;/p&gt;
&lt;p&gt;This guide compares the most important authentication platforms used by B2B SaaS companies in 2026, including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Auth0&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Okta&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;WorkOS&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SSOJet&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;MojoAuth&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;FusionAuth&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keycloak&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We will analyze their:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Features&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pricing Models&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enterprise SSO Capabilities&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Developer Experience&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Real-World Use Cases&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Why Authentication Infrastructure Is Critical for B2B SaaS&lt;/h2&gt;
&lt;p&gt;Authentication is no longer just a login feature.&lt;/p&gt;
&lt;p&gt;For modern SaaS platforms, identity infrastructure directly impacts security, enterprise sales, and user management.&lt;/p&gt;
&lt;p&gt;Enterprise customers expect SaaS applications to support secure authentication standards and identity federation. Without these capabilities, many companies cannot even begin a security review.&lt;/p&gt;
&lt;p&gt;Below are the key reasons why authentication infrastructure has become essential for B2B SaaS products.&lt;/p&gt;
&lt;h3&gt;Enterprise Customers Require SSO&lt;/h3&gt;
&lt;p&gt;Enterprise companies typically manage employee access through a centralized identity provider.&lt;/p&gt;
&lt;p&gt;These identity providers control:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Employee Authentication&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Application Access&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Security Policies&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Multi-factor Authentication&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instead of creating separate accounts for every SaaS application, employees sign in using Single Sign-On (SSO).&lt;/p&gt;
&lt;p&gt;SSO allows a company&amp;rsquo;s identity provider to authenticate users across multiple applications.&lt;/p&gt;
&lt;p&gt;For example, a company using Okta can allow employees to log into a SaaS product using their corporate account.&lt;/p&gt;
&lt;p&gt;This approach improves security and reduces password management issues.&lt;/p&gt;
&lt;p&gt;However, it also means that SaaS companies must integrate with enterprise identity providers to support SSO.&lt;/p&gt;
&lt;p&gt;Platforms like SSOJet and WorkOS help SaaS applications quickly add enterprise SSO integrations without building complex identity infrastructure.&lt;/p&gt;
&lt;h3&gt;Enterprise Procurement Requires Identity Integration&lt;/h3&gt;
&lt;p&gt;SSO is often a mandatory requirement during enterprise procurement.&lt;/p&gt;
&lt;p&gt;Security teams typically evaluate SaaS vendors based on their identity and access management capabilities.&lt;/p&gt;
&lt;p&gt;Common enterprise requirements include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;SAML SSO support&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SCIM Provisioning&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Multi-Factor Authentication&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Role-Based Access Control&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Audit Logging&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If a SaaS product cannot support enterprise identity standards, it may fail security reviews.&lt;/p&gt;
&lt;p&gt;This is one of the main reasons many SaaS startups adopt dedicated authentication platforms such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Auth0&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SSOJet&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;WorkOS&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These platforms simplify enterprise identity integrations and reduce development effort.&lt;/p&gt;
&lt;h3&gt;SCIM Automates User Provisioning&lt;/h3&gt;
&lt;p&gt;Managing user accounts manually becomes difficult for large organizations.&lt;/p&gt;
&lt;p&gt;When a new employee joins a company, the IT team needs to create accounts across multiple SaaS tools.&lt;/p&gt;
&lt;p&gt;When an employee leaves, those accounts must be removed immediately.&lt;/p&gt;
&lt;p&gt;SCIM (System for Cross-domain Identity Management) solves this problem.&lt;/p&gt;
&lt;p&gt;SCIM allows enterprise identity providers to automatically:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Create User Accounts&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Update User Information&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Deactivate Accounts&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This process is called automated user provisioning.&lt;/p&gt;
&lt;p&gt;For SaaS companies selling to enterprises, SCIM support is becoming just as important as SSO.&lt;/p&gt;
&lt;p&gt;Platforms such as SSOJet and WorkOS provide built-in SCIM provisioning to simplify enterprise onboarding.&lt;/p&gt;
&lt;h3&gt;Security Risks of Building Authentication Internally&lt;/h3&gt;
&lt;p&gt;Some SaaS teams attempt to build authentication systems from scratch.&lt;/p&gt;
&lt;p&gt;While this may seem simple initially, identity systems quickly become complex.&lt;/p&gt;
&lt;p&gt;Authentication infrastructure must support:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Password Hashing and Security&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Multi-Factor Authentication&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Session Management&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Token Handling&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Identity Federation&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enterprise SSO Protocols&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Maintaining these systems securely requires significant engineering effort.&lt;/p&gt;
&lt;p&gt;Security vulnerabilities in authentication systems can lead to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Account Takeovers&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Data Breaches&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Compliance Violations&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For this reason, most SaaS companies rely on specialized identity providers rather than building authentication internally.&lt;/p&gt;
&lt;p&gt;Platforms like Auth0 provide a complete identity platform, while providers like SSOJet focus specifically on enterprise SSO infrastructure.&lt;/p&gt;
&lt;h3&gt;Identity Has Become the Security Perimeter&lt;/h3&gt;
&lt;p&gt;Modern cloud applications no longer operate within traditional network boundaries.&lt;/p&gt;
&lt;p&gt;Employees access SaaS platforms from:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Remote Locations&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mobile Devices&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Multiple Networks&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because of this, security teams increasingly rely on identity-based security models.&lt;/p&gt;
&lt;p&gt;In a zero-trust architecture, identity verification becomes the primary method of controlling access.&lt;/p&gt;
&lt;p&gt;Authentication providers play a central role in this model by managing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;User Authentication&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Identity Federation&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Access Policies&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Security Signals&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This shift is one reason why identity infrastructure has become a critical component of SaaS architecture.&lt;/p&gt;
&lt;h2&gt;Why Many B2B SaaS Platforms Use Dedicated SSO Providers&lt;/h2&gt;
&lt;p&gt;Supporting enterprise authentication standards can significantly slow product development.&lt;/p&gt;
&lt;p&gt;Implementing SAML integrations with multiple enterprise identity providers often requires:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Complex Protocol Handling&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Metadata Configuration&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Certificate Management&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Security Testing&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Dedicated enterprise SSO providers simplify this process.&lt;/p&gt;
&lt;p&gt;Platforms like SSOJet provide ready-to-use integrations with enterprise identity providers, allowing SaaS companies to support enterprise authentication quickly.&lt;/p&gt;
&lt;p&gt;This enables SaaS teams to focus on their core product rather than building identity infrastructure.&lt;/p&gt;
&lt;h2&gt;Core Authentication Protocols Every SaaS Company Should Know&lt;/h2&gt;
&lt;p&gt;Enterprise authentication relies on a few core identity standards.&lt;/p&gt;
&lt;p&gt;These protocols allow SaaS applications to integrate with enterprise identity providers and support secure login flows.&lt;/p&gt;
&lt;p&gt;Most modern SaaS authentication platforms support the following protocols:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;SAML&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;OAuth 2.0&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;OpenID Connect (OIDC)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SCIM&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Understanding these protocols helps SaaS teams choose the right authentication provider.&lt;/p&gt;
&lt;h3&gt;SAML (Security Assertion Markup Language)&lt;/h3&gt;
&lt;p&gt;SAML is the most widely used protocol for &lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/@ssojet/what-is-saml-and-how-saml-authentication-works-e6ac1b5a5fbb&quot;&gt;&lt;strong&gt;enterprise Single Sign-On (SSO)&lt;/strong&gt;.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;SAML allows an enterprise identity provider to authenticate users for external applications.&lt;/p&gt;
&lt;p&gt;In a typical SAML login flow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;A user attempts to access a SaaS application.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The application redirects the user to their company&amp;rsquo;s identity provider.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The identity provider authenticates the user.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The identity provider sends a signed authentication response back to the application.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The SaaS application trusts this response and grants access.&lt;/p&gt;
&lt;p&gt;SAML is commonly used by enterprise identity providers such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Okta&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Google Workspace&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Microsoft identity services&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because SAML integrations can be complex, many SaaS companies rely on platforms like SSOJet or WorkOS to simplify implementation.&lt;/p&gt;
&lt;h3&gt;OAuth 2.0&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://ssojet.com/blog/oauth-2-0-overview&quot;&gt;OAuth 2.0&lt;/a&gt; is an authorization framework.&lt;/p&gt;
&lt;p&gt;It allows applications to access resources on behalf of a user without sharing passwords.&lt;/p&gt;
&lt;p&gt;OAuth is commonly used for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;API access&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;third-party integrations&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;delegated permissions&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example, when a SaaS application asks to access a user's Google Drive files, it typically uses OAuth authorization.&lt;/p&gt;
&lt;p&gt;OAuth does not authenticate users directly. Instead, it provides &lt;strong&gt;access tokens&lt;/strong&gt; that allow applications to access specific resources.&lt;/p&gt;
&lt;h3&gt;OpenID Connect (OIDC)&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://ssojet.com/protocol/openid-connect/&quot;&gt;OpenID Connect&lt;/a&gt; is an authentication layer built on top of OAuth 2.0.&lt;/p&gt;
&lt;p&gt;OIDC allows applications to verify a user&amp;rsquo;s identity.&lt;/p&gt;
&lt;p&gt;Unlike OAuth, which focuses on authorization, OpenID Connect provides authentication information.&lt;/p&gt;
&lt;p&gt;OIDC is widely used for modern applications because it supports:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Mobile Applications&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Web Applications&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;API-Based Authentication&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Many modern authenticati&lt;/p&gt;
</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/15553/b2b-authentication-provider-comparison-features-pricing-sso-support-2026</guid>
<pubDate>Tue, 28 Apr 2026 20:29:55 +0000</pubDate>
</item>
<item>
<title>Why Language Schools Should Prioritize Cybersecurity: A Passwordless Approach</title>
<link>https://coderlegion.com/15282/why-language-schools-should-prioritize-cybersecurity-a-passwordless-approach</link>
<description>
&lt;p&gt;As language schools use more digital tools, they face bigger risks from cyberattacks. Hackers often target schools because of the valuable personal data they hold. Traditional passwords are not strong enough to protect this information and can be easily hacked. A better solution is passwordless authentication.&lt;/p&gt;
&lt;p&gt;In this article, we will explore why language schools should prioritize cybersecurity and how adopting passwordless authentication can protect sensitive information while improving user experience.&lt;/p&gt;
&lt;h2&gt;The Rising Threats to Language School Security&lt;/h2&gt;
&lt;p&gt;Language schools face a variety of cybersecurity risks, including data breaches, phishing attacks, and the theft of sensitive student information. Hackers target educational platforms because they hold valuable personal data, such as student records, payment details, and communication histories.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Unfortunately, many schools have weak security systems that make them easy targets. For example, a &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.cybersecuritydive.com/news/2020-k12-cybersecurity/597666/&quot;&gt;&lt;u&gt;2020 report&lt;/u&gt;&lt;/a&gt; found that educational institutions were among the top sectors targeted by cyberattacks, with over 1,200 cybersecurity incidents reported in the U.S. alone.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;These attacks not only threaten student privacy but also damage the reputation and trust that language schools have built. With the rise in cyberattacks, it&amp;rsquo;s crucial for schools to strengthen their security measures before it&amp;rsquo;s too late.&lt;/p&gt;
&lt;h2&gt;Why Passwords Aren&amp;rsquo;t Enough for Language Schools&lt;/h2&gt;
&lt;p&gt;For years, passwords have been the standard way to secure online accounts, but they are no longer enough to protect against modern cyber threats. Hackers can &lt;a rel=&quot;nofollow&quot; href=&quot;https://mojoauth.com/blog/password-attacks&quot;&gt;&lt;u&gt;easily guess or steal passwords&lt;/u&gt;&lt;/a&gt; through techniques like phishing or brute force attacks, leading to data breaches and identity theft. This makes relying on passwords a risky choice for language schools. Common challenges with traditional password systems include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Password fatigue:&lt;/strong&gt; Users often forget complex passwords, leading them to reuse simple ones across different accounts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Weak password practices:&lt;/strong&gt; Many individuals choose easily guessable passwords, like &amp;ldquo;123456&amp;rdquo; or &amp;ldquo;password,&amp;rdquo; which are highly vulnerable to attack.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Security loopholes:&lt;/strong&gt; When passwords are reused or not updated regularly, they create gaps in security that hackers can exploit.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What Is Passwordless Authentication and How Does It Work?&lt;/h2&gt;
&lt;p&gt;Passwordless authentication is a secure login method that eliminates the need for traditional passwords. Instead, users authenticate through alternative methods, such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Magic links:&lt;/strong&gt; A one-time link sent to the user&amp;rsquo;s email to grant access.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;One-time passwords (OTPs):&lt;/strong&gt; Temporary codes sent via email, SMS, or apps like WhatsApp.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Biometrics:&lt;/strong&gt; Authentication through fingerprints, facial recognition, or other physical identifiers.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key benefits of passwordless systems include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Improved security:&lt;/strong&gt; Without passwords, there's less risk of phishing or password theft.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reduced login friction:&lt;/strong&gt; Users don't have to remember or type passwords, making access faster and easier.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Better user experience:&lt;/strong&gt; A more secure login process leads to higher user satisfaction.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Modern EdTech platforms, especially those offering multilingual learning, must balance ease of access with strong security. Whether you're building a custom LMS or integrating with &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.teachngo.com/solutions/language-school-management-software&quot;&gt;&lt;u&gt;easy to use language teacher software&lt;/u&gt;&lt;/a&gt;, implementing passwordless authentication ensures students and instructors can log in securely without friction.&lt;/p&gt;
&lt;h2&gt;Endnote&lt;/h2&gt;
&lt;p&gt;Language schools must prioritize cybersecurity to protect sensitive data. Traditional passwords are no longer enough, leaving schools vulnerable to attacks. Adopting passwordless authentication enhances security, reduces login friction, and improves the user experience. By integrating solutions like MojoAuth, language schools can safeguard their platforms and ensure a safer and efficient learning environment.&lt;/p&gt;

</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/15282/why-language-schools-should-prioritize-cybersecurity-a-passwordless-approach</guid>
<pubDate>Mon, 27 Apr 2026 09:12:54 +0000</pubDate>
</item>
<item>
<title>7 Enterprise Infrastructure Tools That Eliminate Months of Engineering Work</title>
<link>https://coderlegion.com/15555/7-enterprise-infrastructure-tools-that-eliminate-months-of-engineering-work</link>
<description>
&lt;p&gt;Engineering teams lose thousands of hours annually building infrastructure that already exists as battle-tested platforms. Authentication systems, AI security layers, internal developer portals, and database management consume months of development cycles that could go toward shipping product features.&lt;/p&gt;
&lt;p&gt;This guide covers seven enterprise infrastructure tools that eliminate this engineering toil. Each tool addresses a specific infrastructure bottleneck, with verified pricing and deployment timelines based on current market data.&lt;/p&gt;
&lt;h2&gt;Quick Comparison: All 7 Tools at a Glance&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Free Tier&lt;/th&gt;
&lt;th&gt;Paid From&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MojoAuth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Passwordless Auth&lt;/td&gt;
&lt;td&gt;25K MAUs&lt;/td&gt;
&lt;td&gt;$50/month&lt;/td&gt;
&lt;td&gt;B2C/B2B apps needing SSO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gopher MCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AI/MCP Security&lt;/td&gt;
&lt;td&gt;1K tool calls&lt;/td&gt;
&lt;td&gt;$179/month&lt;/td&gt;
&lt;td&gt;AI agent deployments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Port.io&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Developer Portal&lt;/td&gt;
&lt;td&gt;15 seats&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Platform engineering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pulumi&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Infrastructure as Code&lt;/td&gt;
&lt;td&gt;Individual&lt;/td&gt;
&lt;td&gt;$0.0005/credit&lt;/td&gt;
&lt;td&gt;Multi-cloud IaC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Retool&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Internal Tools&lt;/td&gt;
&lt;td&gt;5 users&lt;/td&gt;
&lt;td&gt;$12/user/mo&lt;/td&gt;
&lt;td&gt;Admin panels, dashboards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LaunchDarkly&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Feature Flags&lt;/td&gt;
&lt;td&gt;Developer tier&lt;/td&gt;
&lt;td&gt;$10/conn/mo&lt;/td&gt;
&lt;td&gt;Progressive rollouts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Neon&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Serverless Postgres&lt;/td&gt;
&lt;td&gt;100 CU-hours&lt;/td&gt;
&lt;td&gt;$5/month&lt;/td&gt;
&lt;td&gt;Dev/test databases&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;1. MojoAuth: Passwordless Authentication Platform&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Website:&lt;/strong&gt; mojoauth.com&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; MojoAuth provides a unified API for passwordless authentication methods including Passkeys, Magic Links, OTPs (Email, SMS, WhatsApp), and enterprise SSO. It replaces months of custom authentication development with production-ready infrastructure that integrates in hours.&lt;/p&gt;
&lt;h3&gt;Key Features&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Multiple authentication methods:&lt;/strong&gt; Passkeys/WebAuthn, Magic Links, Email OTP, SMS OTP, WhatsApp OTP, TOTP/HOTP, and Social Login through a single unified API.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enterprise SSO integration:&lt;/strong&gt; SAML 2.0 and OIDC support for connecting to corporate identity providers including Okta, Azure AD, and Google Workspace.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-factor authentication:&lt;/strong&gt; Layer additional security with SMS, TOTP, or push notifications without rebuilding authentication flows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Developer SDKs:&lt;/strong&gt; Production-ready libraries for JavaScript/Node.js, Python, Go, .NET, and React with comprehensive documentation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Self-enrollment and recovery:&lt;/strong&gt; Users manage their own passwordless credentials without requiring support intervention.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Pricing (March 2026)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Free Plan:&lt;/strong&gt; Up to 25,000 Monthly Active Users. Includes Email OTP, Magic Links, Social Login, and basic features.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Business Pro:&lt;/strong&gt; Starting at $50/month. Scales to 500,000 MAUs at approximately $1,700/month. Includes all authentication methods, enterprise SSO, custom branding, and priority support.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enterprise:&lt;/strong&gt; Custom pricing. Includes dedicated infrastructure, SLA guarantees, and compliance certifications.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Engineering Time Saved&lt;/h3&gt;
&lt;p&gt;Building a secure, production-ready authentication system typically requires 3 to 6 months of engineering time. This includes handling token management, rate limiting, account recovery flows, and staying current with evolving security threats. MojoAuth claims integration in under a day with up to 75% faster time-to-market compared to building in-house. The platform reports 99.9999% uptime SLA and claims to reduce support tickets related to authentication by 80%.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. Gopher MCP: AI Agent Security Gateway&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Website:&lt;/strong&gt; gopher.security&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; Gopher MCP provides enterprise-grade security infrastructure for Model Context Protocol (MCP) deployments. As AI agents gain access to production databases, APIs, and internal tools, Gopher acts as a security layer that inspects every tool call and enforces granular access policies.&lt;/p&gt;
&lt;h3&gt;Key Features&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;4D Security Framework:&lt;/strong&gt; Deep inspection and threat detection, context-aware access control, granular policy enforcement, and post-quantum end-to-end encryption.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP threat protection:&lt;/strong&gt; Defends against tool poisoning, puppet attacks, prompt injection, and malicious external resource loading.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API schema to MCP server:&lt;/strong&gt; Upload Swagger, Postman, or OpenAPI schemas to deploy secure MCP servers in minutes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Post-quantum encryption:&lt;/strong&gt; P2P connections use quantum-resistant cryptographic algorithms to protect against both current and future threats.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compliance support:&lt;/strong&gt; Built-in audit logging and policy controls for SOC 2, GDPR, and HIPAA requirements.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Pricing (March 2026)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Free Plan:&lt;/strong&gt; 1 MCP server, 1,000 tool calls/month, 5GB data transfer, community support.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Starter:&lt;/strong&gt; $199/month ($179/month annual). 3 MCP servers, 1M tool calls/month, 5TB data transfer, multi-environment support.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Business:&lt;/strong&gt; $999/month ($899/month annual). 10 MCP servers, 5M tool calls/month, 25TB data transfer, advanced IAM, SSO, and compliance reports.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enterprise:&lt;/strong&gt; Custom pricing. Unlimited servers, dedicated infrastructure, phone support, and custom integrations.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Engineering Time Saved&lt;/h3&gt;
&lt;p&gt;The MCP authorization specification has evolved rapidly through 2025 with major revisions adding OAuth 2.1, Cross App Access protocols, and machine-to-machine authentication. Building spec-compliant MCP authentication in-house typically requires 6 to 12 weeks of dedicated development. Gopher claims deployment in under 30 minutes with the platform handling ongoing spec compliance updates automatically.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. Port.io: Internal Developer Platform&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Website:&lt;/strong&gt; port.io&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; Port.io is an internal developer portal that provides a unified view of your software catalog, self-service infrastructure provisioning, and developer experience scorecards. It replaces the fragmented tooling that forces developers to context-switch between AWS console, Datadog, PagerDuty, and dozens of other platforms.&lt;/p&gt;
&lt;h3&gt;Key Features&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dynamic software catalog:&lt;/strong&gt; Define custom blueprints that model your specific SDLC, with relationships between services, teams, and resources.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Self-service actions:&lt;/strong&gt; Developers provision infrastructure, create environments, and perform day-2 operations without filing tickets.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scorecards and standards:&lt;/strong&gt; Define quality, maturity, and production readiness standards with automated compliance tracking.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workflow automation:&lt;/strong&gt; Long-running and asynchronous actions with TTL support and run logs visible to developers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integrations:&lt;/strong&gt; Native connectors for Terraform, ArgoCD, Kubernetes, GitHub, Datadog, PagerDuty, and most enterprise tooling.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Pricing (March 2026)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Free Plan:&lt;/strong&gt; 15 seats, 10,000 entities, 500 automation runs/month. No time limit.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Paid Plans:&lt;/strong&gt; Custom pricing based on team size and feature requirements. Contact sales for quotes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enterprise:&lt;/strong&gt; Flexible deployment options including dedicated tenancy and Private Link connectivity.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Engineering Time Saved&lt;/h3&gt;
&lt;p&gt;Port.io reports customers reducing environment provisioning time from 30 minutes to 30 seconds. According to their State of Internal Developer Portals report, 75% of developers lose 6 to 15 hours weekly to tool sprawl. For a team of 50 engineers, this translates to nearly $1 million in lost productivity annually. Port raised $100M in December 2025 at an $800M valuation, signaling strong enterprise adoption.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. Pulumi: Infrastructure as Code in Real Languages&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Website:&lt;/strong&gt; pulumi.com&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; Pulumi lets engineering teams define cloud infrastructure using TypeScript, Python, Go, C#, Java, or YAML instead of domain-specific languages. This means loops, conditions, functions, unit tests, and package managers work natively with infrastructure code.&lt;/p&gt;
&lt;h3&gt;Key Features&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Real programming languages:&lt;/strong&gt; Write infrastructure in TypeScript, Python, Go, C#, Java, or YAML with full IDE support, type checking, and debugging.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pulumi Neo AI agent:&lt;/strong&gt; Generate infrastructure from natural language requirements, review PRs, and debug deployments with organizational context.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;120+ cloud providers:&lt;/strong&gt; Deploy to AWS, Azure, Google Cloud, Kubernetes, and over 120 providers through a unified model.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Policy as code:&lt;/strong&gt; CrossGuard lets you define security, compliance, and cost controls that run during previews and updates.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Secrets management:&lt;/strong&gt; Pulumi ESC provides a single interface for all secrets with connections to HashiCorp Vault, AWS Secrets Manager, and Azure Key Vault.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Pricing (March 2026)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Individual (Free):&lt;/strong&gt; Unlimited updates, automatic state management, and core features. Free forever for individuals.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Team:&lt;/strong&gt; $0.0005 per Pulumi Credit (one resource managed for one hour). Includes staging environments and release management.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enterprise:&lt;/strong&gt; Starting at $32,850/year via AWS Marketplace. SAML SSO, self-hosted options, and source control integrations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Business Critical:&lt;/strong&gt; Starting at $50,000/year via Azure Marketplace. 150 concurrent deployments and volume pricing.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Engineering Time Saved&lt;/h3&gt;
&lt;p&gt;Werner Enterprises reported reducing provisioning time from 3 days to 4 hours using Pulumi. The platform claims one organization expanded infrastructure contributors from 1-2 people to over 40 active engineers because developers could use familiar programming languages instead of learning specialized DSLs like HCL.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. Retool: Internal Tools Development Platform&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Website:&lt;/strong&gt; retool.com&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; Retool combines drag-and-drop UI building with AI assistance to create internal tools such as admin panels, dashboards, and workflow apps. It connects to databases and APIs natively, eliminating the need to build CRUD interfaces from scratch.&lt;/p&gt;
&lt;h3&gt;Key Features&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;100+ pre-built components:&lt;/strong&gt; Tables, forms, charts, maps, and calendars that connect to data sources with minimal configuration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Native database connectors:&lt;/strong&gt; Direct connections to PostgreSQL, MySQL, MongoDB, Snowflake, and 40+ other data sources with SSH tunneling and SSL support.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI-powered app building:&lt;/strong&gt; Generate apps from prompts, then customize with code or visual editing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workflows and automation:&lt;/strong&gt; Build automated processes with scheduling, webhooks, and conditional logic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mobile apps:&lt;/strong&gt; Build native iOS and Android apps alongside web applications from the same platform.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Pricing (March 2026)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Free:&lt;/strong&gt; 5 users, unlimited apps, 500 workflow runs/month, 5GB storage.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Team:&lt;/strong&gt; $12/month per standard user, $7/month per end user. Includes staging environments and version history.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Business:&lt;/strong&gt; $65/month per standard user, $18/month per end user. Adds audit logging, unlimited environments, and embedded apps.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enterprise:&lt;/strong&gt; Custom pricing. SAML/OIDC SSO, self-hosted deployment, custom SLAs.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Engineering Time Saved&lt;/h3&gt;
&lt;p&gt;Retool claims teams can build internal tools 10x faster than traditional development. Used by Amazon, DoorDash, OpenAI, and Mercedes-Benz for mission-critical internal applications. One customer reported building a complete dashboard with 10 data sources in 24 hours. The platform is particularly effective for replacing Google Sheets or legacy internal tools that have become unstable.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. LaunchDarkly: Feature Flag Management&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Website:&lt;/strong&gt; launchdarkly.com&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; LaunchDarkly provides feature flags that act as on/off switches for features in production. Teams can deploy code continuously while controlling exactly who sees new features through gradual rollouts, A/B tests, and instant kill switches.&lt;/p&gt;
&lt;h3&gt;Key Features&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Feature flags:&lt;/strong&gt; Toggle features on or off for specific user segments without redeploying code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Progressive rollouts:&lt;/strong&gt; Gradually expose features to increasing percentages of users while monitoring for issues.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Experimentation:&lt;/strong&gt; Run A/B tests and funnel optimization experiments with statistical analysis.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Release Guardian:&lt;/strong&gt; Monitor critical flag changes in release pipelines and automatically detect regressions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mobile Lifecycle Assistant:&lt;/strong&gt; Deliver personalized mobile experiences and fix bugs without waiting for app store review cycles.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Pricing (March 2026)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Developer (Free):&lt;/strong&gt; 1 project, 3 environments. Feature flagging for individual projects.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Foundation:&lt;/strong&gt; $10/month per service connection (annual) or $12/month (monthly). Plus $10 per 1,000 client-side MAUs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enterprise:&lt;/strong&gt; Custom pricing. Typically $20,000 to $120,000/year depending on scale and feature requirements.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Engineering Time Saved&lt;/h3&gt;
&lt;p&gt;LaunchDarkly enables instant rollbacks when issues are detected. Instead of a full deployment rollback taking 15 to 30 minutes, a feature flag toggle takes seconds. The platform is trusted by over 5,500 customers. Users report that the ability to decouple deployment from release fundamentally changes how teams ship software, allowing continuous integration without continuous exposure to users until features are ready.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. Neon: Serverless PostgreSQL&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Website:&lt;/strong&gt; neon.tech&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; Neon provides serverless PostgreSQL that separates compute from storage, enabling instant database branching, scale-to-zero for idle databases, and automatic scaling based on actual usage. It eliminates the operational overhead of managing database infrastructure.&lt;/p&gt;
&lt;h3&gt;Key Features&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Database branching:&lt;/strong&gt; Create instant copy-on-write branches for development, testing, and CI/CD pipelines without duplicating data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scale to zero:&lt;/strong&gt; Databases automatically suspend after inactivity (5 minutes default), eliminating charges for idle resources.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Autoscaling compute:&lt;/strong&gt; Automatically scale between minimum and maximum compute units based on query load.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Point-in-time restore:&lt;/strong&gt; Instant recovery to any point in time within the configured retention window.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full PostgreSQL compatibility:&lt;/strong&gt; Standard Postgres with all extensions, no proprietary modifications.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Pricing (March 2026)&lt;/h3&gt;
&lt;p&gt;Following Databricks acquisition in May 2025, Neon reduced compute costs 15 to 25% and storage from $1.75 to $0.35/GB-month.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Free:&lt;/strong&gt; 100 CU-hours per project (doubled from 50 in October 2025), 0.5GB per project, up to 100 projects.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Launch:&lt;/strong&gt; $5/month minimum. $0.106 per CU-hour, $0.35 per GB-month storage. Up to 16 vCPU and 64GB RAM.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scale:&lt;/strong&gt; $700/month base. $0.222 per CU-hour, includes 1,000 CU-hours and 500GB storage. Up to 56 vCPU and 224GB RAM.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Engineering Time Saved&lt;/h3&gt;
&lt;p&gt;Database provisioning traditionally requires DevOps involvement for each new environment. Neon branches create isolated database copies in seconds using copy-on-write technology. For a multi-tenant SaaS with 100 customer databases, one analysis showed Neon costing approximately $4,380/month compared to $9,985/month for equivalent Azure Database for PostgreSQL Flexible Server instances. The serverless architecture eliminates infrastructure management overhead entirely.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Frequently Asked Questions&lt;/h2&gt;
&lt;h3&gt;How do I decide between building infrastructure in-house versus buying?&lt;/h3&gt;
&lt;p&gt;Build in-house when the component is a core differentiator for your product. Buy when the component is commodity infrastructure that every company in your industry needs. Authentication, feature flags, internal tooling, and database management are rarely competitive differentiators. The engineering time spent building these could go toward features that actually distinguish your product.&lt;/p&gt;
&lt;h3&gt;What is the typical ROI calculation for enterprise infrastructure tools?&lt;/h3&gt;
&lt;p&gt;Calculate the fully-loaded cost of engineering time for the alternative. If building a passwordless authentication system requires 4 months of senior engineer time at $200,000/year fully loaded, that represents approximately $67,000 in engineering cost alone. Compare this to annual subscription costs. Most infrastructure tools pay for themselves within the first quarter through avoided engineering time.&lt;/p&gt;
&lt;h3&gt;Do third-party infrastructure tools create vendor lock-in risks?&lt;/h3&gt;
&lt;p&gt;Lock-in risk varies by tool category. Authentication providers using standard protocols (SAML, OIDC) offer relatively easy migration paths. Feature flag platforms have more switching costs since flags become embedded throughout your codebase. Infrastructure as code tools like Pulumi provide escape hatches through code generation and multi-provider support. Evaluate migration complexity during vendor selection.&lt;/p&gt;
&lt;h3&gt;How do these tools affect compliance requirements like SOC 2 and HIPAA?&lt;/h3&gt;
&lt;p&gt;Enterprise infrastructure tools often simplify compliance by providing pre-built audit logging, access controls, and encryption that would otherwise require custom implementation. When you build in-house, your team bears full responsibility for security and compliance of that code. With certified vendors, you inherit their compliance posture and can reference their certifications in your own security assessments.&lt;/p&gt;
&lt;h3&gt;What is the best way to evaluate multiple tools in the same category?&lt;/h3&gt;
</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/15555/7-enterprise-infrastructure-tools-that-eliminate-months-of-engineering-work</guid>
<pubDate>Sun, 26 Apr 2026 03:44:00 +0000</pubDate>
</item>
<item>
<title>Passkeys vs Bots: Do They Really Solve the Human Verification Problem?</title>
<link>https://coderlegion.com/15285/passkeys-vs-bots-do-they-really-solve-the-human-verification-problem</link>
<description>
&lt;p&gt;Passkeys authenticate users securely, but they do not prove a user is human. Passkeys replace passwords with device-bound cryptographic authentication. They prevent phishing, credential theft, and password reuse attacks.&lt;/p&gt;
&lt;p&gt;However, passkeys only verify identity at login, not behavior after login. A bot can still operate inside a valid authenticated session. Automated browsers, scripts, and AI agents can interact with systems after authentication succeeds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Passkeys solve authentication problems, not human verification problems.&lt;/strong&gt;&lt;br&gt;Human verification requires analyzing behavior, context, and request patterns.&lt;/p&gt;
&lt;p&gt;Modern security therefore separates two different questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Who logged in?&lt;/strong&gt; &amp;rarr; authentication&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Is this activity human?&lt;/strong&gt; &amp;rarr; bot detection&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Passkeys answer the first question extremely well. They do not answer the second question.&lt;/p&gt;
&lt;p&gt;That distinction is critical for modern applications facing automated attacks. Bots today perform actions such as scraping APIs, creating fake accounts, and automating purchases. Many of these actions happen after authentication, not before it.&lt;/p&gt;
&lt;p&gt;This means that even a system protected with passkeys can still experience bot abuse.&lt;/p&gt;
&lt;p&gt;To fully protect modern applications, security must combine multiple layers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Strong authentication (passkeys)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Bot detection systems&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Behavioral analysis&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Runtime identity verification&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Understanding where passkeys help&amp;mdash;and where they do not&amp;mdash;is essential for building secure systems.&lt;/p&gt;
&lt;h2&gt;2. Quick TL;DR&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Passkeys replace passwords with device-bound authentication.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Passkeys prevent phishing and credential theft attacks.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Passkeys verify identity but do not verify human behavior.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Bots can operate inside authenticated sessions.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Authentication does not prove human intent.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;CAPTCHA was originally designed to detect bots.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Modern bots can solve or bypass many CAPTCHA challenges.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Bot detection relies on behavioral and device signals.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Passkeys improve authentication security but not bot detection.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Modern security requires authentication plus runtime verification.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. The Core Question: Do Passkeys Prove You Are Human?&lt;/h2&gt;
&lt;p&gt;The short answer is &lt;strong&gt;no&lt;/strong&gt;. Passkeys prove that a user controls a device credential. They do not prove that the actor using that device is human.&lt;/p&gt;
&lt;p&gt;Passkeys verify identity ownership, not human intent.&lt;br&gt;Human verification and authentication are fundamentally different security goals.&lt;/p&gt;
&lt;p&gt;Many developers confuse these two concepts because both appear during login flows. However, they solve different problems in system security.&lt;/p&gt;
&lt;h3&gt;Authentication vs Human Verification&lt;/h3&gt;
&lt;p&gt;Authentication answers one specific question:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Who is the user?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Human verification answers a different question:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is this activity being performed by a human or a bot?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;These two goals require completely different technologies.&lt;/p&gt;
&lt;table class=&quot;border-collapse border border-muted&quot; style=&quot;min-width: 75px;&quot;&gt;&lt;colgroup&gt;&lt;col style=&quot;min-width: 25px;&quot;&gt;&lt;col style=&quot;min-width: 25px;&quot;&gt;&lt;col style=&quot;min-width: 25px;&quot;&gt;&lt;/colgroup&gt;&lt;tbody&gt;&lt;tr class=&quot;border-b border-muted&quot;&gt;&lt;th class=&quot;border border-muted bg-muted px-2 py-1 text-left font-medium&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Security Goal&lt;/p&gt;&lt;/th&gt;&lt;th class=&quot;border border-muted bg-muted px-2 py-1 text-left font-medium&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Purpose&lt;/p&gt;&lt;/th&gt;&lt;th class=&quot;border border-muted bg-muted px-2 py-1 text-left font-medium&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Example Technology&lt;/p&gt;&lt;/th&gt;&lt;/tr&gt;&lt;tr class=&quot;border-b border-muted&quot;&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Authentication&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Verify user identity&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Passwords, Passkeys, OAuth&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr class=&quot;border-b border-muted&quot;&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Authorization&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Control resource access&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;RBAC, Access policies&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr class=&quot;border-b border-muted&quot;&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Human Verification&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Detect automated behavior&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;CAPTCHA, Behavior analysis&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;

&lt;p&gt;Authentication confirms identity credentials.&lt;br&gt;Human verification analyzes interaction behavior.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Authentication proves identity.&lt;/strong&gt;&lt;br&gt;&lt;strong&gt;Human verification proves intent.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;Identity vs Intent&lt;/h3&gt;
&lt;p&gt;Passkeys solve the identity problem extremely well. They replace passwords with cryptographic authentication tied to a device.&lt;/p&gt;
&lt;p&gt;When a user signs in with a passkey:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The device proves ownership of a private key&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The server verifies the cryptographic signature&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The user is authenticated successfully&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At that moment, the system knows:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A valid credential was used.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;However, the system does not know:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Whether the interaction is automated&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Whether a bot controls the browser&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Whether an AI agent triggered the action&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Passkeys prove device ownership, not human presence.&lt;/p&gt;
&lt;h3&gt;Why This Distinction Matters&lt;/h3&gt;
&lt;p&gt;Modern bots rarely try to guess passwords anymore. Instead, they operate through automated browsers or scripts that interact with websites exactly like real users.&lt;/p&gt;
&lt;p&gt;Examples include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;automated account creation&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ticket scalping bots&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ecommerce checkout bots&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;API scraping bots&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These bots can operate after authentication succeeds.&lt;/p&gt;
&lt;p&gt;Most automated abuse happens inside authenticated sessions.&lt;/p&gt;
&lt;p&gt;This means a system using passkeys can still experience bot-driven activity.&lt;/p&gt;
&lt;h3&gt;A Simple Example&lt;/h3&gt;
&lt;p&gt;Imagine a user logs into a website using a passkey.&lt;/p&gt;
&lt;p&gt;The authentication process is secure and successful.&lt;/p&gt;
&lt;p&gt;Now consider two scenarios:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;A real human navigates the site normally.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A bot script controls the browser and performs automated actions.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;From the authentication system's perspective, both sessions look valid.&lt;/p&gt;
&lt;p&gt;The passkey successfully verified identity in both cases.&lt;/p&gt;
&lt;p&gt;Authentication alone cannot distinguish humans from automated behavior.&lt;/p&gt;
&lt;h3&gt;The Key Insight&lt;/h3&gt;
&lt;p&gt;Passkeys are a major improvement for authentication security. They eliminate passwords and prevent many common attacks.&lt;/p&gt;
&lt;p&gt;However, they do not replace bot detection systems.&lt;/p&gt;
&lt;p&gt;Passkeys solve authentication.&lt;br&gt;Human verification requires additional security layers.&lt;/p&gt;
&lt;p&gt;Understanding this difference is essential for building secure modern applications.&lt;/p&gt;
&lt;h2&gt;4. What Problem Passkeys Actually Solve&lt;/h2&gt;
&lt;p&gt;Passkeys were designed to solve the weaknesses of password-based authentication. They replace passwords with device-bound cryptographic credentials that are resistant to phishing and credential theft.&lt;/p&gt;
&lt;p&gt;Passkeys solve authentication security problems, not bot detection problems.&lt;br&gt;They eliminate passwords while improving login success and security.&lt;/p&gt;
&lt;p&gt;Passkeys are based on the FIDO and WebAuthn standards. These standards use public-key cryptography instead of shared secrets like passwords.&lt;/p&gt;
&lt;p&gt;When a user registers a passkey:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The device generates a public-private key pair.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The private key stays securely on the user&amp;rsquo;s device.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The public key is stored on the server.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;During login, the server sends a challenge that the device signs with the private key. The server verifies the signature using the stored public key.&lt;/p&gt;
&lt;p&gt;The private key never leaves the user&amp;rsquo;s device.&lt;/p&gt;
&lt;h3&gt;Password Elimination&lt;/h3&gt;
&lt;p&gt;Passwords have been the weakest link in authentication for decades.&lt;/p&gt;
&lt;p&gt;Common password problems include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;password reuse across services&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;weak passwords chosen by users&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;password database breaches&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;phishing attacks stealing credentials&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Microsoft reports that more than 99% of identity attacks involve passwords.&lt;/p&gt;
&lt;p&gt;Passkeys remove passwords entirely from the authentication process.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No password means nothing for attackers to steal or guess.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;Phishing Resistance&lt;/h3&gt;
&lt;p&gt;Traditional phishing attacks trick users into entering credentials on fake websites.&lt;/p&gt;
&lt;p&gt;Passkeys prevent this because authentication is tied to the website&amp;rsquo;s domain.&lt;/p&gt;
&lt;p&gt;If a phishing site tries to trigger passkey authentication:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The browser detects the domain mismatch.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The passkey will not authenticate.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Passkeys are inherently phishing resistant.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This makes them far more secure than passwords, SMS OTPs, or many MFA systems.&lt;/p&gt;
&lt;h3&gt;Device-Bound Authentication&lt;/h3&gt;
&lt;p&gt;Passkeys are stored securely on the user&amp;rsquo;s device.&lt;/p&gt;
&lt;p&gt;They rely on hardware-backed security features such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;secure enclaves on smartphones&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;trusted platform modules (TPM) on computers&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;biometric authentication like Face ID or fingerprint&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The authentication process requires the user to unlock the device.&lt;/p&gt;
&lt;p&gt;This ensures the device owner is present during login.&lt;/p&gt;
&lt;p&gt;Passkeys combine device possession with biometric verification.&lt;/p&gt;
&lt;h3&gt;Improved User Experience&lt;/h3&gt;
&lt;p&gt;Passwords introduce friction during login.&lt;/p&gt;
&lt;p&gt;Users often forget passwords, reset them, or struggle with password policies.&lt;/p&gt;
&lt;p&gt;Passkeys simplify login significantly.&lt;/p&gt;
&lt;p&gt;Typical passkey login flow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;User enters username or email.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Device prompts biometric verification.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Authentication completes instantly.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Microsoft reports that passwordless authentication achieves 95% login success rates, compared to much lower success rates for password logins.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Passkeys improve both security and usability.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;Protection Against Credential Theft&lt;/h3&gt;
&lt;p&gt;Credential stuffing attacks rely on leaked password databases.&lt;/p&gt;
&lt;p&gt;Attackers reuse stolen passwords across multiple services.&lt;/p&gt;
&lt;p&gt;Passkeys eliminate this attack vector because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;There is no shared secret to reuse.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Each passkey is unique to a specific domain.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even if one service is compromised, the passkey cannot be reused elsewhere.&lt;/p&gt;
&lt;p&gt;Passkeys eliminate credential reuse attacks entirely.&lt;/p&gt;
&lt;h2&gt;5. What Problem Passkeys Do NOT Solve&lt;/h2&gt;
&lt;p&gt;Passkeys dramatically improve authentication security. They prevent phishing, password theft, and credential reuse. However, they do not stop automated behavior or bots operating after login.&lt;/p&gt;
&lt;p&gt;Passkeys verify identity credentials, not user behavior.&lt;br&gt;Bots can still interact with systems after authentication succeeds.&lt;/p&gt;
&lt;p&gt;This distinction is important because most modern automated attacks no longer focus on breaking authentication. Instead, they target the application layer after login.&lt;/p&gt;
&lt;h3&gt;Passkeys Do Not Detect Automated Browsers&lt;/h3&gt;
&lt;p&gt;Modern bots often run inside automated browsers that behave like real users.&lt;/p&gt;
&lt;p&gt;These tools include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Selenium&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Puppeteer&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Playwright&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;headless Chrome&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These automated browsers can load web pages, click buttons, submit forms, and interact with applications exactly like a human user.&lt;/p&gt;
&lt;p&gt;Once authentication succeeds, the system sees only normal requests.&lt;/p&gt;
&lt;p&gt;Passkeys cannot distinguish between human actions and automated browser actions.&lt;/p&gt;
&lt;h3&gt;Passkeys Do Not Detect Scripted Sessions&lt;/h3&gt;
&lt;p&gt;Many automated attacks occur inside valid authenticated sessions.&lt;/p&gt;
&lt;p&gt;Examples include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;automated product purchases&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;fake account activity&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;automated social media posting&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;scraping user data&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These actions happen after authentication has already succeeded.&lt;/p&gt;
&lt;p&gt;From the server&amp;rsquo;s perspective, the requests are coming from a valid session.&lt;/p&gt;
&lt;p&gt;Passkeys do not evaluate how a session is being used.&lt;/p&gt;
&lt;h3&gt;Passkeys Do Not Detect API Automation&lt;/h3&gt;
&lt;p&gt;Modern applications rely heavily on APIs.&lt;/p&gt;
&lt;p&gt;Bots often interact directly with APIs instead of web interfaces.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;automated scraping tools&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;mobile app reverse engineering&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;automated account creation&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;bulk data extraction&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the API accepts valid tokens or session cookies, the requests may look legitimate.&lt;/p&gt;
&lt;p&gt;Passkeys secure login but do not analyze API behavior.&lt;/p&gt;
&lt;h3&gt;Passkeys Do Not Detect AI Agents&lt;/h3&gt;
&lt;p&gt;The rise of AI-driven automation introduces a new category of automated activity.&lt;/p&gt;
&lt;p&gt;AI agents can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;navigate websites&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;interact with forms&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;simulate user actions&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;trigger workflows automatically&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These agents may operate inside authenticated sessions.&lt;/p&gt;
&lt;p&gt;Because authentication was valid, the system cannot easily distinguish between human actions and automated actions.&lt;/p&gt;
&lt;p&gt;Passkeys authenticate the device, not the actor controlling it.&lt;/p&gt;
&lt;h3&gt;Passkeys Do Not Analyze Behavior&lt;/h3&gt;
&lt;p&gt;Human verification systems often rely on behavior signals.&lt;/p&gt;
&lt;p&gt;These signals may include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;mouse movement patterns&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;typing speed&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;navigation patterns&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;request timing&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Passkeys do not analyze these signals.&lt;/p&gt;
&lt;p&gt;They only verify the cryptographic credential during login.&lt;/p&gt;
&lt;p&gt;Passkeys do not evaluate behavioral patterns.&lt;/p&gt;
&lt;h3&gt;Passkeys Do Not Detect Intent&lt;/h3&gt;
&lt;p&gt;A valid login does not guarantee safe behavior.&lt;/p&gt;
&lt;p&gt;A user may log in successfully and then perform malicious actions.&lt;/p&gt;
&lt;p&gt;Examples include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;scraping competitor data&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;abusing APIs&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;automating high-volume transactions&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;launching spam campaigns&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Authentication confirms identity, but it does not validate intent.&lt;/p&gt;
&lt;p&gt;Intent can only be inferred through behavioral analysis.&lt;/p&gt;
&lt;h3&gt;The Key Insight&lt;/h3&gt;
&lt;p&gt;Passkeys solve one specific problem extremely well: secure authentication.&lt;/p&gt;
&lt;p&gt;They eliminate passwords and significantly reduce account takeover attacks.&lt;/p&gt;
&lt;p&gt;However, modern systems face additional challenges that occur after authentication.&lt;/p&gt;
&lt;p&gt;Bots, automated scripts, and AI agents often operate inside valid sessions.&lt;/p&gt;
&lt;p&gt;Passkeys secure the login event, not the entire user session.&lt;/p&gt;
&lt;p&gt;To detect automation, systems need additional mechanisms such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;bot detection systems&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;behavioral analysis&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;device fingerprinting&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;runtime identity evaluation&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Understanding these limitations is essential when designing modern security architectures.&lt;/p&gt;
&lt;p&gt;6. How Bot Detection Actually Works&lt;/p&gt;
&lt;p&gt;Bot detection systems are designed to distinguish automated traffic from human users. Unlike authentication systems, bot detection focuses on analyzing behavior, patterns, and environmental signals.&lt;/p&gt;
&lt;p&gt;Bot detection identifies automated behavior, not identity credentials.&lt;br&gt;Human verification depends on analyzing how a system is used.&lt;/p&gt;
&lt;p&gt;Modern bot detection systems combine multiple techniques to detect suspicious activity.&lt;/p&gt;
&lt;h3&gt;CAPTCHA Challenges&lt;/h3&gt;
&lt;p&gt;CAPTCHA systems were originally designed to separate humans from automated scripts. The name CAPTCHA stands for Completely Automated Public Turing test to tell Computers and Humans Apart.&lt;/p&gt;
&lt;p&gt;CAPTCHA systems challenge users with tasks that are assumed to be easy for humans but difficult for machines.&lt;/p&gt;
&lt;p&gt;Examples include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;selecting images with traffic lights&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;typing distorted characters&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;solving puzzles&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These tests attempt to ensure that the actor interacting with the system is human.&lt;/p&gt;
&lt;p&gt;CAPTCHA systems challenge interaction rather than identity.&lt;/p&gt;
&lt;h3&gt;Behavioral Analysis&lt;/h3&gt;
&lt;p&gt;Behavioral analysis examines how users interact with a website or application.&lt;/p&gt;
&lt;p&gt;Human users behave differently from automated scripts.&lt;/p&gt;
&lt;p&gt;Common behavioral signals include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;mouse movement patterns&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;typing speed and rhythm&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;navigation flow across pages&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;click timing and interaction speed&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bots often generate extremely regular patterns that differ from human behavior.&lt;/p&gt;
&lt;p&gt;Human behavior contains randomness that automated scripts struggle to replicate.&lt;/p&gt;
&lt;p&gt;Behavioral analysis systems monitor activity throughout the session to detect anomalies.&lt;/p&gt;
&lt;h3&gt;Device Fingerprinting&lt;/h3&gt;
&lt;p&gt;Device fingerprinting identifies unique characteristics of a device and browser environment.&lt;/p&gt;
&lt;p&gt;This technique collects signals such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;browser type and version&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;operating system&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;screen resolution&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;installed fonts and plugins&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These attributes can create a fingerprint that helps identify automated tools.&lt;/p&gt;
&lt;p&gt;Bots often use headless browsers or simplified environments that differ from real user devices.&lt;/p&gt;
&lt;p&gt;Device fingerprints help detect suspicious environments.&lt;/p&gt;
&lt;h3&gt;Rate Limiting&lt;/h3&gt;
&lt;p&gt;Rate limiting controls how frequently requests can be made to a system.&lt;/p&gt;
&lt;p&gt;Bots often send large volumes of requests in short periods of time.&lt;/p&gt;
&lt;p&gt;Rate limiting can prevent excessive traffic from automated sources.&lt;/p&gt;
&lt;p&gt;Examples include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;limiting login attempts&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;restricting API request frequency&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;blocking repeated requests from the same IP address&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rate limiting reduces automated abuse by controlling request volume.&lt;/p&gt;
&lt;h3&gt;Network Reputation Analysis&lt;/h3&gt;
&lt;p&gt;Many bot detection systems analyze the reputation of IP addresses and networks.&lt;/p&gt;
&lt;p&gt;Signals may include:&amp;lt;/p&lt;/p&gt;
</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/15285/passkeys-vs-bots-do-they-really-solve-the-human-verification-problem</guid>
<pubDate>Sat, 25 Apr 2026 06:55:07 +0000</pubDate>
</item>
<item>
<title>How to Implement Passwordless Authentication to Boost User Conversion</title>
<link>https://coderlegion.com/15275/how-to-implement-passwordless-authentication-to-boost-user-conversion</link>
<description>
&lt;p&gt;Passwordless authentication is already widely deployed across modern applications. It&amp;rsquo;s the single most effective tool you have to kill the friction that&amp;rsquo;s currently choking your user journey.Passwords create friction in signup and login experiences.Passwordless authentication removes shared secrets from the authentication process.&lt;/p&gt;
&lt;p&gt;Think about the last time you signed up for a service. You&amp;rsquo;re ready to buy, you&amp;rsquo;re excited to use the product, and then&amp;mdash;&lt;em&gt;bam&lt;/em&gt;. You&amp;rsquo;re hit with a password requirement. Needs 12 characters, a special symbol, a blood sacrifice, and a partridge in a pear tree. You forget it five minutes later, hit the &quot;forgot password&quot; loop, and suddenly, you&amp;rsquo;re done. You&amp;rsquo;re gone.&lt;/p&gt;
&lt;p&gt;By replacing that archaic, memory-heavy chore with cryptographic proof, you stop bleeding customers at the exact moment they&amp;rsquo;re ready to engage. Don&amp;rsquo;t take my word for it, the &lt;a rel=&quot;nofollow&quot; href=&quot;https://mojoauth.com/data-and-research-reports/passwordless-conversion-impact-report-2026/&quot;&gt;Passwordless Conversion Impact Report 2026&lt;/a&gt; makes it clear: the faster the entry, the higher the lifetime value. Speed isn't just a luxury; it&amp;rsquo;s a conversion multiplier.&lt;/p&gt;
&lt;h2&gt;The Business Case: Is Passwordless Just About Security?&lt;/h2&gt;
&lt;p&gt;If you think this is purely an IT security play, you&amp;rsquo;re missing the point. Sure, ditching shared secrets helps you avoid the nightmares documented in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.ibm.com/reports/data-breach&quot;&gt;IBM Cost of Data Breach Report&lt;/a&gt;, but the real win happens on your P&amp;amp;L.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s talk about your help desk. How much time and money does your team waste on &quot;I forgot my password&quot; tickets? It&amp;rsquo;s a massive, soul-crushing tax on your support operations. Switching to biometric or device-bound authentication effectively nukes that ticket category. Your team can finally focus on actual problems instead of playing the role of digital locksmiths.&lt;/p&gt;
&lt;p&gt;Then there&amp;rsquo;s the conversion math. Every single field you force a user to fill out is a potential exit ramp. When you stop demanding they create, verify, and store yet another password, the cognitive load plummets. Users aren&amp;rsquo;t balancing the value of your app against the headache of managing another account. You turn the login process from a barrier into a feature.&lt;/p&gt;
&lt;h2&gt;Password-Based vs Passwordless Authentication&lt;/h2&gt;
&lt;table class=&quot;border-collapse border border-muted&quot; style=&quot;min-width: 75px;&quot;&gt;&lt;colgroup&gt;&lt;col style=&quot;min-width: 25px;&quot;&gt;&lt;col style=&quot;min-width: 25px;&quot;&gt;&lt;col style=&quot;min-width: 25px;&quot;&gt;&lt;/colgroup&gt;&lt;tbody&gt;&lt;tr class=&quot;border-b border-muted&quot;&gt;&lt;th class=&quot;border border-muted bg-muted px-2 py-1 text-left font-medium&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Feature&lt;/p&gt;&lt;/th&gt;&lt;th class=&quot;border border-muted bg-muted px-2 py-1 text-left font-medium&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Password Authentication&lt;/p&gt;&lt;/th&gt;&lt;th class=&quot;border border-muted bg-muted px-2 py-1 text-left font-medium&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Passwordless Authentication&lt;/p&gt;&lt;/th&gt;&lt;/tr&gt;&lt;tr class=&quot;border-b border-muted&quot;&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Credential Type&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Shared secret&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Cryptographic key&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr class=&quot;border-b border-muted&quot;&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Phishing Risk&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;High&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Very low&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr class=&quot;border-b border-muted&quot;&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Password Resets&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Frequent&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Rare&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr class=&quot;border-b border-muted&quot;&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;User Experience&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Friction-heavy&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Fast and seamless&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr class=&quot;border-b border-muted&quot;&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Credential Theft Risk&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;High&lt;/p&gt;&lt;/td&gt;&lt;td class=&quot;border border-muted px-2 py-1&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;p&gt;Minimal&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;

&lt;h2&gt;Demystifying the Tech: What Are Passkeys and FIDO2?&lt;/h2&gt;
&lt;p&gt;Passkeys are passwordless authentication credentials based on public-key cryptography.&lt;/p&gt;
&lt;p&gt;Passkeys replace passwords with a cryptographic key pair:&lt;/p&gt;
&lt;p&gt;a private key stored on the user&amp;rsquo;s device&lt;br&gt;a public key stored on the server&lt;/p&gt;
&lt;p&gt;During login, the device signs a cryptographic challenge to prove identity.&lt;/p&gt;
&lt;p&gt;Passkeys are resistant to phishing because the private key never leaves the user&amp;rsquo;s device. We&amp;rsquo;re moving away from &quot;shared secrets&quot;&amp;mdash;the old model where both you and the user hold a copy of a password. In the old world, if your database gets hacked, the keys to the kingdom are gone. In a passwordless world, the secret never actually leaves the user&amp;rsquo;s device.&lt;/p&gt;
&lt;p&gt;It sounds like magic, but it&amp;rsquo;s just math. It uses FIDO2 and WebAuthn standards&amp;mdash;basically a high-tech digital handshake.&lt;/p&gt;
&lt;img src=&quot;https://cdn.pseo.one/67b62b766899109fe72fb789/686ef578027b1d23f092b1cd/topics/69e0857634c26cd11cc524e8/da4a42b1-bb4a-48ed-9c9f-a0d2bcd282c1.webp&quot; width=&quot;100%&quot; style=&quot;width: 100%; display: block; float: left;&quot; alt=&quot;image&quot;&gt;

&lt;p&gt;When a user signs up, their device creates a pair of keys: one public, one private. The private key stays locked in the device&amp;rsquo;s secure enclave, accessible only by a fingerprint or face scan. The public key goes to your server. When they come back, your server sends a &quot;challenge,&quot; the device signs it, and your server checks the signature. No passwords. No phishing. No nonsense.&lt;/p&gt;
&lt;p&gt;Learn more in our guide:&lt;br&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://mojoauth.com/blog/what-are-passkeys-and-how-they-work/&quot;&gt;What are passkeys and how They Work&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Benefits of Passwordless Authentication&lt;/h2&gt;
&lt;p&gt;Passwordless authentication improves both security and user experience.&lt;/p&gt;
&lt;p&gt;Key benefits include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Eliminating Password Reuse&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Preventing Credential Phishing Attacks&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reducing Password Reset Support Tickets&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Improving Login Conversion Rates&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Simplifying Authentication Flows&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Organizations adopting passwordless authentication often see measurable improvements in both security posture and customer experience.&lt;/p&gt;
&lt;h2&gt;How to Design a Frictionless Authentication Flow&lt;/h2&gt;
&lt;p&gt;Here is where most companies screw it up: they turn their login page into a science project.&lt;/p&gt;
&lt;p&gt;If you want people to actually use this, keep it simple. It should feel like a native extension of the phone or laptop they already trust. Follow the &lt;a rel=&quot;nofollow&quot; href=&quot;https://fidoalliance.org/passkeys/&quot;&gt;best practices from the FIDO Alliance&lt;/a&gt; to keep your UI clean.&lt;/p&gt;
&lt;p&gt;Avoid &quot;the cliff.&quot; Don&amp;rsquo;t make the user choose between a password they know and some mysterious, scary biometric prompt. Use progressive disclosure. If they&amp;rsquo;re on a modern device, offer the passkey as the obvious, &quot;recommended&quot; path. Use human language: &quot;Use FaceID to sign in&quot; beats &quot;Authenticate via WebAuthn assertion&quot; every single time.&lt;/p&gt;
&lt;p&gt;And for heaven&amp;rsquo;s sake, give them feedback. If a biometric scan fails, don&amp;rsquo;t just throw a cryptic error code. Offer a secondary way in&amp;mdash;like a magic link&amp;mdash;without making them refresh the page or restart the whole process.&lt;/p&gt;
&lt;h2&gt;The Migration Roadmap: Moving Millions Without Breaking Trust&lt;/h2&gt;
&lt;p&gt;Don't panic&amp;mdash;you don't have to delete your password database overnight. You need a hybrid approach. This is a standard practice in modern CIAM architecture because it keeps the lights on for your legacy users while you transition.&lt;/p&gt;
&lt;p&gt;Keep both methods running for a while. When a user logs in the old way, show a small, non-intrusive overlay: &quot;Want to enable one-tap sign-in for next time?&quot;&lt;/p&gt;
&lt;p&gt;Once they click yes, you register their device. Over a few months, your password-reliant user base will shrink as your passkey base grows. This phased approach lets you catch bugs, refine your fallback protocols, and harden your security without ever locking a loyal user out of their account.&lt;/p&gt;
&lt;h2&gt;Is Passwordless Authentication NIST Compliant?&lt;/h2&gt;
&lt;p&gt;Yes. Passkey-based authentication satisfies phishing-resistant authentication requirements under NIST SP 800-63 guidelines.&lt;/p&gt;
&lt;p&gt;Passkeys meet AAL2 authentication requirements because they:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;resist phishing attacks&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;use cryptographic authentication&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;bind credentials to devices&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This makes passkeys suitable for high-security authentication environments.&lt;/p&gt;
&lt;h2&gt;Staying Compliant: Meeting NIST SP 800-63-4 Standards&lt;/h2&gt;
&lt;p&gt;If you&amp;rsquo;re working with enterprise clients, compliance isn't optional. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://pages.nist.gov/800-63-3/&quot;&gt;NIST SP 800-63-4 guidelines&lt;/a&gt; are the gold standard now.&lt;/p&gt;
&lt;p&gt;Passkeys hit the AAL2 (Authenticator Assurance Level 2) requirements for phishing resistance perfectly. Think about SMS-based MFA&amp;mdash;it&amp;rsquo;s easy to intercept. FIDO2 passkeys are locked to your website&amp;rsquo;s origin. Even if a user visits a fake, malicious version of your site, the browser won&amp;rsquo;t provide the cryptographic signature because the domain doesn&amp;rsquo;t match. That level of security is exactly what enterprise procurement teams are looking for.&lt;/p&gt;
&lt;h2&gt;Passwordless Readiness Checklist: Is Your Stack Prepared?&lt;/h2&gt;
&lt;p&gt;Before you dive in, run a quick audit of your setup:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Browser Compatibility:&lt;/strong&gt; Are you using the latest WebAuthn APIs? Most modern browsers are great, but make sure your frontend handles legacy browsers gracefully.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Database Compatibility:&lt;/strong&gt; Can your schema store public keys? You&amp;rsquo;re moving away from salted hashes to storing public key associations. Make sure your backend team is ready for that shift.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Account Recovery:&lt;/strong&gt; This is the big one. If someone loses their phone, how do they get back in? You need a rock-solid, identity-verified recovery flow&amp;mdash;like a backup email or a secondary device&amp;mdash;that doesn't just fall back to a weak, phishable password.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Overcoming the &quot;Cold Start&quot; Problem&lt;/h2&gt;
&lt;p&gt;The biggest hurdle is getting users to set up that first passkey. People are creatures of habit. They&amp;rsquo;ll keep typing that password until you give them a damn good reason to stop.&lt;/p&gt;
&lt;p&gt;Stop talking about &quot;cryptographic tokens&quot; in your UI. Nobody cares. Sell the benefit. Frame the switch as a massive time-saver. Prompt them during high-intent moments, like right after they log in or finish a checkout. &quot;Speed up your next checkout by 5 seconds with TouchID&quot; is a value proposition that hits home.&lt;/p&gt;
&lt;p&gt;When you frame the transition as a premium feature that makes their life easier, you stop being a tech company forcing an update and start being a product company providing a better experience.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Frequently Asked Questions&lt;/h2&gt;
&lt;h3&gt;Will users find passwordless logins confusing?&lt;/h3&gt;
&lt;p&gt;Not at all. Users are already conditioned by the ubiquity of FaceID and TouchID on their mobile devices. The muscle memory for &quot;glance to unlock&quot; is already there. When you align your web authentication with the native biometric prompts users see every day, the conversion rate typically outperforms traditional password entry.&lt;/p&gt;
&lt;h3&gt;What happens if a user loses their device?&lt;/h3&gt;
&lt;p&gt;Device loss is a reality, which is why multi-device support is essential. Modern passkey providers allow users to sync their credentials across their ecosystem (e.g., iCloud or Google Password Manager). For edge cases, your recovery protocol should rely on verified secondary channels, such as hardware security keys or a pre-verified recovery email, ensuring that the user never loses access to their account.&lt;/p&gt;
&lt;h3&gt;Is passwordless authentication compatible with older browsers?&lt;/h3&gt;
&lt;p&gt;While FIDO2/WebAuthn is supported by all major modern browsers, you should implement a fallback strategy for edge cases. A &quot;magic link&quot; sent to the user's registered email address serves as a perfect, secure bridge for users on legacy systems who cannot utilize biometric passkeys.&lt;/p&gt;
&lt;h3&gt;How do I migrate my existing user database to passwordless?&lt;/h3&gt;
&lt;p&gt;The most effective strategy is the &quot;Hybrid Login&quot; transition. You keep your existing password database active but implement a prompt that triggers during a successful login. Once the user authenticates via their old password, you ask them to register their device as a passkey. Over time, your reliance on the password database wanes until you can eventually deprecate it entirely.&lt;/p&gt;
&lt;h3&gt;Does passwordless authentication increase or decrease security?&lt;/h3&gt;
&lt;p&gt;It significantly increases security. By shifting from shared secrets&amp;mdash;which can be stolen, phished, or guessed&amp;mdash;to cryptographic proof of possession, you effectively neutralize the primary attack vectors of the modern web: credential stuffing and phishing. When the key never leaves the user&amp;rsquo;s device, the server has nothing for an attacker to steal.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;Passwords were designed for an earlier internet.&lt;/p&gt;
&lt;p&gt;Modern applications require authentication systems that prioritize both security and usability.&lt;/p&gt;
&lt;p&gt;Passwordless authentication replaces shared secrets with cryptographic identity verification.&lt;/p&gt;
&lt;p&gt;By adopting passkeys and passwordless login flows, organizations can improve conversion rates while reducing credential-based attacks.&lt;/p&gt;
&lt;p&gt;Passwordless authentication is not just a security upgrade.&lt;br&gt;It is a fundamental improvement to the user authentication experience.&lt;/p&gt;

</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/15275/how-to-implement-passwordless-authentication-to-boost-user-conversion</guid>
<pubDate>Thu, 23 Apr 2026 11:03:25 +0000</pubDate>
</item>
<item>
<title>Enhancing User Experience with Passwordless Authentication: A Design-First Approach</title>
<link>https://coderlegion.com/15288/enhancing-user-experience-with-passwordless-authentication-a-design-first-approach</link>
<description>
&lt;p&gt;&lt;img src=&quot;https://cdn.pseo.one/67b62b766899109fe72fb789/687e6cccf6fe799d28851ea0/69c4d32ab4e689ddb91818ec/content-image/88c9d192-b3a3-48da-83d9-73bba89e9f0b.webp&quot; alt=&quot;88c9d192-b3a3-48da-83d9-73bba89e9f0b&quot;&gt;&lt;/p&gt;
&lt;p&gt;Passwords were meant to protect users. Instead, they&amp;rsquo;ve become one of the biggest sources of friction in digital products.&lt;/p&gt;
&lt;p&gt;As explained by the &lt;a rel=&quot;nofollow&quot; href=&quot;https://fidoalliance.org/online-authentication-barometer/&quot;&gt;FIDO Alliance&lt;/a&gt;, whose survey of 10,000 users across 10 countries found that respondents manually entered passwords nearly 1,639 times per year - and nearly 60% admitted abandoning an online service entirely in the last month because they couldn't remember their password - password fatigue isn&amp;rsquo;t rare. It&amp;rsquo;s routine.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s not just a security issue. It&amp;rsquo;s a user experience problem. A &lt;a rel=&quot;nofollow&quot; href=&quot;https://fuselabcreative.com/&quot;&gt;full-service UI/UX design agency&lt;/a&gt; looks at passwordless authentication not as a technical upgrade, but as a behavior redesign.&lt;/p&gt;
&lt;h3&gt;Why Passwords Create Friction&lt;/h3&gt;
&lt;p&gt;Passwords interrupt flow. You&amp;rsquo;re ready to complete a task. You click login. You hesitate. Was it uppercase first? Did you add a symbol? Do you need to reset it again?&lt;/p&gt;
&lt;p&gt;That pause matters - it breaks momentum and creates doubt. And sometimes it ends the journey entirely. When users abandon products because they can&amp;rsquo;t remember credentials, the issue isn&amp;rsquo;t complexity. It&amp;rsquo;s friction layered into the experience.&lt;/p&gt;
&lt;p&gt;Good design removes friction; it doesn&amp;rsquo;t defend it.&lt;/p&gt;
&lt;h3&gt;Passwordless Authentication Starts with Context&lt;/h3&gt;
&lt;p&gt;Biometrics, passkeys, device-based authentication - the tools are already here.&lt;/p&gt;
&lt;p&gt;The real question is how they&amp;rsquo;re introduced. A full-service UI/UX design agency doesn&amp;rsquo;t just remove the password field and call it progress. It studies how users access the product. Mobile or desktop? Frequent use or occasional? High-stakes access or casual browsing?&lt;/p&gt;
&lt;p&gt;Context shapes the solution. Face ID feels natural on a phone. A one-time secure link might work better in a desktop workflow. The interface should adapt to how people already behave.&lt;/p&gt;
&lt;h3&gt;Trust Has to Be Designed&lt;/h3&gt;
&lt;p&gt;When you remove something familiar, even if it&amp;rsquo;s annoying, users need reassurance.&lt;/p&gt;
&lt;p&gt;If a system logs someone in instantly, they should understand why. Clear messaging, confirmation states, and visible fallback options build confidence.&lt;/p&gt;
&lt;p&gt;You can see this kind of structured clarity in interface explorations like this one:&lt;br&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.pinterest.com/pin/686024955782241025/&quot;&gt;https://www.pinterest.com/pin/686024955782241025/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The layout is calm. Focused. It supports entry without stealing attention from the product itself. That&amp;rsquo;s the goal. Security should feel present, not overwhelming.&lt;/p&gt;
&lt;h3&gt;Reducing Abandonment Through Simplicity&lt;/h3&gt;
&lt;p&gt;The FIDO Alliance data shows how often password friction leads to abandonment. So the opportunity is obvious. Fewer steps. Fewer interruptions. Clear recovery paths. But simplicity isn&amp;rsquo;t just about fewer clicks. It&amp;rsquo;s about fewer decisions.&lt;/p&gt;
&lt;p&gt;If users don&amp;rsquo;t need to think about login, they stay focused on why they came to the product in the first place. That&amp;rsquo;s where design makes a real difference.&lt;/p&gt;
&lt;h3&gt;Security and Experience Don&amp;rsquo;t Compete&lt;/h3&gt;
&lt;p&gt;There&amp;rsquo;s an old idea that stronger security means more complexity. That&amp;rsquo;s outdated.&lt;/p&gt;
&lt;p&gt;Passwordless systems can improve protection while reducing effort. Device-bound credentials and biometric verification often reduce risk compared to reused passwords. But the shift needs guidance.&lt;/p&gt;
&lt;p&gt;A UI/UX design agency ensures onboarding explains what&amp;rsquo;s happening. It ensures recovery flows are clear. It ensures edge cases - lost devices, new hardware, shared accounts - are handled calmly. Security should feel stable, not confusing.&lt;/p&gt;
&lt;h3&gt;Authentication Is Part of the Product, Not a Gate&lt;/h3&gt;
&lt;p&gt;Login is often treated as a barrier before the &amp;ldquo;real&amp;rdquo; product begins. That mindset is flawed. Authentication is the first experience users have. If it&amp;rsquo;s frustrating, everything after it feels harder.&lt;/p&gt;
&lt;p&gt;A full-service UI/UX design agency integrates authentication into the broader journey. Onboarding, device changes, recovery, and session management all connect. Nothing feels patched together. When done well, authentication fades into the background. And that&amp;rsquo;s the point.&lt;/p&gt;
&lt;h2&gt;The Takeaway&lt;/h2&gt;
&lt;p&gt;Passwords are familiar, but familiarity isn&amp;rsquo;t the same as usability. The FIDO Alliance&amp;rsquo;s research makes it clear: repeated manual entry and forgotten credentials drive real abandonment. That&amp;rsquo;s lost trust and lost opportunity. Passwordless authentication works best when it&amp;rsquo;s designed around human behavior, not just security protocols.&lt;/p&gt;
&lt;p&gt;When entry feels natural, users stay focused on value instead of barriers. And when friction disappears quietly, engagement improves without anyone having to think about why.&lt;/p&gt;

</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/15288/enhancing-user-experience-with-passwordless-authentication-a-design-first-approach</guid>
<pubDate>Wed, 22 Apr 2026 21:13:01 +0000</pubDate>
</item>
<item>
<title>Building a Layered Security Stack: Identity, Network and Device Protection</title>
<link>https://coderlegion.com/15290/building-a-layered-security-stack-identity-network-and-device-protection</link>
<description>
&lt;img src=&quot;https://cdn.pseo.one/67b62b766899109fe72fb789/687e6cccf6fe799d28851ea0/69c0ea273be0443d118e94c1/content-image/79ffec1e-c181-4726-86a4-552f0137b9e7.webp&quot; alt=&quot;79ffec1e-c181-4726-86a4-552f0137b9e7&quot; width=&quot;100%&quot; style=&quot;width: 100%; display: block; float: left;&quot;&gt;

&lt;p&gt;Modern organizations use cloud platforms and remote work which helps improve productivity but it also increases cybersecurity risks and a single security solution is no longer enough to protect sensitive systems and data.&lt;/p&gt;
&lt;p&gt;To reduce risk businesses now use a &lt;strong&gt;layered security stack&lt;/strong&gt; that protects identities networks and devices and this approach creates many security layers so if one control fails other layers continue to protect the system.&lt;/p&gt;
&lt;h1&gt;&lt;strong&gt;Layer 1: Identity Security&lt;/strong&gt;&lt;/h1&gt;
&lt;img src=&quot;https://cdn.pseo.one/67b62b766899109fe72fb789/687e6cccf6fe799d28851ea0/69c0ea273be0443d118e94c1/content-image/49263901-d799-40d4-a231-bfade75c0350.webp&quot; alt=&quot;49263901-d799-40d4-a231-bfade75c0350&quot; width=&quot;100%&quot; style=&quot;width: 100%; display: block; float: left;&quot;&gt;

&lt;p&gt;Identity security is the first line of defense in modern cybersecurity and many cyberattacks start with stolen or compromised login details and stronger authentication helps make sure that only authorized users can access company resources.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Passwordless Authentication&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Traditional passwords are often weak and easy targets for phishing attacks and passwordless authentication replaces passwords with stronger ways of verification.&lt;/p&gt;
&lt;p&gt;Common passwordless authentication methods include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Biometric authentication&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Hardware security keys&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Authentication apps&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One time login codes&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By removing passwords organizations reduce the risk of credential theft and also make the login process easier for users.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;MFA&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Multi Factor Authentication ( MFA ) adds an extra verification step during login and even if attackers get a password they cannot access the system without the second authentication step.&lt;/p&gt;
&lt;p&gt;Common MFA verification methods include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Password or PIN&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One time codes sent to mobile devices&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Authentication apps&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Biometric verification&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Using MFA greatly reduces the chance of unauthorized access.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;SSO&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Single Sign On ( SSO ) allows users to log in once and access many applications without entering login details again and this makes authentication easier and reduces password fatigue.&lt;/p&gt;
&lt;p&gt;Key benefits of SSO include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Simplified login experience&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Centralized authentication management&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Improved employee productivity&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stronger access control policies&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When SSO is used with MFA it gives both convenience and stronger identity security.&lt;/p&gt;
&lt;h1&gt;&lt;strong&gt;Layer 2: Network Security Tools&lt;/strong&gt;&lt;/h1&gt;
&lt;img src=&quot;https://cdn.pseo.one/67b62b766899109fe72fb789/687e6cccf6fe799d28851ea0/69c0ea273be0443d118e94c1/content-image/9439d672-e4fb-47cf-add7-64d448237a31.webp&quot; alt=&quot;9439d672-e4fb-47cf-add7-64d448237a31&quot; width=&quot;100%&quot; style=&quot;width: 100%; display: block; float: left;&quot;&gt;

&lt;p&gt;Network security protects the data that moves between users devices and applications and as organizations depend more on remote work and cloud systems securing network access becomes very important.&lt;/p&gt;
&lt;p&gt;Network layer protection usually includes VPN platforms and secure web gateways and SASE solutions and teams that research options often look at providers and review platforms such as Cisco AnyConnect Palo Alto GlobalProtect Zscaler as part of their evaluation process.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;VPN Platforms&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://thebestvpn.com/&quot;&gt;Tested Virtual Private Networks&lt;/a&gt; ( VPNs ) create encrypted tunnels between users and company networks and this encryption keeps sensitive data safe from being intercepted when employees access systems remotely.&lt;/p&gt;
&lt;p&gt;Key advantages of VPN platforms include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Encrypted communication channels&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Secure remote access for employees&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Protection when using public Wi Fi&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Increased data privacy&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;VPNs remain an essential tool for remote workforce security.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Secure Web Gateways&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Secure Web Gateways ( SWGs ) monitor and filter internet traffic to protect users from malicious websites and unsafe downloads.&lt;/p&gt;
&lt;p&gt;Common capabilities include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;URL filtering and website blocking&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Malware detection and prevention&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Phishing protection&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Web traffic monitoring&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These tools help organizations enforce safe browsing policies and reduce exposure to online threats.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;SASE&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Secure Access Service Edge ( SASE ) combines networking and security services into one cloud based platform and this setup helps organizations manage security better in distributed environments.&lt;/p&gt;
&lt;p&gt;SASE platforms typically integrate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Secure web gateways&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Firewall as a service&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cloud access security brokers&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Zero Trust access controls&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This allows companies to apply consistent security policies regardless of user location.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;ZTNA&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Zero Trust Network Access &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.microsoft.com/en-us/security/business/security-101/what-is-zero-trust-network-access-ztna&quot;&gt;ZTNA&lt;/a&gt; follows the rule never trust always verify and instead of giving full network access users are checked every time they try to access an application.&lt;/p&gt;
&lt;p&gt;ZTNA systems evaluate factors such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;User identity&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Device security status&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Location of the request&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Access permissions&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This approach limits access to only the resources users need and reducing the risk of internal attacks.&lt;/p&gt;
&lt;h1&gt;&lt;strong&gt;Layer 3: Device Protection&lt;/strong&gt;&lt;/h1&gt;
&lt;p&gt;Devices like laptops and smartphones and tablets often act as entry points to company systems and protecting these endpoints is an important part of a layered security strategy.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Endpoint Detection and Response ( EDR )&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Endpoint Detection and Response tools watch device activity to find suspicious behavior or possible threats.&lt;/p&gt;
&lt;p&gt;Key EDR features include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Continuous threat monitoring&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Malware detection&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Automated incident response&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Security alerts for IT teams&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These tools help security teams detect and stop threats before they spread.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Mobile Device Management ( MDM )&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Mobile Device Management solutions let organizations manage and keep mobile devices used for work secure.&lt;/p&gt;
&lt;p&gt;Capabilities include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Device encryption enforcement&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Remote wipe for lost or stolen devices&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;App installation controls&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Security configuration management&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This ensures that company data remains protected on employee devices.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Patch and Update Management&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Old software often has weaknesses that attackers can use and patch management systems make sure devices get updates and security fixes on time.&lt;/p&gt;
&lt;p&gt;Patch management typically includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Automatic operating system updates&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Application security patches&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Vulnerability monitoring&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scheduled update deployment&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Keeping software updated helps reduce security risks across the organization.&lt;/p&gt;
&lt;h1&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/h1&gt;
&lt;p&gt;Building a layered security system is important for protecting modern digital environments and by combining &lt;strong&gt;identity security&lt;/strong&gt; and &lt;strong&gt;network protection&lt;/strong&gt; and &lt;strong&gt;device safeguards&lt;/strong&gt; organizations can reduce cybersecurity risks significantly.&lt;/p&gt;
&lt;p&gt;Many growing teams also evaluate &lt;a rel=&quot;nofollow&quot; href=&quot;https://guptadeepak.com/demystifying-soc-2-compliance-for-startups-a-simple-guide/&quot;&gt;SOC 2 compliance as part of your layered security posture&lt;/a&gt;, especially when identity controls, network protections, and endpoint safeguards must align with customer security expectations and audit readiness.&lt;/p&gt;
&lt;p&gt;Technologies like passwordless authentication ,MFA, VPNs, SASE and endpoint protection work together to protect users and sensitive data and make security stronger across systems devices and networks&lt;/p&gt;

</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/15290/building-a-layered-security-stack-identity-network-and-device-protection</guid>
<pubDate>Wed, 22 Apr 2026 05:02:59 +0000</pubDate>
</item>
<item>
<title>How AI Agents Are Transforming Identity Verification and Authentication Systems</title>
<link>https://coderlegion.com/15277/how-ai-agents-are-transforming-identity-verification-and-authentication-systems</link>
<description>
&lt;p&gt;Authentication systems are no longer simple gatekeepers that validate credentials and issue tokens. In modern digital ecosystems &amp;mdash; especially SaaS and API-driven platforms &amp;mdash; authentication operates within complex, distributed environments. Identity providers, OAuth flows, session tokens, device intelligence, and zero-trust policies all interact continuously.&lt;/p&gt;
&lt;p&gt;As threat actors automate credential stuffing, phishing campaigns, and session hijacking attempts, static rule-based authentication models struggle to respond effectively. AI agents are emerging as a powerful layer within authentication stacks, introducing adaptive intelligence into identity verification and access control workflows.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;From Rule-Based Access to Adaptive Risk Engines&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Traditional authentication logic typically follows deterministic paths: validate credentials, check second factor, issue JWT or session token. Risk scoring, if present, often relies on predefined thresholds such as IP reputation or location mismatches.&lt;/p&gt;
&lt;p&gt;AI agents replace static evaluation with contextual analysis. Instead of checking isolated conditions, they process multi-dimensional signals in real time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Device fingerprint consistency&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Behavioral biometrics (keystroke dynamics, interaction velocity)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Network reputation signals&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Historical login timelines&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Session navigation patterns&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These signals feed into dynamic risk models that adjust authentication decisions based on probabilistic assessment rather than binary logic.&lt;/p&gt;
&lt;p&gt;As organizations adopt AI-driven authentication systems, many evaluate specialized engineering partners capable of integrating adaptive risk engines into identity stacks. Teams assessing technical implementation pathways can &lt;a rel=&quot;nofollow&quot; href=&quot;https://anadea.info/blog/top-ai-agent-development-companies/&quot;&gt;&lt;u&gt;explore more options here&lt;/u&gt;&lt;/a&gt;.&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;AI Agents as Risk Microservices in Authentication Architecture&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;In modern authentication stacks, AI agents often operate as dedicated risk evaluation microservices. During a login request, the authentication server communicates with the AI risk engine before issuing an access token. As these AI systems take on more automated decision-making tasks, concerns about &lt;a rel=&quot;nofollow&quot; href=&quot;https://coursiv.io/blog/what-jobs-will-ai-replace-by-2030&quot;&gt;AI job replacement&lt;/a&gt; are growing, particularly for roles focused on routine security monitoring and risk assessment. At the same time, the rise of AI systems is also creating new opportunities for individuals to&amp;nbsp;&lt;a rel=&quot;nofollow&quot; href=&quot;https://jumptask.io/get-paid-to-train-ai/&quot;&gt;&lt;strong&gt;earn training AI&lt;/strong&gt;&lt;/a&gt;&amp;nbsp;models by contributing to data labeling, behavioral analysis, and human-in-the-loop validation processes that improve authentication accuracy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A simplified flow may look like this:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;User submits credentials or initiates passwordless login.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Identity provider validates primary authentication factors.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Risk signals (device, IP, behavioral input) are sent to the AI agent.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The AI agent calculates a contextual risk score.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Based on risk level:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Token is issued immediately,&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Step-up authentication is triggered, or&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Access is denied.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This architecture allows AI agents to influence authentication outcomes without disrupting existing OAuth or OIDC flows. The risk engine becomes an intelligent decision layer rather than a replacement for identity infrastructure.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Real-Time Token Issuance and Conditional Access&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;AI agents enhance token management by introducing conditional issuance logic. Instead of issuing JWTs solely based on credential validation, authentication systems can incorporate contextual intelligence into access decisions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For example:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A known device in a consistent geolocation may receive long-lived tokens.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A suspicious login attempt may trigger short-lived tokens with limited scope.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;High-risk activity may require additional verification before issuing refresh tokens.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This dynamic token lifecycle management strengthens access control without increasing friction for legitimate users.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Continuous Authentication and Session Monitoring&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Authentication does not end once an access token is issued. Session hijacking and privilege escalation attacks frequently occur post-login.&lt;/p&gt;
&lt;p&gt;AI agents support continuous authentication by monitoring session behavior in real time. Instead of relying only on token expiration or inactivity timeouts, systems analyze behavioral patterns throughout a session.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If an AI agent detects anomalies &amp;mdash; such as sudden behavioral deviation, unusual API call patterns, or abnormal navigation flow &amp;mdash; it can:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Invalidate session tokens&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Force re-authentication&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Limit access scope&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Trigger backend security alerts&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This proactive model enhances security posture without requiring constant user interaction.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Strengthening Passwordless and MFA Workflows&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Passwordless authentication reduces reliance on shared secrets, but it does not eliminate identity risks. AI agents enhance passwordless systems by evaluating contextual trust signals surrounding each login attempt.&lt;/p&gt;
&lt;p&gt;Even if biometric authentication or magic link validation succeeds, AI agents assess additional risk factors. If the device environment or behavior appears inconsistent, step-up verification can be enforced automatically.&lt;/p&gt;
&lt;p&gt;Similarly, MFA systems become more intelligent when AI agents determine when additional factors are truly necessary. Low-risk sessions remain frictionless, while high-risk attempts receive additional scrutiny.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;AI in Identity Verification and KYC Pipelines&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Beyond login workflows, AI agents also transform identity verification processes such as onboarding and KYC checks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Within verification pipelines, AI agents can orchestrate:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Document authenticity detection using deep learning&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Face matching and liveness analysis&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cross-database identity validation&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Behavioral consistency checks during submission&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rather than evaluating each validation step independently, AI agents correlate outputs into unified confidence scores. This orchestration reduces manual review load while increasing detection accuracy.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Zero-Trust and Context-Aware Policy Enforcement&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Zero-trust security assumes that no request should be inherently trusted, regardless of origin. AI agents enhance zero-trust frameworks by continuously evaluating contextual signals across authentication and authorization layers.&lt;/p&gt;
&lt;p&gt;This same challenge is explored in &lt;a rel=&quot;nofollow&quot; href=&quot;https://guptadeepak.com/ebooks/ai-agents-security-nightmare/&quot;&gt;&lt;strong&gt;AI Agents and the Security Nightmare Nobody's Solving&lt;/strong&gt;,&lt;/a&gt; which examines how identity, authorization, and trust models break when autonomous agents begin making security-sensitive decisions across distributed systems.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Within API-driven environments, AI agents can:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Adjust access policies dynamically&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Restrict sensitive endpoints based on risk score&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enforce adaptive rate limiting&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Modify authorization scopes in real time&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This enables authentication systems to operate as living security layers rather than static gateways.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Model Governance and Compliance Considerations&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Deploying AI agents within authentication systems introduces governance responsibilities. Risk scoring decisions must be explainable and auditable, particularly in regulated sectors.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Organizations should implement:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Model drift monitoring&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Bias detection in behavioral analysis&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Transparent logging of AI-driven access decisions&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Secure storage of feature inputs&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Regular retraining cycles&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Authentication systems directly impact user access, making accountability essential.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;The Future of Intelligent Identity Infrastructure&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;AI agents are transforming authentication from a static verification checkpoint into an adaptive, context-aware security ecosystem. By operating as real-time risk engines within OAuth and identity provider architectures, they enhance fraud detection, reduce friction, and support continuous session integrity.&lt;/p&gt;
&lt;p&gt;As authentication systems grow more distributed and API-driven, intelligent risk evaluation will become a foundational component of identity infrastructure. Organizations that integrate AI agents thoughtfully &amp;mdash; with strong architectural discipline and governance oversight &amp;mdash; will be better positioned to defend against increasingly automated threats.&lt;/p&gt;
&lt;p&gt;Deploying AI agents in identity pipelines is as much a culture challenge as a technical one. Organizations that integrate adaptive authentication successfully tend to invest equally in the human side, &lt;a rel=&quot;nofollow&quot; href=&quot;https://guptadeepak.com/ai-is-your-coworker-now-navigating-trust-and-transformation-in-the-modern-workplace/&quot;&gt;building organizational trust in AI-driven workflows&lt;/a&gt; that help teams understand when to override AI decisions and when to rely on them.&lt;/p&gt;
&lt;p&gt;AI agents are not replacing authentication systems. They are making them smarter, more responsive, and better equipped for the realities of modern digital security.&lt;/p&gt;

</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/15277/how-ai-agents-are-transforming-identity-verification-and-authentication-systems</guid>
<pubDate>Tue, 21 Apr 2026 02:20:04 +0000</pubDate>
</item>
<item>
<title>How OTP Authentication Streamlines Service Delivery for HVAC Companies</title>
<link>https://coderlegion.com/15291/how-otp-authentication-streamlines-service-delivery-for-hvac-companies</link>
<description>
&lt;p&gt;&lt;img src=&quot;https://cdn.pseo.one/67b62b766899109fe72fb789/687e6cccf6fe799d28851ea0/69be69575148ee4f7d63bbfe/content-image/aa7b6967-63fc-446d-9374-4879a2d281ca.webp&quot; alt=&quot;aa7b6967-63fc-446d-9374-4879a2d281ca&quot;&gt;&lt;/p&gt;
&lt;p&gt;Photo courtesy of &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.freepik.com/free-photo/male-plumber-working-with-client-fix-kitchen-problems_94957515.htm&quot;&gt;Freepik&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;As HVAC businesses grow and expand into new areas, they need to find ways to manage more customers and staff. One of the biggest challenges is making sure that service delivery and payments are secure and efficient, especially when working with new customers in different locations.&lt;/p&gt;
&lt;p&gt;A simple solution to this problem is one-time password (OTP) authentication. OTP authentication makes it easy to verify customers and payments quickly and securely that helps HVAC companies improve their overall service process.&lt;/p&gt;
&lt;p&gt;In this article, we&amp;rsquo;ll explore how OTP authentication helps HVAC companies streamline service delivery by improving customer verification, appointment scheduling, payments, and service completion, all while keeping things secure and simple.&lt;/p&gt;
&lt;h2&gt;1. Simplified Customer Verification&lt;/h2&gt;
&lt;p&gt;OTP (One-Time Password) authentication replaces &lt;a rel=&quot;nofollow&quot; href=&quot;https://mojoauth.com/ciam-101/authentication-server&quot;&gt;traditional login methods&lt;/a&gt; by sending a unique code to a customer&amp;rsquo;s phone or email, which they use to verify their identity instantly. For HVAC service appointments, customers can receive an OTP to confirm their identity, eliminating the need for passwords or lengthy forms.&lt;/p&gt;
&lt;h2&gt;2. Enhanced Service Appointment Scheduling&lt;/h2&gt;
&lt;p&gt;OTP authentication helps verify and confirm HVAC service appointments, ensuring both the customer and service provider are clear on the timing and details. When a customer schedules a service, they receive an OTP confirmation, which confirms the appointment and reduces the risk of any misunderstandings. &lt;/p&gt;
&lt;p&gt;This process ensures that appointments are tracked accurately, leading to fewer scheduling errors and improved customer satisfaction. As you invest in marketing or &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.wearetg.com/industry/hvac-seo-company/&quot;&gt;HVAC SEO services&lt;/a&gt; to generate more leads, it&amp;rsquo;s essential to have a reliable system for booking appointments to avoid confusion and ensure a smooth customer experience.&lt;/p&gt;
&lt;h2&gt;3. Improved Payment and Transaction Security&lt;/h2&gt;
&lt;p&gt;OTP authentication adds a layer of security to payments and transactions, protecting HVAC businesses and their customers. When a payment is made, an OTP is sent to the customer's phone or email to verify the transaction, ensuring it&amp;rsquo;s legitimate. This reduces the risk of fraud or unauthorized charges. &lt;/p&gt;
&lt;p&gt;Additionally, OTP can prevent fraud from service vendors by confirming that payments are processed correctly and only authorized transactions are completed. This enhances security, builds trust, and ensures a reliable payment system for HVAC companies and their clients.&lt;/p&gt;
&lt;h2&gt;4. Streamlined Service Completion Confirmation&lt;/h2&gt;
&lt;p&gt;OTP authentication simplifies service completion verification. After an HVAC service is completed, an OTP is sent to the customer&amp;rsquo;s phone or email to confirm the work was done. This ensures clear communication and prevents any disputes about the service, making the process quicker and more reliable.&lt;/p&gt;
&lt;h2&gt;5. Seamless Communication for Customer Support&lt;/h2&gt;
&lt;p&gt;OTP authentication streamlines customer support by ensuring secure communication. When a customer contacts the HVAC company for assistance, they can verify their identity through OTP, &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.fraud.com/post/account-verification&quot;&gt;confirming they are the account holder&lt;/a&gt;. This prevents unauthorized access to support channels and ensures that customer queries are handled by the right team.&lt;/p&gt;
&lt;h2&gt;Endnote&lt;/h2&gt;
&lt;p&gt;OTP authentication enhances security and simplifies key processes for HVAC businesses. By streamlining customer verification, service confirmations, and payments, it improves efficiency and builds trust. &lt;/p&gt;
&lt;p&gt;Adopting OTP ensures smoother operations, better customer experience, and strengthens the company's competitive edge in a growing market. It&amp;rsquo;s a valuable investment for long-term success.&lt;/p&gt;

</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/15291/how-otp-authentication-streamlines-service-delivery-for-hvac-companies</guid>
<pubDate>Mon, 20 Apr 2026 11:50:01 +0000</pubDate>
</item>
<item>
<title>#  How Authentication Really Works (JWT vs Session vs OAuth)</title>
<link>https://coderlegion.com/15033/how-authentication-really-works-jwt-vs-session-vs-oauth</link>
<description>&lt;p&gt;Whenever you log in to a website…&lt;/p&gt;
&lt;p&gt;You enter:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Email&lt;/li&gt;
&lt;li&gt;Password&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And suddenly…&lt;/p&gt;
&lt;p&gt; You are “logged in”&lt;/p&gt;
&lt;p&gt;But what actually happens behind the scenes?&lt;/p&gt;
&lt;p&gt;How does the system remember you?&lt;/p&gt;
&lt;p&gt;Let’s break it down simply &lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;first-what-is-authentication-&quot;&gt;First: What is Authentication?&lt;/h3&gt;
&lt;p&gt;Authentication =&lt;/p&gt;
&lt;p&gt; “Proving that you are who you say you are”&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Login form&lt;/li&gt;
&lt;li&gt;OTP verification&lt;/li&gt;
&lt;li&gt;Google login&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-basic-flow-before-jwt-session-oauth-&quot;&gt;⚙️ Basic Flow (Before JWT / Session / OAuth)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;User enters email + password&lt;/li&gt;
&lt;li&gt;Server verifies credentials&lt;/li&gt;
&lt;li&gt;Now server needs to “remember” the user&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt; This is where things get interesting&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;1-session-based-authentication&quot;&gt;1. Session-Based Authentication&lt;/h2&gt;
&lt;h3 id=&quot;how-it-works&quot;&gt;How It Works&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;User logs in&lt;/li&gt;
&lt;li&gt;Server creates a &lt;strong&gt;session&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Stores session in database/memory&lt;/li&gt;
&lt;li&gt;Sends a &lt;strong&gt;session ID&lt;/strong&gt; to browser (via cookie)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now:&lt;/p&gt;
&lt;p&gt; Every request → sends session ID&lt;br&gt;
 Server checks → “Yes, this is the user”&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;flow&quot;&gt;Flow&lt;/h3&gt;
&lt;p&gt;Login → Session Created → Session ID stored in browser → Request with ID → Server validates&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-pros&quot;&gt;✅ Pros&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Simple to implement&lt;/li&gt;
&lt;li&gt;Easy to invalidate (logout)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-cons&quot;&gt;❌ Cons&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Server has to store sessions&lt;/li&gt;
&lt;li&gt;Not scalable for large systems&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-2-jwt-json-web-token-&quot;&gt;⚡ 2. JWT (JSON Web Token)&lt;/h2&gt;
&lt;h3 id=&quot;how-it-works-1&quot;&gt;How It Works&lt;/h3&gt;
&lt;p&gt;Instead of storing session on server…&lt;/p&gt;
&lt;p&gt; Server gives token to user&lt;/p&gt;
&lt;p&gt;This token contains:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;User ID&lt;/li&gt;
&lt;li&gt;Expiry time&lt;/li&gt;
&lt;li&gt;Signature&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;header.payload.signature
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now:&lt;/p&gt;
&lt;p&gt; Client stores JWT (localStorage / cookie)&lt;br&gt;
 Sends it with every request&lt;/p&gt;
&lt;p&gt;Server:&lt;/p&gt;
&lt;p&gt; Verifies token → allows access&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;flow-1&quot;&gt;Flow&lt;/h3&gt;
&lt;p&gt;Login → JWT generated → Stored on client → Sent with requests → Verified&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-pros-1&quot;&gt;✅ Pros&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Stateless (no server storage)&lt;/li&gt;
&lt;li&gt;Scalable&lt;/li&gt;
&lt;li&gt;Fast&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-cons-1&quot;&gt;❌ Cons&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Hard to revoke instantly&lt;/li&gt;
&lt;li&gt;Needs proper security handling&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;3-oauth-login-with-google-github-&quot;&gt;3. OAuth (Login with Google, GitHub)&lt;/h2&gt;
&lt;h3 id=&quot;how-it-works-2&quot;&gt;How It Works&lt;/h3&gt;
&lt;p&gt;Instead of creating your own login system…&lt;/p&gt;
&lt;p&gt; You use another provider&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Login with Google”&lt;/li&gt;
&lt;li&gt;“Login with GitHub”&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;flow-2&quot;&gt;Flow&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;User clicks Google login&lt;/li&gt;
&lt;li&gt;Redirect to Google&lt;/li&gt;
&lt;li&gt;User authenticates&lt;/li&gt;
&lt;li&gt;Google sends token back&lt;/li&gt;
&lt;li&gt;Your app trusts Google → logs user in&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-pros-2&quot;&gt;✅ Pros&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;No need to manage passwords&lt;/li&gt;
&lt;li&gt;More secure&lt;/li&gt;
&lt;li&gt;Faster user onboarding&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-cons-2&quot;&gt;❌ Cons&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Dependency on third-party&lt;/li&gt;
&lt;li&gt;Slightly complex setup&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;jwt-vs-session-vs-oauth-simple-comparison-&quot;&gt;JWT vs Session vs OAuth (Simple Comparison)&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Feature     &lt;/th&gt;&lt;th&gt;Session    &lt;/th&gt;&lt;th&gt;JWT         &lt;/th&gt;&lt;th&gt;OAuth&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Storage     &lt;/td&gt;&lt;td&gt;Server     &lt;/td&gt;&lt;td&gt;Client      &lt;/td&gt;&lt;td&gt;Third-party&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Scalability &lt;/td&gt;&lt;td&gt;Medium     &lt;/td&gt;&lt;td&gt;High        &lt;/td&gt;&lt;td&gt;High&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Complexity  &lt;/td&gt;&lt;td&gt;Easy       &lt;/td&gt;&lt;td&gt;Medium      &lt;/td&gt;&lt;td&gt;Medium-Hard&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Use Case    &lt;/td&gt;&lt;td&gt;Small apps &lt;/td&gt;&lt;td&gt;APIs / SPAs &lt;/td&gt;&lt;td&gt;Social login&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;when-to-use-what-&quot;&gt;When to Use What?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Small app → Session&lt;/li&gt;
&lt;li&gt;Modern API / frontend → JWT&lt;/li&gt;
&lt;li&gt;Social login → OAuth&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt; Many apps use combination (JWT + OAuth)&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-common-mistakes&quot;&gt;⚠️ Common Mistakes&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Storing JWT insecurely ❌&lt;/li&gt;
&lt;li&gt;Not setting expiry ❌&lt;/li&gt;
&lt;li&gt;Weak session management ❌&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt; Security matters a lot here&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thought&quot;&gt;Final Thought&lt;/h3&gt;
&lt;p&gt;Authentication looks simple on UI…&lt;/p&gt;
&lt;p&gt;But behind the scenes:&lt;/p&gt;
&lt;p&gt; It’s all about trust, identity, and security&lt;/p&gt;
&lt;p&gt;As a developer:&lt;/p&gt;
&lt;p&gt;Understanding this = real backend skill&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Now next time you click “Login”…&lt;/p&gt;
&lt;p&gt;You know exactly what’s happening &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/15033/how-authentication-really-works-jwt-vs-session-vs-oauth</guid>
<pubDate>Fri, 17 Apr 2026 06:14:22 +0000</pubDate>
</item>
<item>
<title>Thinking auth in producation systems a dive on the backend</title>
<link>https://coderlegion.com/11214/thinking-auth-in-producation-systems-a-dive-on-the-backend</link>
<description>&lt;p&gt;When building an application, authentication is unavoidable. You can outsource it to services like Clerk or Better Auth and be productive fast, but that convenience often comes with trade-offs: third-party lock-in, limited control over flows, and opaque behavior when things go wrong.&lt;/p&gt;
&lt;p&gt;In production systems, authentication is not just about logging users in. It shapes your security model, influences your database design, and quietly dictates how scalable and debuggable your system will be.&lt;/p&gt;
&lt;p&gt;In this article, I’ll walk through how I designed and implemented authentication for a real production project, the decisions I had to make, and the compromises that came with them. Since all systems have different auth requirements, it might vary from your version.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Requirements for my project&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For this project, authentication needs to be intentionally simple:&lt;br&gt;
email + password–based registration and login, with Google OAuth as an alternative for both signup and sign-in.&lt;/p&gt;
&lt;p&gt;If a user registers using email and password, the email must be verified via a verification link. Once the email is verified, the user is automatically logged in. The frontend receives the required tokens and user context without forcing an additional login step.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Strategy&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For this project, I’m using a JWT-based authentication approach with short-lived access tokens and refresh tokens, with refresh token rotation enabled.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private async generateTokens(
    userId: string,
    email: string,
    role: number,
    deviceId: string,
  ) {
    const accessTokenSecret =
      this.configService.get&amp;lt;string&amp;gt;('JWT_ACCESS_SECRET');

    const refreshTokenSecret = this.configService.get&amp;lt;string&amp;gt;(
      'JWT_REFRESH_TOKEN_SECRET',
    );

    const accessTokenExpiryRaw = this.configService.get&amp;lt;string&amp;gt;(
      'JWT_ACCESS_TOKEN_EXPIRE_TIME',
      '1h',
    );
    const refreshTokenExpiryRaw = this.configService.get&amp;lt;string&amp;gt;(
      'JWT_REFRESH_TOKEN_EXPIRE_TIME',
      '7d',
    );

    const accessTokenTtlMs = ms(accessTokenExpiryRaw);
    const refreshTokenTtlMs = ms(refreshTokenExpiryRaw);

    const [accessToken, refreshToken] = await Promise.all([
      this.jwtService.signAsync(
        { sub: userId, email, role },
        {
          secret: accessTokenSecret,
          expiresIn: Math.ceil(accessTokenTtlMs / 1000),
        },
      ),
      this.jwtService.signAsync(
        { sub: userId, email, role },
        {
          secret: refreshTokenSecret,
          expiresIn: Math.ceil(refreshTokenTtlMs / 1000),
        },
      ),
    ]);

    // Extract the signature part of the JWT
    const accessTokenSignature = accessToken.split('.')[2];

    // Keyv expects TTL in ms - cache key includes deviceId for device-specific validation
    await this.cacheManager.set(
      `${CacheKey.ACCESS_KEY}:${userId}:${deviceId}`,
      { tokenSignature: accessTokenSignature, deviceId },
      accessTokenTtlMs,
    );

    return { accessToken, refreshToken };
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To minimize authentication lookup latency, access tokens are cached in Redis, mapped to the corresponding user_id and device_id. This allows the system to validate active sessions without hitting the primary database on every authenticated request.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Device Identification&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
Each client device is assigned a unique deviceId, generated on the backend.&lt;/p&gt;
&lt;p&gt;My first approach was to derive this identifier from the IP address and user agent. From a security perspective, this worked well. Tokens were tightly bound to their original context, and replaying them from elsewhere was difficult.&lt;/p&gt;
&lt;p&gt;In practice, though, IP addresses change far more often than you’d expect, especially for mobile users. The result was unnecessary session invalidation and a poor user experience.&lt;/p&gt;
&lt;p&gt;After weighing those trade-offs, I switched to deriving the deviceId from the user ID and user agent instead.&lt;/p&gt;
&lt;p&gt;The goal of the deviceId is simple:&lt;/p&gt;
&lt;p&gt;Avoid duplicate session entries in Redis when a user logs in multiple times from the same device&lt;br&gt;
Make session tracking and revocation possible on a per-device basis&lt;br&gt;
Add an extra check during token validation beyond signature and expiry&lt;br&gt;
By anchoring sessions to a deterministic deviceIdRedis maintains a single active access-token entry per device rather than accumulating redundant session data.&lt;/p&gt;
&lt;p&gt;In practice, access and refresh tokens are validated not just by their cryptographic signature and expiration, but also by the reason the deviceId they were issued for. This means that even if a token is leaked, it can’t be trivially replayed from a completely different device context unless the attacker also replicates the same user agent.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  private generateDeviceFingerprint(userId: string, userAgent: string): string {
    const raw = `${userId}|${userAgent}`;
    return crypto
      .createHash('sha256')
      .update(raw)
      .digest('hex')
      .substring(0, 32);
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;User agents aren’t secret, and this is not meant to be a perfect defense. What it does provide is a meaningful reduction in the blast radius of token leakage and protection against a large class of naive replay attacks.&lt;/p&gt;
&lt;p&gt;This approach intentionally trades stronger environmental binding for stability and usability. For this system, that trade-off is acceptable. The deviceId isn’t a cryptographic fingerprint; it’s a pragmatic mechanism to keep sessions predictable, revocable, and reasonably safe in production.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Schema design&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For authentication, the core requirements can be met with just two tables: one to represent the user, and another to track login sessions via refresh tokens.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Table users {
  id        uuid PK        // uuidv7
  name      string
  email     string UNIQUE NOT NULL
  password  string null // hashed password
  role      int NOT NULL   // 0 = user, 1 = admin, 2 = super_admin
  created_at timestamp
  updated_at timestamp
}
Table user_login_tokens {
  id                     uuid PK
  user_id                uuid FK -&amp;gt; users.id
  device_id              string
  refresh_token_signature string NOT NULL
  user_agent             string
  ip_address             string
  expires_at             timestamp NOT NULL
  revoked_at             timestamp
  created_at             timestamp
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Rate Limiting &amp;amp; Abuse Protection&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
Authentication endpoints are a natural target for abuse, so basic rate limiting is enforced at the infrastructure level using Redis.&lt;/p&gt;
&lt;p&gt;Rather than applying a single global limit, rate limiting is scoped by action and tracked independently by IP address and email. This allows the system to respond differently to suspicious behavior without punishing legitimate users.&lt;/p&gt;
&lt;p&gt;The following flows are protected:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Login attempts.&lt;/li&gt;
&lt;li&gt;Registration attempts &lt;/li&gt;
&lt;li&gt;Forgot password requests &lt;/li&gt;
&lt;li&gt;Each flow has its own threshold, time window, and temporary block duration.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;How It Works&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For every request, the system checks whether the IP address or email is currently blocked for the given action. If either is blocked, the request is rejected immediately.&lt;/p&gt;
&lt;p&gt;On failed attempts, counters are incremented in Redis with a short TTL. Once the configured threshold is exceeded, a temporary block is applied, and the counter is cleared. Successful authentication clears any existing attempt counters.&lt;/p&gt;
&lt;p&gt;This keeps rate limiting fast, stateless, and independent of the primary database.&lt;/p&gt;
&lt;p&gt;Here’s an example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;this.loginConfig = {
  maxAttempts: this.configService.get&amp;lt;number&amp;gt;('AUTH_LOGIN_MAX_ATTEMPTS', 5),
  windowMs: this.configService.get&amp;lt;number&amp;gt;('AUTH_LOGIN_WINDOW_MS', 60000),
  blockDurationMs: this.configService.get&amp;lt;number&amp;gt;('AUTH_LOGIN_BLOCK_MS',
    60000,
  ),
};

// Registration: 3 attempts per 5 minutes, blocked for 5 minutes
this.registerConfig = {
  maxAttempts: this.configService.get&amp;lt;number&amp;gt;(
    'AUTH_REGISTER_MAX_ATTEMPTS',
    3,
  ),
  windowMs: this.configService.get&amp;lt;number&amp;gt;(
      'AUTH_REGISTER_WINDOW_MS',
      300000,
  ),
  blockDurationMs: this.configService.get&amp;lt;number&amp;gt;(
      'AUTH_REGISTER_BLOCK_MS',
      300000,
    ),
};

// Forgot Password: 3 attempts per 5 minutes, blocked for 5 minutes
this.forgotPasswordConfig = {
   maxAttempts: this.configService.get&amp;lt;number&amp;gt;(
      'AUTH_FORGOT_MAX_ATTEMPTS',
      3,
   ),
   windowMs: this.configService.get&amp;lt;number&amp;gt;('AUTH_FORGOT_WINDOW_MS', 300000),
   blockDurationMs: this.configService.get&amp;lt;number&amp;gt;(
     'AUTH_FORGOT_BLOCK_MS',
     300000,
   ),
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Token Lifecycle &amp;amp; Rotation&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
Access tokens in this system are intentionally short-lived. Any long-running session relies on refresh tokens, which makes the refresh flow a critical security boundary.&lt;/p&gt;
&lt;p&gt;To handle this safely, refresh tokens are rotated on every use.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Refresh Flow&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When a client requests a new access token using a refresh token, the system performs the following steps inside a database transaction:&lt;/p&gt;
&lt;p&gt;Revoke the existing refresh token&lt;br&gt;
The refresh token used for the request is immediately marked as revoked. This ensures that a refresh token can only be used once.&lt;br&gt;
Re-evaluate the device context&lt;br&gt;
The request’s device information is re-derived on the backend, ensuring that the new tokens are bound to the correct deviceId.&lt;br&gt;
Issue a new token pair&lt;br&gt;
A fresh access token and refresh token are generated and returned to the client.&lt;br&gt;
Persist the new refresh token&lt;br&gt;
The new refresh token is stored in the database, replacing any existing token for the same user-device combination.&lt;br&gt;
This entire flow is executed atomically. Either all steps succeed, or none do.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Why Rotation Matters&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Refresh token rotation prevents an entire class of replay attacks.&lt;/p&gt;
&lt;p&gt;If a refresh token is leaked and reused:&lt;/p&gt;
&lt;p&gt;The first request succeeds and rotates the token&lt;br&gt;
Any subsequent reuse of the old token fails because it has already been revoked&lt;br&gt;
This ensures that a compromised refresh token has a very limited window of usefulness.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Failure Behavior&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If a refresh token is revoked, expired, or does not match the stored signature for the given device, the refresh request is rejected. At that point, the client must re-authenticate.&lt;/p&gt;
&lt;p&gt;This makes refresh token misuse noisy, detectable, and self-limiting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Design Trade-off&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This approach slightly increases complexity compared to static refresh tokens, but it dramatically improves security and debuggability.&lt;/p&gt;
&lt;p&gt;Token rotation turns refresh tokens from long-lived secrets into single-use credentials, which aligns much better with production threat models.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;logout and token revocations&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
Logout is handled server-side.&lt;/p&gt;
&lt;p&gt;When a user logs out:&lt;/p&gt;
&lt;p&gt;The refresh token for the current session is revoked in the database&lt;br&gt;
The cached access token for the corresponding userId and deviceId is removed from Redis&lt;br&gt;
After this, the session cannot be refreshed, and any in-flight access tokens are rejected.&lt;/p&gt;
&lt;p&gt;Logout is scoped to the current device. Other active sessions remain unaffected unless explicitly revoked.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async logout(userId: string, refreshToken: string, request: Request) {
    return await this.sequelize.transaction(async (t) =&amp;gt; {
      const dbOptions = { transaction: t };
      // Revoke refresh token in database
      await this.tokenRepository.revokeRefreshToken(
        userId,
        refreshToken,
        dbOptions,
      );

      // Clear access token from cache
      // Extract device info and clear specific device cache

      const { deviceId } = this.extractDeviceInfo(request, userId);
      deleteFromCache(userId, deviceId)

      return null;
    });
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Email Verification Token Handling&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;On events like registration, or forgot password, or a verification token is created, which is sent through email to the user. where the web passes that token along with the other information, i.e., the new password field, to change the password.&lt;/p&gt;
&lt;p&gt;These tokens live shortly in Redis; on successful request processing, these tokens are deleted, and the database is updated properly.&lt;/p&gt;
&lt;p&gt;Password reset tokens are intentionally not bound to a device. These flows are designed to work across devices and browsers, and binding them to a device context would introduce unnecessary friction without meaningful security benefits.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// a generic implementation without rate limiting, since it's getting long
 async resetPassword(email: string, token: string, newPassword: string) {
    let payload: any;
    try {
      payload = await this.jwtService.verifyAsync(token, {
        secret: this.configService.get&amp;lt;string&amp;gt;('JWT_VERIFICATION_SECRET'),
      });
    } catch {
      throwBadRequest('auth.INVALID_OR_EXPIRED_TOKEN');
    }
    if (payload.email !== email || payload.type !== 'password_reset') {
      throwBadRequest('auth.INVALID_CREDENTIALS');
    }
    const tokenSignature = token.split('.')[2];
    const cacheKey = `reset_password_token:${tokenSignature}`;
    const userId = await this.cacheManager.get&amp;lt;string&amp;gt;(cacheKey);
    if (!userId) {
      throwBadRequest('auth.INVALID_OR_EXPIRED_TOKEN');
    }
    const hashedPassword = this.hashPassword(newPassword);
    await this.usersService.updateById(userId, { password: hashedPassword });
    await this.cacheManager.del(cacheKey);
    return null;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Closing thoughts&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
This authentication system is built around clear trade-offs rather than generic patterns. Short-lived access tokens, refresh token rotation, per-device sessions, explicit revocation, and targeted rate limiting together provide predictable behavior and reasonable security without unnecessary complexity.&lt;/p&gt;
&lt;p&gt;Follow me on X (formerly known as Twitter): &lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/Itstheanurag&quot;&gt;https://x.com/Itstheanurag&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/11214/thinking-auth-in-producation-systems-a-dive-on-the-backend</guid>
<pubDate>Sat, 07 Feb 2026 15:07:52 +0000</pubDate>
</item>
<item>
<title>Advance Auth Templete</title>
<link>https://coderlegion.com/10764/advance-auth-templete</link>
<description>&lt;p&gt;&lt;strong&gt; Advance Auth (TypeScript) – Production-Ready Auth Template for Real Projects&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I built a modular, production-grade authentication template so I never have to rewrite auth again, and now you don’t either.&lt;/p&gt;
&lt;p&gt;This is not a demo. This is a fork-and-build foundation for real SaaS, startups, and side projects.&lt;/p&gt;
&lt;p&gt;Repo: &lt;em&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/SAYOUNCDR/Advance-Auth&quot;&gt;https://github.com/SAYOUNCDR/Advance-Auth&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt; What’s inside&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A full auth system written in TypeScript + Node + Express, designed the way it should be done in real backends:&lt;/p&gt;
&lt;p&gt;Login &amp;amp; Register with HTTP-only cookies&lt;/p&gt;
&lt;p&gt;JWT access + refresh token rotation&lt;/p&gt;
&lt;p&gt;RBAC (User / Admin roles)&lt;/p&gt;
&lt;p&gt;Google OAuth&lt;/p&gt;
&lt;p&gt;Email verification flow&lt;/p&gt;
&lt;p&gt;Forgot / Reset password&lt;/p&gt;
&lt;p&gt;2FA with TOTP (Google Authenticator)&lt;/p&gt;
&lt;p&gt;Zod validation everywhere&lt;/p&gt;
&lt;p&gt;Modular, clean architecture&lt;/p&gt;
&lt;p&gt;Secure password hashing (bcrypt)&lt;/p&gt;
&lt;p&gt;Token versioning, secure cookie handling&lt;/p&gt;
&lt;p&gt;Ready for scaling and extension&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;⚠️ What’s left (being honest)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Only two things are pending:&lt;/p&gt;
&lt;p&gt;Final rate limiting middleware wiring&lt;/p&gt;
&lt;p&gt;A few extra security hardening checks&lt;/p&gt;
&lt;p&gt;The core system is already solid and usable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt; Why this matters&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Auth is boring, repetitive, and easy to mess up.&lt;/p&gt;
&lt;p&gt;Now you can:&lt;/p&gt;
&lt;p&gt;Fork this repo&lt;/p&gt;
&lt;p&gt;Plug in your business logic&lt;/p&gt;
&lt;p&gt;Start building features immediately&lt;/p&gt;
&lt;p&gt;Modify or extend auth without pain&lt;/p&gt;
&lt;p&gt;No rewrites. No hacks. No copy-paste hell.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;️ Stack&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;TypeScript&lt;/p&gt;
&lt;p&gt;Node.js + Express&lt;/p&gt;
&lt;p&gt;MongoDB (Mongoose)&lt;/p&gt;
&lt;p&gt;JWT&lt;/p&gt;
&lt;p&gt;Zod&lt;/p&gt;
&lt;p&gt;OAuth&lt;/p&gt;
&lt;p&gt;Nodemailer&lt;/p&gt;
&lt;p&gt;otplib (2FA)&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Who should use this&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Indie hackers&lt;/p&gt;
&lt;p&gt;Startup founders&lt;/p&gt;
&lt;p&gt;Full-stack devs&lt;/p&gt;
&lt;p&gt;Anyone tired of rebuilding auth from scratch&lt;/p&gt;
&lt;p&gt;Fork it, break it, improve it, ship with it.&lt;/p&gt;
&lt;p&gt;If you care about clean backend engineering, this is for you.&lt;/p&gt;
</description>
<category>Launches</category>
<guid isPermaLink="true">https://coderlegion.com/10764/advance-auth-templete</guid>
<pubDate>Fri, 30 Jan 2026 11:40:29 +0000</pubDate>
</item>
<item>
<title>Building Secure JWT Auth in NestJS: Argon2, Redis Blacklisting, and Token Rotation</title>
<link>https://coderlegion.com/9960/building-secure-jwt-auth-in-nestjs-argon2-redis-blacklisting-and-token-rotation</link>
<description>&lt;p&gt;Imagine you've just watched your first tutorial on authentication. You spin up a login flow, add some JWTs, and call it done. Then you push to production and realize those tutorials skipped the hard parts—token revocation, session management, replay attacks.&lt;/p&gt;
&lt;p&gt;It's not enough to have a login endpoint. Production auth means thinking like an attacker and handling edge cases that basic tutorials ignore. Let's talk about what actually secure authentication looks like in NestJS and how to implement it without the security holes.&lt;/p&gt;
&lt;p&gt;When a developer says they are &quot;implementing auth,&quot; they are usually talking about two distinct but inseparable concepts: &lt;strong&gt;Authentication&lt;/strong&gt; and &lt;strong&gt;Authorization&lt;/strong&gt;. When building robust systems, these go hand in hand.&lt;/p&gt;
&lt;h4 id=&quot;authentication-authn-vs-authorization-authz-&quot;&gt;Authentication (AuthN) vs. Authorization (AuthZ)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Authentication&lt;/strong&gt; is about &lt;strong&gt;identity&lt;/strong&gt;. It answers &quot;Who are you?&quot; using credentials like email/password, passkeys, or biometrics.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authorization&lt;/strong&gt; is about &lt;strong&gt;permissions&lt;/strong&gt;. It answers &quot;What can you do?&quot; Once we know who you are, we use strategies like Role-Based Access Control (RBAC) to ensure you only touch the resources you're allowed to.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-nestjs-toolkit-passport-and-guards&quot;&gt;The NestJS Toolkit: Passport and Guards&lt;/h3&gt;
&lt;p&gt;In the NestJS ecosystem, we don't reinvent the wheel. We use &lt;strong&gt;Passport.js&lt;/strong&gt;, the industry standard for Node.js authentication. NestJS wraps this in &lt;code&gt;@nestjs/passport&lt;/code&gt; and provides &lt;strong&gt;AuthGuards&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Guards are the &quot;bouncers&quot; of your application. When a request hits a protected route, the Guard intercepts it, extracts the token, and validates it before the request ever reaches your controller logic.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(
    private readonly reflector: Reflector,
    private readonly redis: RedisService,
  ) {
    super();
  }

  async canActivate(context: ExecutionContext): Promise&amp;lt;boolean&amp;gt; {
    const isPublic = this.reflector.getAllAndOverride&amp;lt;boolean&amp;gt;(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (isPublic) return true;

    const request = context.switchToHttp().getRequest();
    if (!request.headers['authorization']) return false;

    const token = request.headers['authorization'].split(' ')[1] as string;

    // Check if the token was manually revoked (logout)
    const isBlacklisted = await this.redis.isTokenBlacklisted(token);
    if (isBlacklisted) return false;

    const result = super.canActivate(context);
    return result instanceof Promise ? result : (result as boolean);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;secure-onboarding-why-argon2-&quot;&gt;Secure Onboarding: Why Argon2?&lt;/h3&gt;
&lt;p&gt;For sign-up, you need to store passwords. I chose &lt;strong&gt;Argon2&lt;/strong&gt; over the more common Bcrypt. Argon2 was the winner of the Password Hashing Competition for a reason: it's &lt;strong&gt;memory-intensive&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Unlike older algorithms, Argon2 allows you to configure memory usage, making it significantly harder to crack using specialized hardware like GPUs or ASICs. It essentially punishes brute-force attempts with slow, high-resource computation.&lt;/p&gt;
&lt;h4 id=&quot;registration-hashing-&quot;&gt;Registration (Hashing)&lt;/h4&gt;
&lt;p&gt;When a user signs up, the plain-text password is transformed into a hash before being stored in the database.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// UsersService
import { hash } from '@node-rs/argon2';

async create(data: CreateUserDto): Promise&amp;lt;User&amp;gt; {
  let userData = { ...data };
  if (data.password) {
    const passwordHash = await hash(data.password); //
    userData = { ...userData, password: passwordHash };
  }
  return await this.prisma.user.create({ data: userData });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;login-verification-&quot;&gt;Login (Verification)&lt;/h4&gt;
&lt;p&gt;Crucially, we never &quot;decrypt&quot; a password. When a user logs in:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;We retrieve the stored hash from the DB.&lt;/li&gt;
&lt;li&gt;We take the plain-text password provided in the login form.&lt;/li&gt;
&lt;li&gt;We run the provided password through the same hashing process (including the &quot;salt&quot; stored within the hash).&lt;/li&gt;
&lt;li&gt;If the resulting hash matches the stored one, the user is verified.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { verify } from '@node-rs/argon2';

// AuthService
async validateUser(email: string, password: string): Promise&amp;lt;UserEntity | undefined&amp;gt; {
  const user = await this.usersService.findOneByEmail(email);
  if (user &amp;amp;&amp;amp; user.password) {
    /* The verify function automatically creates a hash of the plain
password and compares it against our stored password hash */
    const isPasswordValid = await verify(user.password, password);
    if (isPasswordValid) return new UserEntity(user);
    throw new UnauthorizedException('Password is not valid');
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-dual-token-strategy&quot;&gt;The Dual-Token Strategy&lt;/h3&gt;
&lt;p&gt;Once verified, we generate two tokens.&lt;/p&gt;
&lt;h4 id=&quot;1-the-access-token-short-lived-amp-stateless-&quot;&gt;1. The Access Token (Short-lived &amp;amp; Stateless)&lt;/h4&gt;
&lt;p&gt;Signed with a secret key, this contains the user’s ID and metadata. We set this to &lt;strong&gt;15 minutes&lt;/strong&gt;. This is the industry standard because it limits the &quot;blast radius&quot;—if a token is stolen, it’s only useful for a tiny window.&lt;/p&gt;
&lt;h4 id=&quot;2-the-refresh-token-long-lived-amp-stateful-&quot;&gt;2. The Refresh Token (Long-lived &amp;amp; Stateful)&lt;/h4&gt;
&lt;p&gt;Signed with a &lt;strong&gt;completely different secret&lt;/strong&gt; (this is vital for security), we set this to &lt;strong&gt;7 days&lt;/strong&gt;. Unlike the access token, we store this in our database (Prisma) because it needs to be &lt;strong&gt;stateful&lt;/strong&gt;. We also attach a unique JTI (JWT ID) when signing the token to allow for efficient database lookups after decoding the JWT.&lt;/p&gt;
&lt;p&gt;By storing it, we gain a &quot;Kill Switch.&quot; If we detect suspicious activity, we can manually revoke that specific token in the DB, preventing it from ever being used to generate a new access token. Here's how we create and store the refresh token in our database:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { randomUUID } from 'node:crypto';

// JwtRefreshService
async create(payload: CreateRefreshTokenDto): Promise&amp;lt;string&amp;gt; {
  const tokenId = randomUUID(); // Unique ID for this token
  const refresh_token = this.jwtService.sign(
    { ...payload, jti: tokenId },
    {
      secret: this.configService.get('JWT_REFRESH_SECRET'),
      expiresIn: '7d',
    },
  );

  const hashed_refresh_token = await hash(refresh_token);

  await this.prisma.refreshToken.create({
    data: {
      id: tokenId,
      token: hashed_refresh_token,
      userId: payload.sub,
      absolute_limit_at: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // Hard session limit
      isRevoked: false,
    },
  });

  return refresh_token;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;solving-the-stateless-revocation-problem-with-redis&quot;&gt;Solving the &quot;Stateless Revocation&quot; Problem with Redis&lt;/h3&gt;
&lt;p&gt;The biggest flaw in JWTs is that they are stateless. If a user logs out, their Access Token is still technically valid until those 15 minutes are up.&lt;/p&gt;
&lt;p&gt;To fix this without destroying performance, we use &lt;strong&gt;Redis&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Redis is an in-memory key-value store. Because it lives in RAM, it provides the sub-millisecond read/writes we need to check every single incoming request without slowing down the API. On logout, we &quot;blacklist&quot; the access token in Redis for the remainder of its lifespan. Our &lt;code&gt;JwtAuthGuard&lt;/code&gt; then checks Redis on every request: if the token is in the blacklist, access is denied. When the user logs out, we calculate how long until the access token would naturally expire and blacklist it for exactly that duration:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// RedisService implementation
async blacklistToken(token: string, ttl: number): Promise&amp;lt;void&amp;gt; {
  await this.redis.set(`blacklist:${token}`, '1', { EX: ttl }); //
}

async isTokenBlacklisted(token: string): Promise&amp;lt;boolean&amp;gt; {
    const result = await this.redis.exists(`blacklist:${token}`);
    return result === 1;
 }

// JwtAuthGuard
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
    // existing authorization logic

    // Check if the token was manually revoked (logout)
    const isBlacklisted = await this.redis.isTokenBlacklisted(token);
    if (isBlacklisted) return false;

    // return result
   }
}

// Logout logic
async logout(data: { userId: string; access_token: string; refresh_token: string }) {
  const access_token_payload = await this.jwtService.verifyAsync(data.access_token);
  const ttl = access_token_payload.exp - Math.floor(Date.now() / 1000);
  
  if (ttl &amp;gt; 0) {
    await this.redisService.blacklistToken(data.access_token, ttl); //
  }
  // ... proceed to revoke refresh token
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;refresh-token-rotation-amp-absolute-limits&quot;&gt;Refresh Token Rotation &amp;amp; Absolute Limits&lt;/h3&gt;
&lt;p&gt;What if a refresh token is stolen? An attacker could theoretically stay logged in for 7 days. We solve this with &lt;strong&gt;Refresh Token Rotation&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Every time a user uses a Refresh Token to get a new Access Token, we:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Invalidate&lt;/strong&gt; the old Refresh Token.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Issue&lt;/strong&gt; a brand-new Refresh Token.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;the-absolute-time-limit&quot;&gt;The Absolute Time Limit&lt;/h4&gt;
&lt;p&gt;To prevent a session from being kept alive forever by rotation, we implement an &lt;strong&gt;Absolute Time Limit&lt;/strong&gt; (e.g., 90 days). This is set at the initial login. Every time a token rotates, that original 90-day timestamp is carried forward. Once we hit that date, no more rotations are allowed—the user must log in with their password again.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// rotateRefreshToken logic
async rotateRefreshToken(payload: RotateRefreshTokenDto) {
  const { old_refresh_token_id, time_limit, sub, email, deviceInfo } = payload;
  
  await this.revokeRefreshToken(old_refresh_token_id); // Invalidate old

  const new_refresh_token = this.jwtService.sign(
        {
          email,
          sub,
          deviceInfo,
        },
        {
          secret: this.configService.get&amp;lt;string&amp;gt;('JWT_REFRESH_SECRET'),
          expiresIn: '7d',
        },
      );
  
  // Create new token record with the original time_limit
  await this.prisma.refreshToken.create({
    data: {
      token: await hash(new_refresh_token),
      userId: sub,
      absolute_limit_at: time_limit, // Carry forward absolute limit
      isRevoked: false,
    },
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;  async revokeRefreshToken(token_id: string) {
    try {
      await this.prisma.refreshToken.update({
        where: {
          id: token_id,
        },
        data: {
          isRevoked: true,
        },
      });
    } catch (error) {
      if (error instanceof Prisma.PrismaClientKnownRequestError) {
        if (error.code === 'P2025') {
          throw new NotFoundException('Token not found');
        }
      }
      throw error;
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;  @Public()
  @UseGuards(JwtRefreshAuthGuard)
  @Post('refresh')
  async refresh(
    @Request()
    req: RequestType &amp;amp; {
      user: { email: string; id: string; refreshToken: string };
    },
    @Response({ passthrough: true }) res: ResponseType,
  ) {
    if (req.user) {
      const tokens = await this.authService.refreshTokens({
        userId: req.user.id ?? '',
        email: req.user.email ?? '',
        token: req.user.refreshToken ?? '',
      });

      res.cookie('titan_refresh_token', tokens?.refresh_token, {
        httpOnly: true,
        secure: true,
        sameSite: 'strict',
      });

      return { access_token: tokens?.access_token };
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;reuse-detection-the-nuclear-option-&quot;&gt;Reuse Detection (The &quot;Nuclear&quot; Option)&lt;/h4&gt;
&lt;p&gt;If a token that was already revoked is used, it signals a potential theft. The system responds by revoking all tokens for that user.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// validateToken snippet
if (storedToken?.isRevoked) {
  // Nuke 'em
  await this.prisma.refreshToken.updateMany({
    where: { userId: payload.userId },
    data: { isRevoked: true }, // Revoke all active sessions for this user
  });
  throw new UnauthorizedException('Token reuse detected. Please login again');
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;One thing to note is that while we return the access token to the user in the response body, we have to set the refresh token as a http-only cookie.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;  @Post('login')
@UseGuards(LocalAuthGuard)
async login(
  @Request() req: RequestType &amp;amp; { user: UserEntity },
  @Response({ passthrough: true }) res: ResponseType,
) {
  const deviceInfo = req.headers['user-agent'] || 'Unknown Device';

  const { access_token, refresh_token } = await this.authService.login(
    req.user,
    deviceInfo,
  );

  // Securely set the Refresh Token as a cookie
  res.cookie('titan_refresh_token', refresh_token, {
    httpOnly: true, // Prevents JS from reading the cookie
    secure: true,   // Only sent over HTTPS
    sameSite: 'strict', // Protects against CSRF
    path: '/auth/refresh', // Optional: Only send cookie to the refresh endpoint
  });

  // Send the Access Token in the body for the frontend to store in memory
  return { access_token };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The reason for this is to make the refresh token invisible to the client-side javascript. Even if an attacker successfully runs a script on your page, they cannot &quot;read&quot; the refresh token. We also set secure: true (ensuring it's only sent over HTTPS) and sameSite: strict to protect against CSRF attacks.&lt;/p&gt;
&lt;p&gt;As for the access token, the frontend receives it in the JSON response and typically stores it in memory (a variable). While it's still technically vulnerable to XSS, because it isn't persisted in storage, it's much harder to exfiltrate, and its short 15-minute lifespan limits the damage if it is compromised&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion-3&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;At the end of the day, writing code alone doesn't build a secure authentication system. We need to constantly balance security, speed, and user experience. By thinking like an attacker, we can turn standard JWTs into a fortified system using tools like Redis and techniques like Refresh Rotation. It takes a bit more effort to set up, but the peace of mind knowing you’ve accounted for edge cases like token theft or stale sessions is well worth the complexity, especially in sensitive applications.&lt;/p&gt;
&lt;h4 id=&quot;summary-of-our-security-workflow&quot;&gt;Summary of our Security Workflow&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Memory-Hard Hashing:&lt;/strong&gt; Used Argon2 to ensure passwords are resistant to GPU-based cracking.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dual-Token Architecture:&lt;/strong&gt; Balanced security with short-lived (15 min) access tokens and long-lived (7 day) refresh tokens.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Real-time Revocation:&lt;/strong&gt; Leveraged Redis for sub-millisecond access token blacklisting upon logout.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Session Lifecycle Management:&lt;/strong&gt; Implemented 90-day absolute time limits to ensure no session lasts forever.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automatic Breach Detection:&lt;/strong&gt; Enabled Refresh Token Rotation with a &quot;nuclear&quot; reuse detection trigger that invalidates all user sessions if a stolen token is detected.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;technical-stack-overview&quot;&gt;Technical Stack Overview&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Tool &lt;/th&gt;&lt;th&gt;Purpose &lt;/th&gt;&lt;th&gt;Key Feature&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;NestJS &lt;/td&gt;&lt;td&gt;Backend Framework &lt;/td&gt;&lt;td&gt;Modular architecture and robust dependency injection.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Passport.js &lt;/td&gt;&lt;td&gt;Auth Logic &lt;/td&gt;&lt;td&gt;Industry-standard middleware for handling JWT strategies.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Argon2 &lt;/td&gt;&lt;td&gt;Password and Token Hashing &lt;/td&gt;&lt;td&gt;Memory-intensive hashing&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Prisma &lt;/td&gt;&lt;td&gt;Database ORM &lt;/td&gt;&lt;td&gt;Type-safe interaction with our stateful Refresh Token storage.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Redis &lt;/td&gt;&lt;td&gt;In-Memory Store &lt;/td&gt;&lt;td&gt;Rapid read/write operations for stateless token revocation (blacklisting).&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;JWT &lt;/td&gt;&lt;td&gt;Tokenization &lt;/td&gt;&lt;td&gt;Secure, encoded payload delivery for AuthN and AuthZ.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/9960/building-secure-jwt-auth-in-nestjs-argon2-redis-blacklisting-and-token-rotation</guid>
<pubDate>Sun, 18 Jan 2026 17:16:38 +0000</pubDate>
</item>
<item>
<title>Login-free trials using localStorage refresh tokens</title>
<link>https://coderlegion.com/8430/login-free-trials-using-localstorage-refresh-tokens</link>
<description>&lt;p&gt;I just posted on Hacker News about a pattern I implemented for letting users try my side project without signing up first.&lt;/p&gt;
&lt;p&gt;The short version: click &quot;Try it now&quot;, you're immediately in the dashboard with a working account. No email, no OAuth. A guest secret gets stored in localStorage, and if you come back later we swap it for a fresh JWT. Sign up for real whenever you want and your stuff transfers over.&lt;/p&gt;
&lt;p&gt;I wrote up the security considerations (separate JWT keypairs for guests vs real users, hashed secrets, rate limiting, etc.) and the trade-offs I'm accepting (localStorage clearing = lost access, single device only).&lt;/p&gt;
&lt;p&gt;Would love to hear if anyone else has done something similar or has thoughts on the approach.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Full post and discussion on HN:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://news.ycombinator.com/item?id=46244742&quot;&gt;https://news.ycombinator.com/item?id=46244742&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The project is Spikelog (simple metrics tracking) if you want to poke at the implementation: &lt;a rel=&quot;nofollow&quot; href=&quot;https://spikelog.com&quot;&gt;https://spikelog.com&lt;/a&gt;&lt;/p&gt;
</description>
<category>Web Dev</category>
<guid isPermaLink="true">https://coderlegion.com/8430/login-free-trials-using-localstorage-refresh-tokens</guid>
<pubDate>Fri, 12 Dec 2025 15:25:16 +0000</pubDate>
</item>
<item>
<title>Website Authentication Made Simple with SafeLine</title>
<link>https://coderlegion.com/8108/website-authentication-made-simple-with-safeline</link>
<description>&lt;p&gt;Website authentication is a fundamental part of modern web security. It ensures that only authorized users can access protected resources, reducing the risk of data breaches, abuse, and unauthorized operations.&lt;/p&gt;
&lt;p&gt;This article will explain what website authentication is, when it is needed, introduce several free and practical authentication tools, and finally walk through SafeLine WAF’s authentication feature and how to configure it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-website-authentication-&quot;&gt;What Is Website Authentication?&lt;/h3&gt;
&lt;p&gt;Website authentication is the process of &lt;strong&gt;verifying a user’s identity&lt;/strong&gt; before granting access to a website, application, or specific resources.&lt;/p&gt;
&lt;p&gt;Common authentication methods include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Username and password&lt;/li&gt;
&lt;li&gt;Tokens or API keys&lt;/li&gt;
&lt;li&gt;Cookies and sessions&lt;/li&gt;
&lt;li&gt;Single Sign-On (SSO)&lt;/li&gt;
&lt;li&gt;IP-based or request-based verification&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Authentication answers the question:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;“Who are you, and should you be allowed to access this resource?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;when-do-you-need-website-authentication-&quot;&gt;When Do You Need Website Authentication?&lt;/h3&gt;
&lt;p&gt;Not every website needs authentication, but it becomes essential in many scenarios:&lt;/p&gt;
&lt;h4 id=&quot;administrative-interfaces&quot;&gt;Administrative Interfaces&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Admin panels (&lt;code&gt;/admin&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Internal dashboards&lt;/li&gt;
&lt;li&gt;Management APIs&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;internal-or-private-services&quot;&gt;Internal or Private Services&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Staging environments&lt;/li&gt;
&lt;li&gt;Internal tools&lt;/li&gt;
&lt;li&gt;Test platforms not meant for public access&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;apis-and-web-services&quot;&gt;APIs and Web Services&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Prevent unauthorized API usage&lt;/li&gt;
&lt;li&gt;Protect against abuse and scraping&lt;/li&gt;
&lt;li&gt;Control access for partners or customers&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;temporary-protection&quot;&gt;Temporary Protection&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Maintenance pages&lt;/li&gt;
&lt;li&gt;Pre-launch websites&lt;/li&gt;
&lt;li&gt;Emergency protection during attacks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your website exposes &lt;strong&gt;sensitive functionality or data&lt;/strong&gt;, authentication should be considered mandatory.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;free-and-useful-website-authentication-tools&quot;&gt;Free and Useful Website Authentication Tools&lt;/h3&gt;
&lt;p&gt;Here are some popular and free authentication solutions commonly used today:&lt;/p&gt;
&lt;h4 id=&quot;1-basic-authentication&quot;&gt;1. Basic Authentication&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Built into HTTP standard&lt;/li&gt;
&lt;li&gt;Username and password via browser prompt&lt;/li&gt;
&lt;li&gt;Easy to configure&lt;/li&gt;
&lt;li&gt;Best for internal tools or temporary protection&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;✅ Simple&lt;br&gt;
❌ Not suitable for public-facing login systems&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;2-oauth-openid-connect&quot;&gt;2. OAuth / OpenID Connect&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Used by Google, GitHub, Microsoft, etc.&lt;/li&gt;
&lt;li&gt;Enables Single Sign-On (SSO)&lt;/li&gt;
&lt;li&gt;Supported by many open-source tools&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Popular providers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Google OAuth&lt;/li&gt;
&lt;li&gt;GitHub OAuth&lt;/li&gt;
&lt;li&gt;Auth0 (free tier available)&lt;/li&gt;
&lt;li&gt;Keycloak (open-source)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;✅ Secure and scalable&lt;br&gt;
❌ Slightly more complex to deploy&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;3-api-key-token-based-authentication&quot;&gt;3. API Key / Token-Based Authentication&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Common for APIs&lt;/li&gt;
&lt;li&gt;Client sends a token in headers&lt;/li&gt;
&lt;li&gt;Easy to rotate and revoke&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;✅ Ideal for machine-to-machine access&lt;br&gt;
❌ Requires careful key management&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;4-reverse-proxy-authentication&quot;&gt;4. Reverse Proxy Authentication&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Authentication handled at the gateway layer&lt;/li&gt;
&lt;li&gt;No need to modify backend applications&lt;/li&gt;
&lt;li&gt;Ideal for legacy systems&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;✅ Very flexible&lt;br&gt;
✅ Application-agnostic  &lt;/p&gt;
&lt;p&gt;This is where &lt;strong&gt;SafeLine WAF authentication&lt;/strong&gt; shines.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jznkxfn4y1c1v2am7q0p.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;safeline-waf-authentication-overview&quot;&gt;SafeLine WAF Authentication: Overview&lt;/h3&gt;
&lt;p&gt;SafeLine WAF provides a built-in &lt;strong&gt;identity authentication feature&lt;/strong&gt; at the WAF layer, allowing users to protect websites and applications &lt;strong&gt;without changing backend code&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;With SafeLine authentication, you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add authentication to any website behind SafeLine&lt;/li&gt;
&lt;li&gt;Protect directories, paths, or entire domains&lt;/li&gt;
&lt;li&gt;Block unauthorized users before traffic reaches the server&lt;/li&gt;
&lt;li&gt;Reduce attack surface and backend load&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is especially useful for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Admin panels&lt;/li&gt;
&lt;li&gt;Internal services&lt;/li&gt;
&lt;li&gt;Temporary access control&lt;/li&gt;
&lt;li&gt;Protecting legacy applications&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-to-configure-authentication-in-safeline&quot;&gt;How To Configure Authentication in SafeLine&lt;/h3&gt;
&lt;h4 id=&quot;basic-configuration&quot;&gt;Basic Configuration&lt;/h4&gt;
&lt;p&gt;SafeLine provides two authentication modes: Simple Auth and SSO.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Simple Auth: Application-specific authentication system where users can only access that application after logging in&lt;/li&gt;
&lt;li&gt;SSO (Single Sign-On): allowing users to access multiple related applications after logging in once, without repeated logins. See &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/reference/articles/Auth-SSO&quot;&gt;SSO&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Field &lt;/th&gt;&lt;th&gt;Description&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Login Method &lt;/td&gt;&lt;td&gt;Multiple login methods available, including AccountPassword, &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/reference/articles/Auth-OIDC&quot;&gt;OIDC&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/reference/articles/Auth-GitHub&quot;&gt;Github&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/reference/articles/Auth-LDAP&quot;&gt;LDAP&lt;/a&gt;. For SSO mode, configuration must be done in &lt;code&gt;AUTH-SETTINGS-SSO&lt;/code&gt;.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Auth Callback URL &lt;/td&gt;&lt;td&gt;Required when passing user authentication information to the application server, used to exchange for user information.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Application Redirect URL &lt;/td&gt;&lt;td&gt;Required in SSO mode. When clicking on the application in the unified management panel, it will redirect to this address.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;advanced-configuration&quot;&gt;Advanced Configuration&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Approval Configuration&lt;/strong&gt;: Options for authorization approval&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Need to approve access: First-time users will trigger an approval request after authentication, requiring admin approval before accessing.&lt;/li&gt;
&lt;li&gt;Access directly after authentication: Users can directly access the application after authentication without approval.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Enable authentication for specific conditions&lt;/strong&gt;: Supports options to authenticate when conditions are met or skip authentication when conditions are met&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;how-to-configure-passing-user-authentication-information-to-the-application-server-&quot;&gt;How to Configure Passing User Authentication Information to the Application Server?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;After successful user authentication, SafeLine will redirect to the following URL, where &lt;code&gt;http://example.com/application&lt;/code&gt; is the &lt;code&gt;Auth Callback URL&lt;/code&gt; configured:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;http://example.com/application?code=123456&amp;amp;redirect_uri=original_user_access_address  
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Find the &lt;code&gt;API Token&lt;/code&gt; in the SafeLine console under &lt;code&gt;Settings-Management&lt;/code&gt; page&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The application should implement using the &lt;code&gt;code&lt;/code&gt; to request SafeLine's &lt;code&gt;/.safeline/auth/api/user&lt;/code&gt; interface to obtain authenticated user information at the &lt;code&gt;Auth Callback URL&lt;/code&gt;. The &lt;code&gt;code&lt;/code&gt; can only be used once&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// Request:

GET http://safeline-console.com/.safeline/auth/api/user?code=123456

X-SLCE-API-TOKEN: safeline-api-token

// Response:

Content-Type: application/json

{
    &quot;code&quot;: 0, // 0: success, non-zero: failure
    &quot;msg&quot;: &quot;Error description&quot;,
    &quot;data&quot;: {
        &quot;id&quot;: 1,
        &quot;username&quot;: &quot;username&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;After successfully obtaining user information, the application should cache the login information to determine whether the current user is logged in for subsequent requests, then redirect to the &lt;code&gt;redirect_uri&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;If retrieving user information fails, the application needs to delete cookies with the prefix sl_auth&lt;em&gt;session&lt;/em&gt;, prompting the user to authenticate again&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iz6t6a14hagyamp97vol.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-use-waf-level-authentication-&quot;&gt;Why Use WAF-Level Authentication?&lt;/h3&gt;
&lt;p&gt;Compared to application-level authentication, WAF-level authentication offers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ No code changes&lt;/li&gt;
&lt;li&gt;✅ Faster deployment&lt;/li&gt;
&lt;li&gt;✅ Consistent protection across services&lt;/li&gt;
&lt;li&gt;✅ Reduced backend attack surface&lt;/li&gt;
&lt;li&gt;✅ Ideal for legacy or third-party systems&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It provides a lightweight but effective layer of access control exactly where it matters.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion-4&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Website authentication is a critical security mechanism for protecting sensitive resources and preventing unauthorized access. While many tools and frameworks exist, &lt;strong&gt;WAF-level authentication&lt;/strong&gt; offers a powerful and flexible option—especially when backend changes are impractical.&lt;/p&gt;
&lt;p&gt;SafeLine WAF enables teams to implement authentication quickly, securely, and transparently, making it an excellent choice for internal tools, admin panels, APIs, and temporary access control scenarios.&lt;/p&gt;
&lt;p&gt;By combining authentication with SafeLine’s traffic inspection and protection capabilities, you gain both &lt;strong&gt;identity control and attack defense at the gateway level&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;related-resources&quot;&gt;Related Resources&lt;/h3&gt;
&lt;blockquote&gt;&lt;p&gt;SafeLine Website: &lt;a rel=&quot;nofollow&quot; href=&quot;https://ly.safepoint.cloud/ShZAy9x&quot;&gt;https://ly.safepoint.cloud/ShZAy9x&lt;/a&gt;&lt;br&gt;
Live Demo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://demo.waf.chaitin.com:9443/statistics&quot;&gt;https://demo.waf.chaitin.com:9443/statistics&lt;/a&gt;&lt;br&gt;
Discord: &lt;a rel=&quot;nofollow&quot; href=&quot;https://discord.gg/dy3JT7dkmY&quot;&gt;https://discord.gg/dy3JT7dkmY&lt;/a&gt;&lt;br&gt;
Doc: &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;&lt;br&gt;
Github: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/chaitin/SafeLine&quot;&gt;https://github.com/chaitin/SafeLine&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/8108/website-authentication-made-simple-with-safeline</guid>
<pubDate>Wed, 03 Dec 2025 03:08:23 +0000</pubDate>
</item>
<item>
<title>Auth Sessions Should Never Cross Internal Boundaries</title>
<link>https://coderlegion.com/8087/auth-sessions-should-never-cross-internal-boundaries</link>
<description>&lt;p&gt;It is tempting to treat user authentication sessions as ambient context. You pass them through API layers, into service calls, across event processors, and through background jobs. If a component needs to know who the user is, it needs the user's session, right?&lt;/p&gt;
&lt;blockquote class=&quot;pull-quote&quot;&gt;
&lt;p&gt;This conflates authentication with context. You're treating an external trust artifact as internal state.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The result is internal services coupled to the external auth mechanism, a replayable credential spread across them, and systems that break when work runs after the session ends or without one.&lt;/p&gt;
&lt;h3 id=&quot;sessions-validate-at-boundaries-context-flows-internally&quot;&gt;Sessions Validate at Boundaries, Context Flows Internally&lt;/h3&gt;
&lt;h4 id=&quot;what-crosses-the-boundary&quot;&gt;What Crosses the Boundary&lt;/h4&gt;
&lt;p&gt;&quot;Session&quot; here means whatever the client presents to prove who the user is, whether that's a cookie bound to a server-side session or a bearer access token such as a JWT. The arguments below apply to both, and where they differ, the post says which one it means.&lt;/p&gt;
&lt;p&gt;Authentication sessions serve one purpose: validating identity at a security boundary. Once validated, the session should be consumed and replaced with explicit context and service credentials.&lt;/p&gt;
&lt;p&gt;A user session arrives at the boundary: an API endpoint, gateway, or edge function. The boundary validates the session and confirms identity. The session is then consumed. You extract only the necessary context: user_id, tenant_id, correlation_id, a non-secret session id, and, when a client acts for the user, the client's identity and the scopes the user granted it. From that point forward, internal operations use service roles. Components operate with service credentials like database connections, API keys, and queue permissions. User context is passed explicitly as structured data, not as the original session token.&lt;/p&gt;
&lt;p&gt;The granted scopes in that context matter. A third-party app that holds only read access to a user's orders must not gain everything the user can do once its token is converted at the edge. So downstream authorization allows an action only when the user's current permissions and the client's granted scopes both permit it.&lt;/p&gt;
&lt;h4 id=&quot;the-boundary-signs-the-context&quot;&gt;The Boundary Signs the Context&lt;/h4&gt;
&lt;p&gt;Passing context explicitly doesn't mean leaving it unprotected. A downstream service can't accept a bare user_id from anyone who sends one, or any caller could claim to be any user. The context has to arrive in a form the receiver can trust, which means a small object the boundary signs. An authenticated service-to-service channel isn't enough on its own. It proves which service is calling but not which user that service is acting for, so a compromised caller could assert any user_id it liked.&lt;/p&gt;
&lt;p&gt;Netflix describes this design in its &quot;Edge Authentication and Token-Agnostic Identity Propagation&quot; post. Its edge services validate the many token types clients present and mint an integrity-protected identity object, called a Passport, which is what downstream services receive.&lt;/p&gt;
&lt;p&gt;A signed context object is still a credential, so it needs limits of its own:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It's issued and accepted only inside your trust domain, never by an external API.&lt;/li&gt;
&lt;li&gt;It's bound to one request or one message rather than to the user's login, through a short expiry, an audience claim that limits it to your internal services, and a request or message id.&lt;/li&gt;
&lt;li&gt;It carries only the fields the work needs.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The boundary signs with a private key that only it holds, so a compromised internal service can read the context it was given but can't mint one for a different user. Like Netflix's Passport, the same context is passed along the whole call chain rather than re-signed at each hop, so no hop has to call a signing service mid-request.&lt;/p&gt;
&lt;p&gt;The context can still be replayed inside your system. Within its short validity window, a service in the call path could replay it to another internal service, which is the same exposure a forwarded token has. What's gone is the external replay. No API outside your system accepts it, and it doesn't outlive the request.&lt;/p&gt;
&lt;h4 id=&quot;token-exchange-meets-the-line-forwarding-doesn-t&quot;&gt;Token Exchange Meets the Line, Forwarding Doesn't&lt;/h4&gt;
&lt;p&gt;This post draws its line at the first two limits above. Whatever travels inward is issued inside your trust domain and bound to one request or message. A gateway that exchanges the user's token for an internally issued, internally audienced one, using OAuth 2.0 Token Exchange (RFC 8693), meets that line. Forwarding the token the client presented does not.&lt;/p&gt;
&lt;p&gt;Another alternative, per-hop delegation from the external identity provider, has each service trade for a new token audienced to the next one. It answers the replay concern but still crosses the line. Every hop then calls the provider at runtime and parses its token format, while a boundary-signed context is verified locally with a key you own.&lt;/p&gt;
&lt;p&gt;Meeting that line at the gateway doesn't have to be heavy. For a system with two or three services and one issuer, the gateway signing a short-lived internal token with a standard JWT library keeps its cost to one key to manage.&lt;/p&gt;
&lt;h3 id=&quot;why-sessions-don-t-belong-in-internal-flows&quot;&gt;Why Sessions Don't Belong in Internal Flows&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Sessions are external trust artifacts.&lt;/strong&gt; A user session token represents authentication with an external identity provider or your auth system. It's proof that someone outside your system is who they claim to be. Once verified at your boundary, propagating it further treats authentication as global state rather than a specific validation event.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Repeated validation adds a runtime dependency on the auth server.&lt;/strong&gt; Every component that receives a user session has to validate it. For a self-contained token like a JWT, that's a local signature and expiry check. Verifying a boundary-signed context costs about the same, so the saving there is small. For opaque tokens and server-side sessions, validation means a call to the auth server, such as the introspection endpoint defined in RFC 7662. Each hop then adds a network round trip and makes the auth server's availability a condition of every internal call for a single user action.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Coupling to external authentication.&lt;/strong&gt; When internal components accept user sessions, they become coupled to the token format, the issuer's validation requirements, token refresh logic, and external auth provider availability. With one standard OIDC issuer and a shared validation library, switching issuers can be mostly configuration. The coupling shows when the estate isn't that tidy, with several client token types, a proprietary session format, or a migration between them. Then every component that parses sessions has to change with it. Netflix's Passport work was a response to exactly that variety. A boundary-signed context still needs a shared format and key distribution, but that format is yours and doesn't change when the external issuer does.&lt;/p&gt;
&lt;h3 id=&quot;session-propagation-breaks-when-work-outlives-the-request&quot;&gt;Session Propagation Breaks When Work Outlives the Request&lt;/h3&gt;
&lt;p&gt;Propagating user sessions through internal components creates predictable failures whenever work outlives the request or has no user behind it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Session expiration mid-flow&lt;/strong&gt; is the first to appear. Event processors run minutes later and the session has expired. Background jobs run hours later and the session is gone. Async processors retry and the session is invalid. You build workarounds: token refresh in queues, session persistence in metadata, special &quot;system sessions&quot; for background work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Missing sessions entirely&lt;/strong&gt; breaks your logic. Scheduled reports run at 3 AM with no user logged in. Webhooks arrive from external systems with no user session. System maintenance tasks have no user context. You duplicate logic: one path for user requests and one for non-user operations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Context across time and actors&lt;/strong&gt; fragments workflows. Multi-step workflows span hours. The original user's session expires. Different actors have different sessions. Background processors have none. The workflow breaks without explicit context that persists beyond the session lifetime.&lt;/p&gt;
&lt;p&gt;Storing context as data, which the boundary pattern makes the default, removes all three. The service that owns a workflow stores the original context with the workflow's own record, as data about who started it. Each later step or retry works from that record rather than from a credential that expires. When a later step calls another service, the workflow service calls as itself and passes the initiating user_id as data. The receiver checks that this service is allowed to request this action for this tenant, then checks the user's current permissions. A scheduled job or a webhook handler works the same way, with its own service identity and no user at all.&lt;/p&gt;
&lt;p&gt;Pair the pattern with one rule. Every step authorizes against the user's current permissions and account status when it runs, so a user whose access was removed, or whose session was revoked after a compromise, is refused even hours later. The rule is independent of the token question, and a design that forwards tokens could adopt it too, but this pattern can't work without it. The context carries identity and the client's scope ceiling, not the user's roles, so a step has to look up permissions to decide anything. That lookup reads your own permission data, which the service already depends on to do the work, rather than making the external auth server a condition of every hop.&lt;/p&gt;
&lt;p&gt;Async work has a limit here. Nothing in this path ties the user_id to a workflow that user started. A compromised workflow service can therefore act for any user in the tenants it serves, through the operations its own service identity is allowed. That is the same trust any background job carries, and it's why each service's identity should be allowed only the operations that service performs.&lt;/p&gt;
&lt;h3 id=&quot;common-objections&quot;&gt;Common Objections&lt;/h3&gt;
&lt;h4 id=&quot;-we-can-forward-tokens-on-synchronous-calls-only-&quot;&gt;&quot;We Can Forward Tokens on Synchronous Calls Only&quot;&lt;/h4&gt;
&lt;p&gt;The strongest version of this view forwards a short-lived, audience-restricted token down synchronous call chains and uses explicit context only for async work. That view's async half is this post's design. Its sync half still carries the external artifact, so it keeps the coupling to the issuer's format described earlier. Widening the token's audience to cover the internal services, which RFC 9700 permits for a small set of resource servers, keeps the external replay out. It still leaves a credential in the issuer's format that lasts the token's whole lifetime rather than one request.&lt;/p&gt;
&lt;h4 id=&quot;-we-lose-user-context-for-auditing-&quot;&gt;&quot;We Lose User Context for Auditing&quot;&lt;/h4&gt;
&lt;p&gt;You lose the session token, not the user context. Proper auditing requires intentional context capture: user_id, tenant_id, correlation_id, source_ip, user_agent, and timestamp.&lt;/p&gt;
&lt;p&gt;The identity fields travel in the signed context through every component. Request details like source_ip and user_agent are logged once at the boundary and joined to downstream records by correlation_id, so no internal service can forge them. Relying on session tokens for auditing is worse because sessions don't persist for audit trails, you're logging authentication artifacts instead of business context, and different components might parse tokens differently.&lt;/p&gt;
&lt;p&gt;Intentional data flow is the point. If you need user_id in audit logs, require it as an explicit parameter. This forces you to think about what context crosses boundaries and prevents accidental omissions.&lt;/p&gt;
&lt;h4 id=&quot;-compliance-requires-user-identity-throughout-the-system-&quot;&gt;&quot;Compliance Requires User Identity Throughout the System&quot;&lt;/h4&gt;
&lt;p&gt;Compliance rules ask you to authenticate who is acting and to record what they did. The HIPAA Security Rule, for example, requires verifying that a person seeking access is the one claimed (45 CFR 164.312(d)) and mechanisms that record and examine activity in systems holding health data (164.312(b)). Authentication happens at the boundary. Recording happens wherever the action happens, and a trusted user_id in explicit context is what that record needs. Passing the session token does not improve compliance. It couples your compliance logging to your auth mechanism.&lt;/p&gt;
&lt;h4 id=&quot;-service-roles-create-privilege-escalation-risks-&quot;&gt;&quot;Service Roles Create Privilege Escalation Risks&quot;&lt;/h4&gt;
&lt;p&gt;The risk is that a service with broad credentials can be tricked or compromised into acting outside the current user's rights. A forwarded token limits that risk only on calls to other services. On those calls, within one request, a boundary-signed context limits it just as well, since a compromised service can't mint context for another user. Async steps rely on the calling service's own identity, with the limit described earlier.&lt;/p&gt;
&lt;p&gt;For the service's own resources, the token changes nothing. A compromised service with a database connection that reads every tenant's rows can read them whether or not it holds a user token, unless the database itself enforces the user's identity.&lt;/p&gt;
&lt;p&gt;Service roles provide technical capability like database access and API credentials. Business logic enforces authorization through grant checks, tenant isolation, and permission verification. Where a breach would be severe, the data layer can enforce tenant isolation too, for example with row-level security keyed to the tenant in the context. These checks are necessary whether you use service roles or propagate user sessions. Scope each service's credentials to what that service does, so a compromise is limited to one service's capability.&lt;/p&gt;
&lt;p&gt;A forwarded session adds its own risk. A bearer token works for anyone who holds it, so every internal component that receives the user's token can replay it as that user anywhere the token is accepted. That's why RFC 9700, the OAuth 2.0 security best practice, says access tokens should be audience-restricted to a specific resource server. A token restricted to your API shouldn't be accepted by every service behind it, and one that is has become a credential your whole system leaks.&lt;/p&gt;
&lt;p&gt;Service roles also make the split clearer. The service has technical capability but requires explicit business authorization. User sessions blur this distinction by suggesting the session itself grants permission.&lt;/p&gt;
&lt;h4 id=&quot;-zero-trust-means-every-service-verifies-the-user-&quot;&gt;&quot;Zero Trust Means Every Service Verifies the User&quot;&lt;/h4&gt;
&lt;p&gt;Zero trust doesn't ask for the user's original token at every hop. NIST SP 800-207 requires that authentication and authorization are enforced before every access to a resource, rather than inherited from network location or an earlier step. A downstream service that verifies its caller's service identity and the signature on the user context, then authorizes the action against that context, meets the requirement. Forwarding the session meets it no better and spreads a replayable credential.&lt;/p&gt;
&lt;p&gt;The legitimate exception is a component that must call another system as the user, such as a third-party API that only accepts the user's delegated token. Even then the original session doesn't travel. The boundary, the one place that holds the user's token, uses OAuth 2.0 Token Exchange (RFC 8693) to trade it for a new token issued for that one audience and scoped to that call. Work that calls the third party hours later relies on a grant the owning service stores as data, the way a workflow stores its context, not on a token carried through queues.&lt;/p&gt;
&lt;h4 id=&quot;-we-need-sessions-for-distributed-tracing-&quot;&gt;&quot;We Need Sessions for Distributed Tracing&quot;&lt;/h4&gt;
&lt;p&gt;You need correlation IDs for distributed tracing, not session tokens.&lt;/p&gt;
&lt;p&gt;Distributed systems require explicit tracing infrastructure: correlation_id, trace_id, span_id, and timestamps. This works for all request types including user requests, background jobs, webhooks, internal system tasks, lambda invocations, and event processors.&lt;/p&gt;
&lt;p&gt;If you rely on session tokens for tracing, non-user-initiated requests fail. When you need to join traces to a login session, the non-secret session id in the context does that without carrying the token.&lt;/p&gt;
&lt;h4 id=&quot;-we-re-using-a-monolith-this-doesn-t-apply-&quot;&gt;&quot;We're Using a Monolith, This Doesn't Apply&quot;&lt;/h4&gt;
&lt;p&gt;Inside one process there's no token crossing a network and nothing to replay, so the security arguments above weigh less. Most web frameworks already apply the boundary rule for you, since their request principal holds a validated identity rather than the raw token. What remains is where business logic gets that identity. You still must avoid global user session context and pass user context within structures appropriate to the needs of each module.&lt;/p&gt;
&lt;p&gt;Business logic that reads the current user from the web request couples your service layer to your web layer. A background job, CLI tool, or internal script that calls the same logic has no web request, so it either fails the way a missing session fails or has to fake a request principal first. Logic that takes the user as a parameter can be called from any of them as it stands.&lt;/p&gt;
&lt;p&gt;The same principles apply: modules accept explicit context and perform explicit authorization. Whether those modules are in separate processes or the same codebase is irrelevant to that design.&lt;/p&gt;
&lt;h3 id=&quot;respect-the-boundary&quot;&gt;Respect the Boundary&lt;/h3&gt;
&lt;p&gt;User authentication sessions serve one purpose: validating identity at a security boundary. Once validated, the session has done its job.&lt;/p&gt;
&lt;p&gt;In practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Validate the session once, and let no component behind it parse the token&lt;/li&gt;
&lt;li&gt;Extract only the context the work needs, such as user_id, tenant_id, correlation_id, and the calling client's granted scopes&lt;/li&gt;
&lt;li&gt;Sign that context at the boundary, bind it to one request or message, and send it over authenticated service calls&lt;/li&gt;
&lt;li&gt;Give each service its own narrowly scoped credentials&lt;/li&gt;
&lt;li&gt;Authorize every action against current permissions, account status, and the client's granted scopes, whether a user, a webhook, or a scheduled job started it&lt;/li&gt;
&lt;li&gt;When a component must act as the user elsewhere, exchange for a new token scoped to that call&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote class=&quot;pull-quote&quot;&gt;
&lt;p&gt;This isn't about microservices versus monoliths. It's about recognizing architectural boundaries and respecting them. Authentication artifacts belong at boundaries, not in internal flows.&lt;/p&gt;
&lt;/blockquote&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/8087/auth-sessions-should-never-cross-internal-boundaries</guid>
<pubDate>Tue, 02 Dec 2025 15:31:54 +0000</pubDate>
</item>
<item>
<title>Authentication Tokens: Your Digital VIP Pass </title>
<link>https://coderlegion.com/5540/authentication-tokens-your-digital-vip-pass</link>
<description>&lt;p&gt;In today's connected world, we all want to access our favorite apps and websites without re-entering our password every two seconds. That's where &lt;strong&gt;authentication tokens&lt;/strong&gt;, or &quot;auth tokens,&quot; come in. Think of them like a digital VIP pass. When you log in with your username and password, the server checks your credentials and, if everything's good, hands you a special token. Instead of sending your password with every request, your app sends this token. The server then uses the token to confirm you're still you, and you can access all the cool stuff you're supposed to.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-it-works-a-simple-flow&quot;&gt;How It Works: A Simple Flow&lt;/h3&gt;
&lt;p&gt;The basic process is pretty straightforward:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Login:&lt;/strong&gt; You enter your username and password on an application's login screen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verification:&lt;/strong&gt; The application sends your credentials to its server for verification.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Token Generation:&lt;/strong&gt; If your credentials are correct, the server generates a unique auth token.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Token Received:&lt;/strong&gt; The server sends this token back to your application, which stores it, usually in a cookie or local storage.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Access:&lt;/strong&gt; From now on, whenever you need to access a protected resource, like your profile page or your shopping cart, the application includes the token in the request header.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Token Validation:&lt;/strong&gt; The server receives the request, validates the token, and grants access if it's valid.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=7036098958547073519&quot; alt=&quot;A Simple Flow&quot;&gt;&lt;/p&gt;
&lt;p&gt;This system is great for &lt;strong&gt;scalability&lt;/strong&gt; because the server doesn't need to remember who is logged in. This makes the server &lt;strong&gt;stateless&lt;/strong&gt;, which is a huge plus for performance and building microservice architectures.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;types-of-tokens-you-ll-encounter&quot;&gt;Types of Tokens You'll Encounter&lt;/h3&gt;
&lt;p&gt;There are a few different types of tokens, each with its own pros and cons.&lt;/p&gt;
&lt;h4 id=&quot;1-json-web-tokens-jwts-&quot;&gt;1. JSON Web Tokens (JWTs)&lt;/h4&gt;
&lt;p&gt;A &lt;strong&gt;JWT&lt;/strong&gt; (pronounced &quot;jot&quot;) is a popular type of token that's &lt;strong&gt;self-contained&lt;/strong&gt;. This means all the necessary user information is right inside the token itself. A JWT is made up of three parts, separated by periods:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Header:&lt;/strong&gt; Describes the token type and the signing algorithm.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Payload:&lt;/strong&gt; Contains &quot;claims,&quot; which are statements about the user and the token. This is where you'd find user ID, roles, and the token's expiration date.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Signature:&lt;/strong&gt; A unique signature created using a secret key. This is what the server uses to verify that the token hasn't been tampered with.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because a JWT contains everything the server needs to know, the server doesn't have to perform a database lookup for every request. This makes them really fast and efficient.&lt;/p&gt;
&lt;p&gt;Here's a quick Python example using the &lt;code&gt;PyJWT&lt;/code&gt; library to create a JWT:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import jwt
import datetime

payload_data = {
    'sub': '1234567890',
    'name': 'John Doe',
    'iat': datetime.datetime.now(datetime.timezone.utc),
    'exp': datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=30)
}

secret_key = 'your_super_secret_key' # In a real app, use an environment variable!

token = jwt.encode(payload_data, secret_key, algorithm='HS256')

print(token)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can then decode and verify this token on the server side to get the payload data.&lt;/p&gt;
&lt;h4 id=&quot;2-opaque-tokens&quot;&gt;2. Opaque Tokens&lt;/h4&gt;
&lt;p&gt;Unlike JWTs, opaque tokens are just random strings of characters. The client can't read anything from them. They are essentially a &lt;strong&gt;reference&lt;/strong&gt; to a record stored in the server's database. When a server receives an opaque token, it has to go to the database to look up the token and retrieve the associated user data and permissions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros &amp;amp; Cons:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;JWTs:&lt;/strong&gt; Fast, scalable, and don't require database lookups. The downside is that once issued, they can't be easily revoked before they expire.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Opaque Tokens:&lt;/strong&gt; More secure since no information is exposed to the client. They are also easily revocable. The trade-off is that they require a database lookup on every request, which can add overhead.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;3-hybrid-tokens&quot;&gt;3. Hybrid Tokens&lt;/h4&gt;
&lt;p&gt;A common and secure pattern is to use a &lt;strong&gt;hybrid approach&lt;/strong&gt;. This combines the best of both worlds. You'll often see this with OAuth 2.0:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Short-lived Access Token:&lt;/strong&gt; This is a JWT, used for accessing resources. It's valid for a short time (e.g., 15 minutes), so if it's stolen, the attacker doesn't have much time to use it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Long-lived Refresh Token:&lt;/strong&gt; This is an opaque token, used to get a new access token once the old one expires. It's stored securely and can be revoked by the server if needed. This prevents a user from having to log in again every 15 minutes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This approach balances the performance benefits of JWTs with the security and revocability of opaque tokens.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion-which-one-should-you-use-&quot;&gt;Conclusion: Which One Should You Use?&lt;/h3&gt;
&lt;p&gt;The choice of token depends on your application's needs. For a simple app where you want to keep things fast and stateless, a &lt;strong&gt;JWT&lt;/strong&gt; might be the perfect fit. For a system that requires a high level of security and granular control over token revocation, a &lt;strong&gt;hybrid model&lt;/strong&gt; is often the way to go. No matter which you choose, understanding how they work is a crucial skill for any developer building secure and scalable applications. &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/5540/authentication-tokens-your-digital-vip-pass</guid>
<pubDate>Wed, 24 Sep 2025 13:05:12 +0000</pubDate>
</item>
<item>
<title>PostgreSQL Client Certificate Authentication: Complete Setup Guide</title>
<link>https://coderlegion.com/4373/postgresql-client-certificate-authentication-complete-setup-guide</link>
<description>&lt;p&gt;Client certificate authentication in PostgreSQL provides a secure, passwordless way to authenticate users. Instead of relying on passwords, clients present valid X.509 certificates to prove their identity.&lt;/p&gt;
&lt;h3 id=&quot;what-to-configure&quot;&gt;What to Configure&lt;/h3&gt;
&lt;h4 id=&quot;server-side-requirements&quot;&gt;Server-Side Requirements&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SSL certificates&lt;/strong&gt;: Server certificate + client certificates&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PostgreSQL SSL settings&lt;/strong&gt;: Enable SSL and configure certificate paths&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authentication rules&lt;/strong&gt;: Configure &lt;code&gt;pg_hba.conf&lt;/code&gt; for certificate-based auth&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User mapping&lt;/strong&gt;: Link certificate Common Names to database users&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;client-side-requirements&quot;&gt;Client-Side Requirements&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Client certificate&lt;/strong&gt;: Valid X.509 certificate for the user&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Private key&lt;/strong&gt;: Matching private key for the certificate&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Root certificate&lt;/strong&gt;: Server's certificate for verification&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connection parameters&lt;/strong&gt;: Proper SSL mode and certificate paths&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;how-to-configure&quot;&gt;How to Configure&lt;/h3&gt;
&lt;h4 id=&quot;1-generate-ssl-certificates&quot;&gt;1. Generate SSL Certificates&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Create SSL directory
mkdir -p /var/lib/postgresql/17/main/ssl
cd /var/lib/postgresql/17/main/ssl

# Generate server certificate
openssl genrsa -out server.key 4096
openssl req -new -key server.key -out server.csr \
  -subj &quot;/C=US/ST=State/L=City/O=Org/OU=OrgUnit/CN=localhost&quot;
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

# Generate client certificate for user 'appuser'
openssl genrsa -out appuser.key 4096
openssl req -new -key appuser.key -out appuser.csr \
  -subj &quot;/C=US/ST=State/L=City/O=Org/OU=OrgUnit/CN=appuser&quot;
openssl x509 -req -days 365 -in appuser.csr \
  -CA server.crt -CAkey server.key -CAcreateserial -out appuser.crt

# Set permissions
chown postgres:postgres *.crt *.key
chmod 600 *.key
chmod 644 *.crt
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;2-configure-postgresql-ssl-settings&quot;&gt;2. Configure PostgreSQL SSL Settings&lt;/h4&gt;
&lt;p&gt;Add to &lt;code&gt;postgresql.conf&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;# SSL Configuration
ssl = on
ssl_cert_file = '/var/lib/postgresql/17/main/ssl/server.crt'
ssl_key_file = '/var/lib/postgresql/17/main/ssl/server.key'
ssl_ca_file = '/var/lib/postgresql/17/main/ssl/server.crt'
ssl_prefer_server_ciphers = on
ssl_min_protocol_version = 'TLSv1.2'
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;3-configure-authentication-rules&quot;&gt;3. Configure Authentication Rules&lt;/h4&gt;
&lt;p&gt;Update &lt;code&gt;pg_hba.conf&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# SSL connections require client certificate authentication
hostssl all             all             0.0.0.0/0               cert clientcert=verify-full
hostssl all             all             ::/0                    cert clientcert=verify-full

# Non-SSL connections require password authentication
hostnossl all           all             0.0.0.0/0               scram-sha-256
hostnossl all           all             ::/0                    scram-sha-256
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;4-create-database-user&quot;&gt;4. Create Database User&lt;/h4&gt;
&lt;p&gt;The database user must match the certificate's Common Name(&lt;strong&gt;CN&lt;/strong&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE USER appuser WITH LOGIN;
GRANT CONNECT ON DATABASE mydb TO appuser;
-- Grant additional permissions as needed
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;how-to-connect-from-client&quot;&gt;How to Connect from Client&lt;/h3&gt;
&lt;h4 id=&quot;critical-connection-parameters&quot;&gt;Critical Connection Parameters&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Parameter     &lt;/th&gt;&lt;th&gt;Self-Managed Value            &lt;/th&gt;&lt;th&gt;RDS Value                           &lt;/th&gt;&lt;th&gt;Purpose&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;sslmode&lt;/code&gt;     &lt;/td&gt;&lt;td&gt;&lt;code&gt;require&lt;/code&gt; or &lt;code&gt;verify-full&lt;/code&gt;    &lt;/td&gt;&lt;td&gt;&lt;code&gt;require&lt;/code&gt; or &lt;code&gt;verify-full&lt;/code&gt;          &lt;/td&gt;&lt;td&gt;Force SSL connection&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;sslcert&lt;/code&gt;     &lt;/td&gt;&lt;td&gt;&lt;code&gt;/path/to/client.crt&lt;/code&gt;         &lt;/td&gt;&lt;td&gt;N/A (not supported)                 &lt;/td&gt;&lt;td&gt;Client certificate file&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;sslkey&lt;/code&gt;      &lt;/td&gt;&lt;td&gt;&lt;code&gt;/path/to/client.key&lt;/code&gt;         &lt;/td&gt;&lt;td&gt;N/A (not supported)                 &lt;/td&gt;&lt;td&gt;Client private key file&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;sslrootcert&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;/path/to/server.crt&lt;/code&gt;         &lt;/td&gt;&lt;td&gt;&lt;code&gt;./global-bundle.pem&lt;/code&gt;               &lt;/td&gt;&lt;td&gt;Server certificate for verification&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;host&lt;/code&gt;        &lt;/td&gt;&lt;td&gt;&lt;code&gt;localhost&lt;/code&gt; or custom         &lt;/td&gt;&lt;td&gt;&lt;code&gt;instance.region.rds.amazonaws.com&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Database server endpoint&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;password&lt;/code&gt;    &lt;/td&gt;&lt;td&gt;Not required with client cert &lt;/td&gt;&lt;td&gt;Required (or IAM token)             &lt;/td&gt;&lt;td&gt;Authentication credential&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;connection-examples&quot;&gt;Connection Examples&lt;/h4&gt;
&lt;h5 id=&quot;self-managed-postgresql-with-client-certificates&quot;&gt;Self-Managed PostgreSQL with Client Certificates&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# psql - No password required
psql &quot;host=localhost port=5432 dbname=mydb user=appuser \
      sslmode=require \
      sslcert=./certs/appuser.crt \
      sslkey=./certs/appuser.key \
      sslrootcert=./certs/server.crt&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// .NET - Client certificate authentication
var connectionString = &quot;Host=localhost;Port=5432;Database=mydb;Username=appuser;&quot; +
                      &quot;SSL Mode=Require;&quot; +
                      &quot;Client Certificate=./certs/appuser.crt;&quot; +
                      &quot;Client Key=./certs/appuser.key;&quot; +
                      &quot;Root Certificate=./certs/server.crt;&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Python - Client certificate authentication
import psycopg2
conn = psycopg2.connect(
    host=&quot;localhost&quot;,
    port=5432,
    database=&quot;mydb&quot;,
    user=&quot;appuser&quot;,
    sslmode=&quot;require&quot;,
    sslcert=&quot;./certs/appuser.crt&quot;,
    sslkey=&quot;./certs/appuser.key&quot;,
    sslrootcert=&quot;./certs/server.crt&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// Java JDBC - Client certificate authentication
String url = &quot;jdbc:postgresql://localhost:5432/mydb&quot; +
            &quot;?ssl=true&quot; +
            &quot;&amp;amp;sslmode=require&quot; +
            &quot;&amp;amp;sslcert=./certs/appuser.crt&quot; +
            &quot;&amp;amp;sslkey=./certs/appuser.key&quot; +
            &quot;&amp;amp;sslrootcert=./certs/server.crt&quot; +
            &quot;&amp;amp;user=appuser&quot;;

Connection conn = DriverManager.getConnection(url);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;aws-rds-postgresql-with-ssl-password-required-&quot;&gt;AWS RDS PostgreSQL with SSL (Password Required)&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# psql - Password required, verify server certificate
psql &quot;host=mydb.cluster-xyz.us-east-1.rds.amazonaws.com \
      port=5432 dbname=postgres user=myuser password=mypassword \
      sslmode=verify-full sslrootcert=./global-bundle.pem&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// .NET - RDS with SSL verification
var connectionString = &quot;Host=mydb.cluster-xyz.us-east-1.rds.amazonaws.com;&quot; +
                      &quot;Port=5432;Database=postgres;Username=myuser;Password=mypassword;&quot; +
                      &quot;SSL Mode=Require;Root Certificate=./global-bundle.pem;&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Python - RDS with SSL verification  
import psycopg2
conn = psycopg2.connect(
    host=&quot;mydb.cluster-xyz.us-east-1.rds.amazonaws.com&quot;,
    port=5432,
    database=&quot;postgres&quot;,
    user=&quot;myuser&quot;,
    password=&quot;mypassword&quot;,
    sslmode=&quot;verify-full&quot;,
    sslrootcert=&quot;./global-bundle.pem&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// Java JDBC - RDS with SSL verification
String url = &quot;jdbc:postgresql://mydb.cluster-xyz.us-east-1.rds.amazonaws.com:5432/postgres&quot; +
            &quot;?ssl=true&quot; +
            &quot;&amp;amp;sslmode=verify-full&quot; +
            &quot;&amp;amp;sslrootcert=./global-bundle.pem&quot; +
            &quot;&amp;amp;user=myuser&quot; +
            &quot;&amp;amp;password=mypassword&quot;;

Connection conn = DriverManager.getConnection(url);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;server-configuration-vs-client-parameters&quot;&gt;Server Configuration vs Client Parameters&lt;/h3&gt;
&lt;h4 id=&quot;server-side-pg-hba-conf-&quot;&gt;Server Side (&lt;code&gt;pg_hba.conf&lt;/code&gt;)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Authentication method MUST be 'cert'
hostssl all all 0.0.0.0/0 cert clientcert=verify-full
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;client-side-connection-string-&quot;&gt;Client Side (Connection String)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# SSL mode MUST be 'require' or 'verify-full'
sslmode=require
# All three certificate files MUST be provided
sslcert=./client.crt
sslkey=./client.key  
sslrootcert=./server.crt
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;common-pitfalls&quot;&gt;Common Pitfalls&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Certificate CN mismatch&lt;/strong&gt;: Client certificate CN must match database username&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Missing clientcert=verify-full&lt;/strong&gt;: Server won't require client certificates&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wrong SSL mode&lt;/strong&gt;: &lt;code&gt;sslmode=disable&lt;/code&gt; or &lt;code&gt;prefer&lt;/code&gt; may skip certificate auth&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;File permissions&lt;/strong&gt;: Private keys must be readable only by the client process&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Certificate expiration&lt;/strong&gt;: Monitor and rotate certificates before expiry&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;security-benefits&quot;&gt;Security Benefits&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No password transmission&lt;/strong&gt;: Eliminates password-based attacks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Strong authentication&lt;/strong&gt;: Based on cryptographic proof of identity&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Non-repudiation&lt;/strong&gt;: Certificate-based audit trails&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mutual authentication&lt;/strong&gt;: Both client and server verify each other&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;aws-rds-postgresql-configuration&quot;&gt;AWS RDS PostgreSQL Configuration&lt;/h3&gt;
&lt;h4 id=&quot;rds-limitations-for-client-certificates&quot;&gt;RDS Limitations for Client Certificates&lt;/h4&gt;
&lt;p&gt;AWS RDS PostgreSQL has &lt;strong&gt;limited support&lt;/strong&gt; for client certificate authentication compared to self-managed PostgreSQL:&lt;/p&gt;
&lt;h5 id=&quot;what-s-not-available-in-rds&quot;&gt;What's NOT Available in RDS&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Custom client certificate generation&lt;/strong&gt;: You cannot upload custom client certificates&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full &lt;code&gt;cert&lt;/code&gt; authentication method&lt;/strong&gt;: RDS doesn't support the &lt;code&gt;cert&lt;/code&gt; method in &lt;code&gt;pg_hba.conf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Direct certificate file access&lt;/strong&gt;: No access to &lt;code&gt;/var/lib/postgresql/&lt;/code&gt; directory&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;what-is-available-in-rds&quot;&gt;What IS Available in RDS&lt;/h5&gt;
&lt;h4 id=&quot;1-rds-ssl-server-certificates-amp-configuration&quot;&gt;1. RDS SSL Server Certificates &amp;amp; Configuration&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Download RDS CA certificate bundle
wget https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem

# Enable SSL in RDS parameter group
aws rds modify-db-parameter-group \
    --db-parameter-group-name mygroup \
    --parameters &quot;ParameterName=ssl,ParameterValue=1,ApplyMethod=immediate&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;2-alternative-iam-database-authentication&quot;&gt;2. Alternative: IAM Database Authentication&lt;/h4&gt;
&lt;p&gt;For passwordless authentication in RDS, use IAM database authentication:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Create IAM-enabled database user
CREATE USER iamuser;
GRANT rds_iam TO iamuser;
GRANT CONNECT ON DATABASE postgres TO iamuser;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Connect using IAM token (Python example)
import boto3
import psycopg2

rds_client = boto3.client('rds', region_name='us-east-1')
token = rds_client.generate_db_auth_token(
    DBHostname='mydb.xyz.us-east-1.rds.amazonaws.com',
    Port=5432,
    DBUsername='iamuser'
)

conn = psycopg2.connect(
    host=&quot;mydb.xyz.us-east-1.rds.amazonaws.com&quot;,
    port=5432,
    database=&quot;postgres&quot;,
    user=&quot;iamuser&quot;,
    password=token,  # IAM token as password
    sslmode=&quot;require&quot;,
    sslrootcert=&quot;./global-bundle.pem&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;3-hybrid-approach-alb-client-certificates-rds&quot;&gt;3. Hybrid Approach: ALB + Client Certificates + RDS&lt;/h4&gt;
&lt;p&gt;For applications requiring client certificate authentication with RDS:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[Client with Client Cert] → [ALB with SSL termination] → [App Server] → [RDS with IAM Auth]
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;ALB handles client certificate verification&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Application validates certificates in code&lt;/strong&gt;  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RDS connection uses IAM authentication&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;summary-amp-verification&quot;&gt;Summary &amp;amp; Verification&lt;/h3&gt;
&lt;h4 id=&quot;self-managed-vs-aws-rds-comparison&quot;&gt;Self-Managed vs AWS RDS Comparison&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Feature                     &lt;/th&gt;&lt;th&gt;Self-Managed          &lt;/th&gt;&lt;th&gt;AWS RDS&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Client Certificate Auth     &lt;/td&gt;&lt;td&gt;✅ Full support        &lt;/td&gt;&lt;td&gt;❌ Not supported&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Server SSL Certificates     &lt;/td&gt;&lt;td&gt;✅ Custom certificates &lt;/td&gt;&lt;td&gt;✅ AWS-managed certificates&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Passwordless Authentication &lt;/td&gt;&lt;td&gt;✅ Client certificates &lt;/td&gt;&lt;td&gt;✅ IAM authentication&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Certificate Management      &lt;/td&gt;&lt;td&gt;❌ Manual rotation     &lt;/td&gt;&lt;td&gt;✅ Automatic rotation&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;SSL Configuration Control   &lt;/td&gt;&lt;td&gt;✅ Full control        &lt;/td&gt;&lt;td&gt;❌ Limited parameters&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;verification-commands&quot;&gt;Verification Commands&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Verify SSL is enabled
SHOW ssl;

-- Check active SSL connections with certificate details
SELECT datname, usename, client_addr, ssl, ssl_version, ssl_cipher,
       client_dn, issuer_dn
FROM pg_stat_ssl 
JOIN pg_stat_activity ON pg_stat_ssl.pid = pg_stat_activity.pid 
WHERE ssl = true;
-- Example output
pid|ssl |version|cipher                |bits|client_dn                                                      |client_serial                                   |issuer_dn                                                   |datid|datname       |pid|leader_pid|usesysid|usename       |application_name|client_addr|client_hostname|client_port|backend_start                |xact_start|query_start                  |state_change                 |wait_event_type|wait_event|state|backend_xid|backend_xmin|query_id           |query                                                                                                                        |backend_type  |
---+----+-------+----------------------+----+---------------------------------------------------------------+------------------------------------------------+------------------------------------------------------------+-----+--------------+---+----------+--------+--------------+----------------+-----------+---------------+-----------+-----------------------------+----------+-----------------------------+-----------------------------+---------------+----------+-----+-----------+------------+-------------------+-----------------------------------------------------------------------------------------------------------------------------+--------------+
754|true|TLSv1.3|TLS_AES_256_GCM_SHA384| 256|/C=US/ST=State/L=City/O=Organization/OU=OrgUnit/CN=redgatemonit|608692775915102986323080169170088721995325147053|/C=US/ST=State/L=City/O=Organization/OU=OrgUnit/CN=localhost|16384|redgatemonitor|754|          |   16385|redgatemonitor|psql            |10.88.0.1  |               |      59168|2025-07-30 09:43:20.240 +0100|          |2025-07-30 09:43:58.869 +0100|2025-07-30 09:43:58.870 +0100|Client         |ClientRead|idle |           |            |8885888923103386772|SELECT datname, usename, client_addr, ssl,* FROM pg_stat_ssl JOIN pg_stat_activity ON pg_stat_ssl.pid = pg_stat_activity.pid;|client backend|
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;key-takeaways&quot;&gt;Key Takeaways&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Self-managed PostgreSQL&lt;/strong&gt;: Use client certificates for true passwordless authentication&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AWS RDS&lt;/strong&gt;: Combine IAM authentication with SSL encryption for secure connections&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hybrid approach&lt;/strong&gt;: Use ALB for client certificate verification with RDS backend&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security&lt;/strong&gt;: Both approaches provide enterprise-grade security with different trade-offs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Client certificate authentication in PostgreSQL provides enterprise-grade security without the overhead of password management, making it ideal for automated systems and high-security environments.&lt;/p&gt;
&lt;h4 id=&quot;references&quot;&gt;References&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.postgresql.org/docs/current/ssl-tcp.html&quot;&gt;https://www.postgresql.org/docs/current/ssl-tcp.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/4373/postgresql-client-certificate-authentication-complete-setup-guide</guid>
<pubDate>Fri, 01 Aug 2025 12:50:19 +0000</pubDate>
</item>
<item>
<title>DevLog 20250523: Firebase Auth</title>
<link>https://coderlegion.com/3139/devlog-20250523-firebase-auth</link>
<description>&lt;h3 id=&quot;the-authentication-problem&quot;&gt;The Authentication Problem&lt;/h3&gt;
&lt;p&gt;Let's face it: develop a custom authentication service is conceptually straightforward but practically troublesome. You either allow user registration or not, then authenticate by taking a look at username and password (likely hashed) against database, then provide user profile results for authorized users. Throughout the process you might set browser cache, deal with (generated) API keys for app usages (so password need not be sent every time). It's a handful of steps, and lots of work to code everything piece by piece. Worse, there is a whole database to worry about.  One would naturally expect, such a thing should just exist as a service. Yes, there is.&lt;/p&gt;
&lt;h3 id=&quot;quick-comparison&quot;&gt;Quick Comparison&lt;/h3&gt;
&lt;p&gt;|Platform|What it is|Free tier|Pros|Cons|&lt;br&gt;
|-|-|-|-|-|&lt;br&gt;
|&lt;strong&gt;AWS Cognito&lt;/strong&gt; | Amazon’s identity and user‑management service for web and mobile apps, integrating natively with API Gateway/Lambda and Amplify JS. | 10,000 MAUs for direct/social sign‑in + 50 MAUs for SAML/OIDC federation; Identity Pools are free | No extra vendor to learn; pay only for MAUs above free tier; built‑in social and SAML/OIDC support; AWS‑managed hosted UI. | Learning curve around User Pools vs Identity Pools and IAM policies.|&lt;br&gt;
|&lt;strong&gt;Auth0&lt;/strong&gt; | Popular identity platform with SDKs for nearly every framework. | 25,000 MAUs; unlimited social &amp;amp; Okta connections; 5 organizations; 5 actions/forms; passwordless &amp;amp; custom domain  | Intuitive, developer‑friendly dashboard; extensible via Rules (JavaScript); excellent documentation. | Pricing increases sharply above free tier.|&lt;br&gt;
|&lt;strong&gt;Firebase Authentication&lt;/strong&gt; | Google’s auth service, part of Firebase/GCP. | Unlimited email/password, anonymous, and most social sign‑ins; pay only for phone‑SMS verification | Fastest setup for web/mobile; pre‑built UI widgets; seamless integration with other Firebase services. | SMS‑based authentication incurs per‑message cost.|&lt;br&gt;
|&lt;strong&gt;Supabase Auth&lt;/strong&gt; | Open‑source Firebase alternative built on PostgreSQL. | Unlimited API requests; 50,000 MAUs; 500 MB database; 5 GB bandwidth; 1 GB file storage; community support | True SQL under the hood; you own your data; robust JS client. | Free projects pause after 1 week of inactivity; limited to 2 active projects.|&lt;br&gt;
|&lt;strong&gt;Netlify Identity&lt;/strong&gt; | Netlify’s built‑in auth service powered by GoTrue. | Up to 1,000 registered users per site | Seamless drop‑in for Netlify‑hosted. | Limited to Netlify sites; basic compared to full‑blown IDaaS; deprecated as of Feb 28, 2025 .|&lt;br&gt;
|&lt;strong&gt;Okta Developer Edition&lt;/strong&gt;  | Enterprise‑grade identity platform in a developer sandbox. | 100 MAUs | Enterprise features: OpenID Connect federation, SCIM provisioning, lifecycle hooks, universal directory | Console can feel heavy for hobby projects.|&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Recommendations:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Full AWS&lt;/strong&gt;: Use &lt;strong&gt;AWS Cognito&lt;/strong&gt; (with Amplify libraries) to stay within the AWS ecosystem and enjoy a free tier up to 10 K direct/social MAUs and 50 MAUs for SAML/OIDC. &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rapid prototyping&lt;/strong&gt;: &lt;strong&gt;Firebase Authentication&lt;/strong&gt; for the fastest setup (unlimited email/password/social; pay only for SMS). &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SQL‑power &amp;amp; control&lt;/strong&gt;: &lt;strong&gt;Supabase Auth&lt;/strong&gt; if you need open‑source Postgres‑based auth with a 50 K MAU free tier and 500 MB of built‑in storage. &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For reasons I would not disclose, we picked Firebase for Divooka and other Methodox services.&lt;/p&gt;
&lt;h3 id=&quot;the-setup&quot;&gt;The Setup&lt;/h3&gt;
&lt;p&gt;For Firebase Auth, the &quot;API Key&quot; just identifies the application and can totally be shown on the client side script.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
  &amp;lt;meta charset=&quot;utf-8&quot;/&amp;gt;
  &amp;lt;title&amp;gt;Custom Email/Password Auth&amp;lt;/title&amp;gt;
  &amp;lt;script type=&quot;module&quot;&amp;gt;
    import { initializeApp } from &quot;https://www.gstatic.com/firebasejs/11.8.1/firebase-app.js&quot;;
    import {
      getAuth,
      createUserWithEmailAndPassword,
      updateProfile,
      signInWithEmailAndPassword,
      signOut
    } from &quot;https://www.gstatic.com/firebasejs/11.8.1/firebase-auth.js&quot;;

    // Firebase web config
    // For Firebase JS SDK v7.20.0 and later, measurementId is optional
    const firebaseConfig = {
        apiKey: &quot;*********************&quot;,
        authDomain: &quot;*********************&quot;,
        projectId: &quot;*********************&quot;,
        storageBucket: &quot;*********************&quot;,
        messagingSenderId: &quot;*********************&quot;,
        appId: &quot;*********************&quot;,
        measurementId: &quot;*********************&quot;
    };
    // Initialize Firebase
    const app = initializeApp(firebaseConfig);
    const auth = getAuth(app);

    // Register a new user
    async function registerUser() {
      const email = document.getElementById(&quot;regEmail&quot;).value;
      const password = document.getElementById(&quot;regPass&quot;).value;
      const username = document.getElementById(&quot;regName&quot;).value;
      try {
        const cred = await createUserWithEmailAndPassword(auth, email, password);
        // Set a displayName on the Firebase user record
        await updateProfile(cred.user, { displayName: username });
        alert(&quot;Registration successful! You can now log in.&quot;);
      } catch (e) {
        console.error(e);
        alert(&quot;Registration error: &quot; + e.message);
      }
    }

    // Login existing user
    async function loginUser() {
      const email = document.getElementById(&quot;loginEmail&quot;).value;
      const password = document.getElementById(&quot;loginPass&quot;).value;
      try {
        const cred = await signInWithEmailAndPassword(auth, email, password);
        const idToken = await cred.user.getIdToken();

        // Call your API
        const resp = await fetch(&quot;https://myapi.com/profile&quot;, {
          headers: { &quot;Authorization&quot;: &quot;Bearer &quot; + idToken }
        });
        if (!resp.ok) throw new Error(await resp.text());
        const profile = await resp.json();
        document.getElementById(&quot;output&quot;).textContent = JSON.stringify(profile, null, 2);
      } catch (e) {
        console.error(e);
        alert(&quot;Login error: &quot; + e.message);
      }
    }

    // Logout
    function logoutUser() {
      signOut(auth);
      document.getElementById(&quot;output&quot;).textContent = &quot;&quot;;
      alert(&quot;Signed out&quot;);
    }

    window.onload = () =&amp;gt; {
      document.getElementById(&quot;btnRegister&quot;).onclick = registerUser;
      document.getElementById(&quot;btnLogin&quot;).onclick    = loginUser;
      document.getElementById(&quot;btnLogout&quot;).onclick   = logoutUser;
    };
  &amp;lt;/script&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
  &amp;lt;h2&amp;gt;Register&amp;lt;/h2&amp;gt;
  &amp;lt;input id=&quot;regName&quot;  type=&quot;text&quot;     placeholder=&quot;Username&quot;/&amp;gt;&amp;lt;br/&amp;gt;
  &amp;lt;input id=&quot;regEmail&quot; type=&quot;email&quot;    placeholder=&quot;Email&quot;/&amp;gt;&amp;lt;br/&amp;gt;
  &amp;lt;input id=&quot;regPass&quot;  type=&quot;password&quot; placeholder=&quot;Password&quot;/&amp;gt;&amp;lt;br/&amp;gt;
  &amp;lt;button id=&quot;btnRegister&quot;&amp;gt;Register&amp;lt;/button&amp;gt;

  &amp;lt;hr/&amp;gt;

  &amp;lt;h2&amp;gt;Login&amp;lt;/h2&amp;gt;
  &amp;lt;input id=&quot;loginEmail&quot; type=&quot;email&quot;    placeholder=&quot;Email&quot;/&amp;gt;&amp;lt;br/&amp;gt;
  &amp;lt;input id=&quot;loginPass&quot;  type=&quot;password&quot; placeholder=&quot;Password&quot;/&amp;gt;&amp;lt;br/&amp;gt;
  &amp;lt;button id=&quot;btnLogin&quot;&amp;gt;Log In&amp;lt;/button&amp;gt;
  &amp;lt;button id=&quot;btnLogout&quot;&amp;gt;Log Out&amp;lt;/button&amp;gt;

  &amp;lt;h3&amp;gt;Profile:&amp;lt;/h3&amp;gt;
  &amp;lt;pre id=&quot;output&quot;&amp;gt;&amp;lt;/pre&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;create-api-key&quot;&gt;Create API Key&lt;/h3&gt;
&lt;p&gt;To create API key, navigate to the project then find &quot;Add App&quot; in Project Settings, which is a &lt;strong&gt;hidden menu&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3xk2syz5jlz1g043ama1.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qqu308x2hp0ljkq85fzy.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v0tpxprzuqyxiwkmt53z.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;references-1&quot;&gt;References&lt;/h3&gt;
&lt;p&gt;Official doc on API key, which is not very clear without pictures: &lt;a rel=&quot;nofollow&quot; href=&quot;https://firebase.google.com/docs/projects/api-keys&quot;&gt;here&lt;/a&gt;&lt;/p&gt;
</description>
<category>Web Dev</category>
<guid isPermaLink="true">https://coderlegion.com/3139/devlog-20250523-firebase-auth</guid>
<pubDate>Fri, 23 May 2025 19:06:57 +0000</pubDate>
</item>
<item>
<title>A quick guide to adding wallets like Phantom and Solflare to your Solana DApp.</title>
<link>https://coderlegion.com/1841/a-quick-guide-to-adding-wallets-like-phantom-and-solflare-to-your-solana-dapp</link>
<description>&lt;h3 id=&quot;integrating-wallets-into-your-solana-dapp&quot;&gt;Integrating Wallets into Your Solana DApp&lt;/h3&gt;
&lt;h4 id=&quot;why-wallet-integration-is-essential-in-solana-dapps&quot;&gt;&lt;strong&gt;Why Wallet Integration Is Essential in Solana DApps&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;In the Solana ecosystem, &lt;strong&gt;wallets serve as a user’s digital identity&lt;/strong&gt;. Unlike traditional apps that rely on usernames and passwords, decentralized applications use wallets for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Authentication&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Signing and submitting transactions&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Viewing balances and token ownership&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interacting with smart contracts&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without wallet integration, your DApp will be read-only — users won’t be able to do anything meaningful like stake, mint, or transfer tokens.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;prerequisites-and-tools-needed&quot;&gt;&lt;strong&gt;Prerequisites and Tools Needed&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Before integrating wallets, ensure your environment includes the following:&lt;/p&gt;
&lt;h4 id=&quot;development-setup&quot;&gt;&lt;strong&gt;Development Setup&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;React (or any JavaScript frontend framework)&lt;/li&gt;
&lt;li&gt;Node.js and npm&lt;/li&gt;
&lt;li&gt;Basic knowledge of Solana’s architecture&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;required-libraries&quot;&gt;&lt;strong&gt;Required Libraries&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;```bash
npm install @solana/web3.js
npm install @solana/wallet-adapter-react \
            @solana/wallet-adapter-wallets \
            @solana/wallet-adapter-react-ui
```
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt; &lt;em&gt;These libraries allow you to connect to Solana nodes and interact with user wallets.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;understanding-wallet-adapter-and-why-it-matters&quot;&gt;&lt;strong&gt;Understanding Wallet Adapter and Why It Matters&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Solana’s &lt;strong&gt;Wallet Adapter&lt;/strong&gt; provides a standardized, plug-and-play interface to support **multiple &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wallets** like:
- **Phantom**
- **Solflare**
- **Backpack**
- **Glow**
- **Ledger (hardware)**
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It handles the complexities of detecting installed wallets, establishing connections, signing transactions, and disconnecting sessions.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-step-by-step-guide-to-integrate-phantom-wallet&quot;&gt;️ &lt;strong&gt;Step-by-Step Guide to Integrate Phantom Wallet&lt;/strong&gt;&lt;/h3&gt;
&lt;h4 id=&quot;-step-1-bootstrap-your-react-project&quot;&gt;⚙️ Step 1: &lt;strong&gt;Bootstrap Your React Project&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;```bash
npx create-react-app solana-wallet-dapp
cd solana-wallet-dapp
```

Install necessary packages:
```bash
npm install @solana/web3.js \
            @solana/wallet-adapter-react \
            @solana/wallet-adapter-wallets \
            @solana/wallet-adapter-react-ui
```
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;-step-2-create-a-wallet-context-provider&quot;&gt;⚙️ Step 2: &lt;strong&gt;Create a Wallet Context Provider&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;```jsx
// WalletContextProvider.js
import {
  ConnectionProvider,
  WalletProvider
} from '@solana/wallet-adapter-react';

import {
  PhantomWalletAdapter,
  SolflareWalletAdapter,
} from '@solana/wallet-adapter-wallets';

import { WalletModalProvider } from '@solana/wallet-adapter-react-ui';
import { clusterApiUrl } from '@solana/web3.js';
import React, { useMemo } from 'react';

require('@solana/wallet-adapter-react-ui/styles.css');

export const WalletContextProvider = ({ children }) =&amp;gt; {
  const network = 'devnet'; // Change to 'mainnet-beta' for production
  const endpoint = useMemo(() =&amp;gt; clusterApiUrl(network), [network]);

  const wallets = useMemo(() =&amp;gt; [
    new PhantomWalletAdapter(),
    new SolflareWalletAdapter(),
  ], []);

  return (
    &amp;lt;ConnectionProvider endpoint={endpoint}&amp;gt;
      &amp;lt;WalletProvider wallets={wallets} autoConnect&amp;gt;
        &amp;lt;WalletModalProvider&amp;gt;{children}&amp;lt;/WalletModalProvider&amp;gt;
      &amp;lt;/WalletProvider&amp;gt;
    &amp;lt;/ConnectionProvider&amp;gt;
  );
};
```
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;-step-3-wrap-your-app-with-wallet-provider&quot;&gt;⚙️ Step 3: &lt;strong&gt;Wrap Your App with Wallet Provider&lt;/strong&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;In `index.js`, wrap your `&amp;lt;App /&amp;gt;` component:

```jsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { WalletContextProvider } from './WalletContextProvider';

ReactDOM.render(
  &amp;lt;React.StrictMode&amp;gt;
    &amp;lt;WalletContextProvider&amp;gt;
      &amp;lt;App /&amp;gt;
    &amp;lt;/WalletContextProvider&amp;gt;
  &amp;lt;/React.StrictMode&amp;gt;,
  document.getElementById('root')
);
```
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;-step-4-display-connect-wallet-button&quot;&gt;⚙️ Step 4: &lt;strong&gt;Display Connect Wallet Button&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Use the built-in WalletMultiButton:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```jsx
// App.js
import { WalletMultiButton } from '@solana/wallet-adapter-react-ui';

function App() {
  return (
    &amp;lt;div style={{ padding: '2rem' }}&amp;gt;
      &amp;lt;h1&amp;gt; Solana Wallet DApp&amp;lt;/h1&amp;gt;
      &amp;lt;WalletMultiButton /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export default App;
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This button dynamically shows installed wallets like Phantom and handles connection/disconnection automatically.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;bonus-send-sol-from-connected-wallet&quot;&gt;&lt;strong&gt;Bonus: Send SOL From Connected Wallet&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Add a component to send SOL from the connected wallet to another address:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```jsx
import {
  useWallet,
  useConnection
} from '@solana/wallet-adapter-react';
import {
  LAMPORTS_PER_SOL,
  Transaction,
  SystemProgram,
  PublicKey
} from '@solana/web3.js';

const SendSol = () =&amp;gt; {
  const { connection } = useConnection();
  const { publicKey, sendTransaction } = useWallet();

  const handleSend = async () =&amp;gt; {
    if (!publicKey) return alert(&quot;Connect your wallet first!&quot;);

    const transaction = new Transaction().add(
      SystemProgram.transfer({
        fromPubkey: publicKey,
        toPubkey: new PublicKey(&quot;DestinationPublicKeyHere&quot;),
        lamports: 0.01 * LAMPORTS_PER_SOL,
      })
    );

    const signature = await sendTransaction(transaction, connection);
    await connection.confirmTransaction(signature, 'processed');

    alert(&quot;Transaction successful: &quot; + signature);
  };

  return &amp;lt;button onClick={handleSend}&amp;gt;Send 0.01 SOL&amp;lt;/button&amp;gt;;
};
```
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;other-wallets-you-can-integrate&quot;&gt;&lt;strong&gt;Other Wallets You Can Integrate&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Solana supports a wide ecosystem of wallets. With Wallet Adapter, you can plug in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Glow&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Solong&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Torus&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ledger hardware wallets&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backpack (XNFTs)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt; &lt;em&gt;Each wallet has different capabilities — such as hardware security or mobile support.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;official-resources&quot;&gt;Official Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.solana.com/&quot;&gt;Solana Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.phantom.app/&quot;&gt;Phantom Developer Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/solana-labs/wallet-adapter&quot;&gt;Solana Wallet Adapter GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-best-practices&quot;&gt;✅ Best Practices&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Detect wallet availability&lt;/strong&gt; before rendering connect buttons.&lt;/li&gt;
&lt;li&gt;Show user feedback on &lt;strong&gt;network connection&lt;/strong&gt; (e.g., devnet vs mainnet).&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;tooltips or modals&lt;/strong&gt; to explain wallet permissions.&lt;/li&gt;
&lt;li&gt;Always check for &lt;code&gt;publicKey&lt;/code&gt; before sending transactions.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;conclusion-5&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Integrating wallets into your Solana DApp is a &lt;strong&gt;must-have feature&lt;/strong&gt; for real blockchain interactions. With libraries like &lt;code&gt;wallet-adapter&lt;/code&gt; and &lt;code&gt;web3.js&lt;/code&gt;, you can easily implement secure, scalable wallet connectivity.&lt;/p&gt;
&lt;p&gt;By empowering users to authenticate and transact through their wallets, you create a truly &lt;strong&gt;decentralized, user-driven experience&lt;/strong&gt; — the essence of Web3.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;✨ &lt;em&gt;The more seamless the wallet connection, the higher the user trust and engagement in your DApp.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/1841/a-quick-guide-to-adding-wallets-like-phantom-and-solflare-to-your-solana-dapp</guid>
<pubDate>Sat, 12 Apr 2025 21:13:14 +0000</pubDate>
</item>
<item>
<title>A Beginner's Guide to Authentication: Best Practices and Key Concepts</title>
<link>https://coderlegion.com/1322/a-beginners-guide-to-authentication-best-practices-and-key-concepts</link>
<description>&lt;p&gt;Have you ever wondered how websites or apps know that it's really you logging in? Or how they keep your data secure when you sign in? Authentication is the process that makes this possible, ensuring that users are who they claim to be. In today's digital world, understanding authentication is crucial for developers and users alike. This guide will take you through the basics of authentication, its types, and how it plays a key role in securing web applications.&lt;/p&gt;
&lt;h4 id=&quot;what-is-authentication-&quot;&gt;What is Authentication?&lt;/h4&gt;
&lt;p&gt;Authentication is the process of verifying the identity of a user, device, or system. It's a fundamental aspect of security in computing, ensuring that only authorized users can access specific resources. Without proper authentication, websites and applications would be vulnerable to unauthorized access and data breaches.&lt;/p&gt;
&lt;h4 id=&quot;why-is-authentication-important-&quot;&gt;Why is Authentication Important?&lt;/h4&gt;
&lt;p&gt;Authentication is important because it acts as the first line of defense against cyberattacks. If a user's identity is not verified correctly, sensitive information or actions could be accessed or manipulated by malicious actors. It also helps maintain privacy and control over personal data, which is crucial in today's online environment.&lt;/p&gt;
&lt;h3 id=&quot;types-of-authentication&quot;&gt;Types of Authentication&lt;/h3&gt;
&lt;p&gt;Authentication can be classified into different types, based on how the user proves their identity.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Password-based Authentication&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The most common form of authentication, where users log in using a username and a password.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;While convenient, passwords can be vulnerable to theft if not handled securely.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;Two-Factor Authentication (2FA)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;This requires two forms of identification: something you know (e.g., a password) and something you have (e.g., a phone or hardware token).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It adds an extra layer of security by requiring a second form of validation.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;Biometric Authentication&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Uses physical characteristics like fingerprints, facial recognition, or retina scans.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Often used in mobile devices, biometrics are more difficult to replicate, making them highly secure.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;OAuth Authentication&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Used when logging in through third-party services like Google or Facebook.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Instead of using a password, you grant the application access through the third-party service's authentication system.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;Single Sign-On (SSO)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A method where users log in once to gain access to multiple applications.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Great for improving user experience while maintaining security.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=962630755282425705&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4 id=&quot;authentication-factors-and-technologies&quot;&gt;Authentication Factors and Technologies&lt;/h4&gt;
&lt;p&gt;Authentication is built on different factors, which are categorized as follows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Something You Know:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Passwords, PINs, or passphrases.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;Something You Have:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;A mobile device (for SMS or app-based codes), smartcards, or hardware tokens.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;Something You Are:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Biometric data such as fingerprints or voice recognition.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;technologies-in-authentication&quot;&gt;Technologies in Authentication&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Password Hashing and Salting: Protects stored passwords by converting them into an unreadable format.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;JWT (JSON Web Tokens): A compact way of securely transmitting information between parties. Often used in token-based authentication.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;OAuth and OpenID Connect: Protocols for secure, token-based authentication.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;authentication-in-web-development&quot;&gt;Authentication in Web Development&lt;/h4&gt;
&lt;p&gt;Authentication plays a key role in both the frontend and backend of web development.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Frontend Authentication&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The frontend is where the user interacts with the application. It collects input, like login credentials, and sends them to the backend for validation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Example: A user submits a login form with their username and password, which is sent to the server for verification.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;Backend Authentication&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The backend handles the actual authentication process, such as validating credentials, generating tokens, and ensuring secure storage of user data.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Example: The server checks the submitted password against a hashed password stored in a database, then creates a session or JWT token for the user.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;authentication-process&quot;&gt;Authentication Process&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Signup / register/join/create account: The process where a new user creates an account by providing a username, password, and possibly other personal information.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sign-in/login: To verify a user's identity and use their credentials (username and password) to access their account.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Password Recovery/forgot password/reset password: If a user forgets their password, a recovery process (via email or SMS) is initiated to allow them to reset it and regain access to their account.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=5633618545210237070&quot; alt=&quot;Authentication flow&quot;&gt;&lt;/p&gt;
&lt;h4 id=&quot;security-best-practices-for-web-application-authentication&quot;&gt;Security Best Practices for Web Application Authentication&lt;/h4&gt;
&lt;p&gt;To keep your web application secure, it's important to follow the best practices for authentication:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Use Strong Passwords&lt;br&gt;
Encourage users to create strong, unique passwords by combining letters, numbers, and special characters.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Implement Multi-Factor Authentication (MFA)&lt;br&gt;
Adding a second layer of authentication significantly reduces the risk of unauthorized access eg Password + Authenticator App, and Password + SMS Text Code&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use HTTPS&lt;br&gt;
Always encrypt your website with HTTPS to protect the data being transmitted between the client and server.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Hash Passwords Properly&lt;br&gt;
Never store passwords in plaintext. Use hashing algorithms like bcrypt or Argon2 to securely store passwords.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Limit Login Attempts&lt;br&gt;
Implement account lockout or CAPTCHA after several failed login attempts to prevent brute-force attacks.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Secure Tokens&lt;br&gt;
For token-based authentication, ensure tokens are stored securely and have a short expiration time.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Session Management&lt;br&gt;
Ensure secure session management practices, such as invalidating sessions on logout and using secure cookies.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;common-challenges-and-solutions&quot;&gt;Common Challenges and Solutions&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;Weak Passwords&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Solution: Enforce strong password policies and encourage the use of password managers.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;Session Hijacking&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Solution: Use secure cookie flags and ensure tokens are stored safely.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;Phishing Attacks&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Solution: Implement 2FA and educate users about phishing scams.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;advanced-techniques-and-additional-considerations&quot;&gt;Advanced Techniques and Additional Considerations&lt;/h4&gt;
&lt;p&gt;For advanced users or developers, consider these approaches to enhance authentication security:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Adaptive Authentication&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Use machine learning or risk-based authentication to adjust security measures based on user behavior, location, or device.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;Federated Identity Management&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Utilize SSO (Single Sign-On) and other federated identity systems to allow users to log in across multiple applications with one identity.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;Biometric Authentication in Web Apps&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Explore WebAuthn and FIDO2 standards to implement biometric authentication in web browsers.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;conclusion-6&quot;&gt;Conclusion&lt;/h5&gt;
&lt;p&gt;Authentication is a critical part of securing web applications. By understanding the different types of authentication, factors, and best practices, you can ensure that your applications remain secure while offering a smooth user experience. Start implementing these strategies in your development projects today, and always stay ahead of the latest security trends to protect user data and prevent unauthorized access.&lt;/p&gt;
&lt;p&gt;Ready to level up your web app's security? Start applying these authentication techniques today and share your thoughts in the comments below!&lt;/p&gt;
</description>
<category>App Security</category>
<guid isPermaLink="true">https://coderlegion.com/1322/a-beginners-guide-to-authentication-best-practices-and-key-concepts</guid>
<pubDate>Sun, 02 Mar 2025 02:11:50 +0000</pubDate>
</item>
</channel>
</rss>