<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ authentication - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ authentication - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 23 Aug 2026 10:00:50 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/authentication/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build an AI Agent with Per-User OAuth Access [Full Handbook] ]]>
                </title>
                <description>
                    <![CDATA[ When your AI agent serves more than one person, every tool call must answer: who's the agent acting for? Let's learn how to solve this by building an AI agent that connects with Slack and GitHub. A Sl ]]>
                </description>
                <link>https://www.freecodecamp.org/news/ai-agent-per-user-oauth-slack-github/</link>
                <guid isPermaLink="false">6a7c95758a35a7792fd567c3</guid>
                
                    <category>
                        <![CDATA[ ai agents ]]>
                    </category>
                
                    <category>
                        <![CDATA[ authentication ]]>
                    </category>
                
                    <category>
                        <![CDATA[ tool calling ]]>
                    </category>
                
                    <category>
                        <![CDATA[ oauth ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Saif Ali Shaik ]]>
                </dc:creator>
                <pubDate>Wed, 12 Aug 2026 15:47:01 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/889cf8c8-41f9-4dec-aa5f-128cb24f0082.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>When your AI agent serves more than one person, every tool call must answer: who's the agent acting for? Let's learn how to solve this by building an AI agent that connects with Slack and GitHub.</p>
<p>A Slack read uses that user's workspace. A GitHub issue is created as that user, in a repository they can access. An agent can make the wrong call, but it must never act with the wrong user's access.</p>
<p>The fix has two parts, and both appear in the first half of this tutorial:</p>
<ol>
<li><p><strong>Each user grants access separately.</strong> Alice authorizes Slack for herself. Bob authorizes it for himself.</p>
</li>
<li><p><strong>Your agent passes an identifier, not a token.</strong> A string like <code>alice@example.com</code> selects whose grant to use. One function turns it into a token at the moment of the call, and that token never reaches your model inputs, your tool schemas, or your logs.</p>
</li>
</ol>
<p>Most agent tutorials stop before either point. They hand you an API key, wire up one function, and the model calls it. The design works until a second person shows up.</p>
<p>To make the pattern concrete, you'll build a command-line agent that watches a Slack channel, decides on its own which messages describe real work, files a GitHub issue for those, and replies in the Slack thread with the issue link. Every call runs as one user's own OAuth grant.</p>
<p>You'll write the OAuth flow yourself: the consent redirect, the <code>state</code> check, the token exchange, an encrypted store, and the refresh path. None of it is long, and seeing it whole is what makes the identity argument checkable instead of a claim you take on faith.</p>
<p>Two topics stay out of scope here: we won't cover Model Context Protocol servers or voice or realtime hosts. The identity pattern holds in both settings, but the surrounding plumbing deserves its own article.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-what-youll-build">What You'll Build</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-are-ai-agent-tools">What Are AI Agent Tools?</a></p>
</li>
<li><p><a href="#heading-why-a-shared-token-breaks">Why a Shared Token Breaks</a></p>
</li>
<li><p><a href="#heading-architecture-overview">Architecture Overview</a></p>
</li>
<li><p><a href="#heading-how-to-register-the-slack-and-github-oauth-apps">How to Register the Slack and GitHub OAuth Apps</a></p>
</li>
<li><p><a href="#heading-how-to-run-the-consent-flow">How to Run the Consent Flow</a></p>
</li>
<li><p><a href="#heading-how-to-store-tokens-encrypted-keyed-by-user">How to Store Tokens Encrypted, Keyed by User</a></p>
</li>
<li><p><a href="#heading-how-to-run-tool-calls-as-the-current-user">How to Run Tool Calls as the Current User</a></p>
</li>
<li><p><a href="#heading-how-to-handle-refresh-and-revocation">How to Handle Refresh and Revocation</a></p>
</li>
<li><p><a href="#heading-how-to-add-a-second-provider">How to Add a Second Provider</a></p>
</li>
<li><p><a href="#heading-full-walkthrough">Full Walkthrough</a></p>
</li>
<li><p><a href="#heading-how-to-apply-the-pattern-to-other-use-cases">How to Apply the Pattern to Other Use Cases</a></p>
</li>
<li><p><a href="#heading-what-went-wrong-when-i-built-this">What Went Wrong When I Built This</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-youll-build">What You'll Build</h2>
<p>The agent is called <code>channel-watcher-agent</code>. Each run does four things:</p>
<ol>
<li><p>Reads recent messages from a Slack channel.</p>
</li>
<li><p>Asks a model, message by message, whether the text describes a bug or a concrete action item.</p>
</li>
<li><p>Files a GitHub issue for the messages that qualify.</p>
</li>
<li><p>Replies in the original Slack thread with a link to the new issue.</p>
</li>
</ol>
<p><strong>Nobody clicks a button to start any of it.</strong> Slack already ships a "create an issue from this message" action, which is a different product. Here the agent reads the channel, forms its own judgment, and acts only on what it judges worth acting on.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5d426742d3ccd88c5676d4a2/cb98e59f-ac1a-48c7-9ab3-46a49f78af58.png" alt="Example of the tool in action" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>The stack stays small on purpose:</p>
<table>
<thead>
<tr>
<th>Piece</th>
<th>Role</th>
</tr>
</thead>
<tbody><tr>
<td>Node.js, plain ES modules</td>
<td>No web framework, no queue</td>
</tr>
<tr>
<td><code>node:http</code></td>
<td>The OAuth callback server</td>
</tr>
<tr>
<td><code>node:crypto</code></td>
<td>Token encryption</td>
</tr>
<tr>
<td><code>node:sqlite</code></td>
<td>The token store, with no dependency to install</td>
</tr>
<tr>
<td><a href="https://ai-sdk.dev/">Vercel AI SDK</a></td>
<td>The model call and the tool loop</td>
</tr>
</tbody></table>
<p>Three of those five ship with Node. The only packages you install are the AI SDK and its friends.</p>
<p>By the end you'll have:</p>
<ul>
<li><p>Two OAuth apps, Slack and GitHub, that a user consents to once.</p>
</li>
<li><p>An encrypted token store keyed by user and provider.</p>
</li>
<li><p>An agent that resolves the current user to an identifier and never lets a token reach the model.</p>
</li>
<li><p>A tool loop where the model decides whether to file an issue at all.</p>
</li>
<li><p>A demonstration that a second user's run stops instead of reading the first user's data.</p>
</li>
</ul>
<p>The finished code lives at <a href="https://github.com/saif-shines/channel-watcher-agent">github.com/saif-shines/channel-watcher-agent</a>.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Accounts and tools:</p>
<ul>
<li><p><strong>Node.js 22.13 or newer</strong>, plus npm. The token store uses <a href="https://nodejs.org/api/sqlite.html"><code>node:sqlite</code></a>, which is stable from that version on.</p>
</li>
<li><p><strong>A Slack workspace</strong> where you can install apps, and a channel to watch. A throwaway channel works best.</p>
</li>
<li><p><strong>A GitHub account</strong> and a repository that can absorb test issues.</p>
</li>
<li><p><strong>An API key for a model provider</strong> the AI SDK supports. Anthropic is used in the examples.</p>
</li>
<li><p><a href="https://github.com/FiloSottile/mkcert"><strong>mkcert</strong></a>, to issue a local HTTPS certificate. <a href="#heading-how-to-register-the-slack-and-github-oauth-apps">How to Register the Slack and GitHub OAuth Apps</a> explains why an ordinary <code>http://localhost</code> callback will not do.</p>
</li>
</ul>
<p>Useful background, though none of it is a hard requirement:</p>
<ul>
<li><p><code>async</code> and <code>await</code>, and reading a small Node script.</p>
</li>
<li><p>OAuth 2.0 at a high level: an app redirects a user to a provider, the user consents, the app receives a token.</p>
</li>
<li><p>Tool calling, sometimes called function calling. The next section covers what the tutorial needs.</p>
</li>
</ul>
<p><strong>One warning before starting:</strong> The agent writes to real systems. It opens real GitHub issues and posts real Slack messages. Use a test Slack channel and a throwaway GitHub repository while you're still checking that it only acts on messages you intend.</p>
<h2 id="heading-what-are-ai-agent-tools">What Are AI Agent Tools?</h2>
<p><strong>A tool is a function you hand the model along with your input.</strong> The model can't run that function itself. It can only ask: call <code>fileGithubIssue</code> with this title and this body. Your code performs the call, returns the result, and the model uses that result to choose the next step.</p>
<p>Request, execute, return. The exchange is the whole mechanism, and everything called an "agent" is a loop around it.</p>
<h3 id="heading-how-a-tool-differs-from-an-api">How a Tool Differs from an API</h3>
<p>Tools and APIs wrap the same call but are written for different readers.</p>
<p>An API is written for you. It assumes you read the documentation, and that you know <code>thread_ts</code> is the field that turns a Slack message into a threaded reply.</p>
<p>A tool is written for a model that has read nothing. So a tool carries its own explanation:</p>
<ul>
<li><p>A <strong>name</strong> the model can reason about, like <code>fileGithubIssue</code>.</p>
</li>
<li><p>A <strong>description</strong> in plain language, including when not to use the tool.</p>
</li>
<li><p>A <strong>schema</strong> for the inputs, so the model knows <code>title</code> is a required string.</p>
</li>
</ul>
<p>Below is one tool from the project. Most of the code is explanation rather than logic:</p>
<pre><code class="language-javascript">const fileGithubIssue = tool({
  description: 'File a GitHub issue for an actionable Slack message',
  inputSchema: z.object({
    title: z.string(),
    body: z.string(),
  }),
  execute: async ({ title, body }) =&gt; {
    // ... the actual API call goes here
  },
});
</code></pre>
<p>The <code>description</code> and <code>inputSchema</code> are the parts the model sees. The <code>execute</code> function is yours alone. Identity gets settled inside <code>execute</code>, so the model never learns which account the call ran against.</p>
<h3 id="heading-why-models-handle-tools-better-than-raw-api-calls">Why Models Handle Tools Better Than Raw API Calls</h3>
<p>Pasting a curl command into the input and asking the model to fill in the blanks is possible. But this approach fails in predictable ways.</p>
<p>Tools work better for three reasons:</p>
<ol>
<li><p>The schema is enforced before your code runs. A malformed tool call gets rejected and retried by the SDK. A malformed URL fails at runtime instead.</p>
</li>
<li><p>Results return to the model. After <code>fileGithubIssue</code> returns, the model can read the new issue URL and use it in the Slack reply. The chaining is what makes the second step possible.</p>
</li>
<li><p>Credentials stay out of the conversation. The model asks for an action by name and never sees a token. A token it never sees can't leak into a completion, a log line, or a prompt-injection payload.</p>
</li>
</ol>
<p>Reason three is what the rest of this tutorial builds toward. You'll keep tokens out of the model on purpose: the agent holds an identifier, and a token appears only at the moment of the provider call.</p>
<h3 id="heading-most-agents-need-more-than-one-app">Most Agents Need More Than One App</h3>
<p>Few useful agents talk to a single app. A support agent reads Zendesk and updates Salesforce. A standup agent reads GitHub and posts to Slack. A scheduling agent reads Gmail and writes to Google Calendar.</p>
<p>Each app brings its own OAuth registration, scope names, token lifetime, and refresh behavior. Multiply the list by every user of the agent, and the real problem appears.</p>
<h2 id="heading-why-a-shared-token-breaks">Why a Shared Token Breaks</h2>
<p>One shared credential for everybody works in a demo and fails once a second person shows up. Picture the quick version of the Slack half: create a Slack app, install it, copy the bot token into <code>.env</code>, and let every tool call use it.</p>
<p>Three problems arrive together.</p>
<p>First, every run uses the same permissions. The bot sees every channel it was invited to, no matter who triggered the run. Ask the agent about a channel you were never in, and the bot reads it anyway. The agent has become a way around your own workspace permissions.</p>
<p>Second, the audit trail is also wrong. Every GitHub issue says the bot opened it. Every Slack reply comes from the bot. Asked why an issue exists, the honest answer is "an agent filed it for somebody, and we can't tell who."</p>
<p>And third, revocation stops working. A user leaves the company and their Slack account is deactivated. The agent keeps running, because it never used their credentials.</p>
<p>The alternative is per-user grants. Each user authorizes the apps for themselves. That creates a new requirement, though: somewhere to keep those grants.</p>
<h3 id="heading-the-distinction-is-one-field-in-one-response">The Distinction is One Field in One Response</h3>
<p>Slack makes the difference unusually easy to see. When a user finishes the consent screen, <a href="https://docs.slack.dev/authentication/installing-with-oauth">the token exchange</a> returns both kinds of token in the same JSON object:</p>
<pre><code class="language-json">{
  "ok": true,
  "access_token": "xoxb-REDACTED-BOT-TOKEN",
  "token_type": "bot",
  "authed_user": {
    "id": "U0A1B2C3D",
    "scope": "channels:history,chat:write,users:read",
    "access_token": "xoxp-REDACTED-USER-TOKEN",
    "token_type": "user"
  }
}
</code></pre>
<p>The top-level <code>access_token</code> is the bot. The nested <code>authed_user.access_token</code> is the person who just consented. Reading <code>conversations.history</code> with the first one returns every channel the app was invited to. Reading it with the second returns only the channels that users can already see. The same split governs writes: <a href="https://docs.slack.dev/reference/methods/chat.postMessage"><code>chat.postMessage</code></a> with a user token posts under that person's name.</p>
<p>Two fields, one letter apart in the prefix, and the entire permission model of your agent hangs on which one you store. This tutorial requests only user scopes, so Slack issues no bot token at all.</p>
<h3 id="heading-tokens-must-stay-out-of-the-model-and-the-logs">Tokens Must Stay Out of the Model and the Logs</h3>
<p>Per-user tokens become the most sensitive data in the system. Two destinations are off limits:</p>
<ul>
<li><p><strong>The model:</strong> Keep tokens out of inputs, tool descriptions, and tool return values. A model that has seen a token can repeat it, and prompt injection turns any tool result into untrusted input.</p>
</li>
<li><p><strong>Your logs:</strong> Tool inputs and outputs are exactly what you want to log while debugging an agent. Tokens traveling in those payloads land in your log store permanently.</p>
</li>
</ul>
<p>This tutorial keeps tokens on one narrow path. Your code passes an identifier, a stable reference to one user. One helper turns that identifier into a token, and from there the token goes straight into a provider call and nowhere else. It's never named in a tool schema, never attached to anything the model can read, and never returned from a tool.</p>
<h3 id="heading-why-you-own-the-oauth-apps-and-the-store">Why You Own the OAuth Apps and the Store</h3>
<p>The point of writing the flow yourself isn't the plumbing. It's control over who may use whose grant.</p>
<p>In this tutorial the users are teammates. Each person connects their own Slack and GitHub, and the agent acts as whoever triggered the run. The same design holds when those users are customers of your product: each person still has their own grant, and a wrong mapping means one person's run using someone else's access. Only the source of the identifier changes. A session for teammates, a tenant record for customers.</p>
<h2 id="heading-architecture-overview">Architecture Overview</h2>
<p>Two flows matter, and they happen at different times. Keeping them separate is most of the work.</p>
<p>Connection time happens once per user, per app. The user consents, and tokens land in your store. The agent isn't running.</p>
<p>Runtime happens on every execution. The agent resolves the current user to an identifier and does its work. No consent screens and no browser.</p>
<pre><code class="language-text">CONNECTION TIME (once per user, per app)

  Your user              connect.js              Slack / GitHub
     |                       |                         |
     |-- "connect Slack" ---&gt;|                         |
     |&lt;--- consent link -----|                         |
     |----------------------- OAuth consent ----------&gt;|
     |                       |&lt;--- redirect + code ----|
     |                       |---- exchange code -----&gt;|
     |                       |&lt;---- tokens ------------|
     |                       |                         |
     |                  [encrypt, store                |
     |                   under (identifier,            |
     |                   provider)]                    |
     |                       |                         |


RUNTIME (every agent run)

  Your agent             Token store             Slack / GitHub
     |                       |                         |
  [resolve identifier        |                         |
   from your own session]    |                         |
     |                       |                         |
     |-- getAccessToken( ---&gt;|                         |
     |     identifier,       |                         |
     |     provider )        |                         |
     |&lt;---- token -----------|                         |
     |                       |                         |
     |------------------ API call as user ------------&gt;|
     |&lt;----------------- result -----------------------|
     |                       |                         |
  [model sees result,        |                         |
   never a token]            |                         |
</code></pre>
<p>Three properties follow from the shape.</p>
<p>The identifier replaces the token in your agent code. Everything above the token store handles a string like <code>alice@example.com</code> or <code>user_8f21c</code>. The string is worthless on its own: without the store and its encryption key, it opens nothing.</p>
<p>One identity spans many apps. A single identifier has a Slack row and a GitHub row beneath it. A third app doesn't create a third identity to reconcile.</p>
<p>Authorization stays in your code. The store answers which tokens belong to an identifier. The store can't know whether the request deserved an answer. Deciding that the caller may act as that identifier happens before any call.</p>
<p>One rule follows, and bending it defeats the whole design: resolve the identifier server-side from an authenticated session. Never accept an identifier from a request body, a query parameter, or a browser. An identifier accepted from a client is an "act as any user" endpoint.</p>
<h2 id="heading-how-to-register-the-slack-and-github-oauth-apps">How to Register the Slack and GitHub OAuth Apps</h2>
<p>The walkthrough uses Slack and GitHub as the two providers end to end. Both need the same three things: a registered app, a redirect URI, and a set of scopes. The details differ enough to be worth walking through separately.</p>
<h3 id="heading-the-redirect-uri-has-to-use-https">The Redirect URI Has to Use HTTPS</h3>
<p>Most tutorials that touch OAuth hand you <code>http://localhost:3000/callback</code> and move on. Slack rejects it. <a href="https://docs.slack.dev/authentication/installing-with-oauth">Slack's documentation</a> states flatly that "a Redirect URL must also use HTTPS", and it makes no exception for <code>localhost</code>. GitHub is more relaxed and accepts either, so a single HTTPS callback satisfies both.</p>
<p>The rule looks pedantic, because on <code>localhost</code> the request never leaves your machine and there's nothing on the wire to intercept. Slack applies it uniformly anyway, and a uniform rule with no exemptions is a defensible choice for a provider handing out credentials: every exemption is a branch somebody has to get right, and "is this really localhost" is a question that has been answered incorrectly before.</p>
<p><a href="https://github.com/FiloSottile/mkcert">mkcert</a> issues a certificate signed by a local authority it adds to your system trust store, so the browser accepts it without a warning:</p>
<pre><code class="language-bash">mkcert -install
mkcert localhost
</code></pre>
<p>That writes <code>localhost.pem</code> and <code>localhost-key.pem</code> into the current directory. A tunneling service such as ngrok also works, but its free URLs rotate, which means re-editing both app registrations every session.</p>
<h3 id="heading-the-slack-app-and-the-one-setting-that-matters">The Slack App, and the One Setting That Matters</h3>
<p>At <a href="https://api.slack.com/apps">api.slack.com/apps</a>, create an app in your workspace. Then open <strong>OAuth &amp; Permissions</strong> and set two things.</p>
<p>Add <code>https://localhost:3000/callback</code> under <strong>Redirect URLs</strong>.</p>
<p>Then find the scopes. The page has two sections, and choosing the wrong one silently rebuilds the shared-bot design:</p>
<table>
<thead>
<tr>
<th>Section</th>
<th>What it grants</th>
<th>Use it here?</th>
</tr>
</thead>
<tbody><tr>
<td>Bot Token Scopes</td>
<td>A <code>xoxb-</code> token that acts as the app</td>
<td>No</td>
</tr>
<tr>
<td>User Token Scopes</td>
<td>A <code>xoxp-</code> token that acts as the person</td>
<td>Yes</td>
</tr>
</tbody></table>
<p>Under <strong>User Token Scopes</strong>, add:</p>
<ul>
<li><p><code>channels:history</code>: read messages in public channels the user belongs to</p>
</li>
<li><p><code>chat:write</code>: post as the user</p>
</li>
<li><p><code>users:read</code>: turn user IDs into names</p>
</li>
</ul>
<p>Leave Bot Token Scopes empty. Copy the Client ID and Client Secret from <strong>Basic Information</strong>.</p>
<h3 id="heading-the-github-oauth-app">The GitHub OAuth App</h3>
<p>Under Settings → Developer settings → OAuth Apps → New OAuth App, set the Authorization callback URL to the same <code>https://localhost:3000/callback</code>, then generate a client secret. GitHub documents <a href="https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps">the web application flow</a> in full if you want the surrounding detail.</p>
<p>GitHub's <a href="https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps">scope</a> for issue creation depends on the repository:</p>
<ul>
<li><p><code>repo</code> covers private repositories, and grants read and write access to code along with it.</p>
</li>
<li><p><code>public_repo</code> is the narrower choice, and enough when your test repository is public.</p>
</li>
</ul>
<p>Take the narrower one when you can. A scope you didn't need is a scope you have to explain later.</p>
<h3 id="heading-the-environment-file">The Environment File</h3>
<p>Both apps produce a client ID and a client secret, and the store needs an encryption key. Generate the key first:</p>
<pre><code class="language-bash">node -e "console.log(require('node:crypto').randomBytes(32).toString('base64'))"
</code></pre>
<p>Then fill in <code>.env</code>:</p>
<pre><code class="language-bash">OAUTH_REDIRECT_URI=https://localhost:3000/callback
TLS_CERT_PATH=./localhost.pem
TLS_KEY_PATH=./localhost-key.pem

SLACK_CLIENT_ID=
SLACK_CLIENT_SECRET=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=

TOKEN_ENCRYPTION_KEY=

SLACK_CHANNEL_ID=C0XXXXXXXXX
GITHUB_REPO=your-name/your-test-repo
</code></pre>
<p>Those client secrets authenticate <strong>your application</strong> to the providers. They're not user credentials, and they never belong in a browser.</p>
<h2 id="heading-how-to-run-the-consent-flow">How to Run the Consent Flow</h2>
<p>Everything provider-specific belongs in one place, so that adding a third provider later means adding an entry rather than a branch.</p>
<h3 id="heading-step-1-describe-each-provider-once">Step 1: Describe Each Provider Once</h3>
<pre><code class="language-javascript">const REDIRECT_URI = process.env.OAUTH_REDIRECT_URI;

export const providers = {
  slack: {
    label: 'Slack',
    authorizeUrl: 'https://slack.com/oauth/v2/authorize',
    tokenUrl: 'https://slack.com/api/oauth.v2.access',

    // These go in `user_scope`, not `scope`. Scopes listed under `scope` grant
    // a bot token, and a bot token is what this project exists to avoid.
    userScopes: ['channels:history', 'chat:write', 'users:read'],

    buildAuthorizeUrl(state) {
      const url = new URL(this.authorizeUrl);
      url.searchParams.set('client_id', process.env.SLACK_CLIENT_ID);
      url.searchParams.set('user_scope', this.userScopes.join(','));
      url.searchParams.set('redirect_uri', REDIRECT_URI);
      url.searchParams.set('state', state);
      return url.toString();
    },
    // exchangeCode and refresh follow below
  },
};
</code></pre>
<p><strong>The</strong> <code>user_scope</code> <strong>parameter is the whole argument in one line.</strong> Slack reads <code>scope</code> for bot permissions and <code>user_scope</code> for user permissions. This project sets only the second, so the response comes back with no bot token in it at all.</p>
<p>The <code>state</code> parameter isn't optional. It's a random string you generate, send to the provider, and check on the way back. Without it, any page on the internet can point a browser at your callback URL with an attacker's <code>code</code> attached, and your server will happily exchange it and store the attacker's token under your user's identifier.</p>
<h3 id="heading-step-2-exchange-the-code-and-take-the-right-token">Step 2: Exchange the Code, and Take the Right Token</h3>
<pre><code class="language-javascript">async exchangeCode(code) {
  const response = await fetch(this.tokenUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      code,
      client_id: process.env.SLACK_CLIENT_ID,
      client_secret: process.env.SLACK_CLIENT_SECRET,
      redirect_uri: REDIRECT_URI,
    }),
  });

  const json = await response.json();

  // Slack answers HTTP 200 even when the exchange failed. The `ok` field
  // is the real status.
  if (!json.ok) {
    throw new Error(`Slack token exchange failed: ${json.error}`);
  }

  return normalizeSlackTokens(json.authed_user);
}
</code></pre>
<p>Two details in that function cost real debugging time when missed.</p>
<p><strong>Slack returns HTTP 200 for failures.</strong> Checking <code>response.ok</code> tells you the HTTP request succeeded, which it did. The <code>json.ok</code> field is the one that reports whether the OAuth exchange worked.</p>
<p><code>json.authed_user</code><strong>, not</strong> <code>json</code><strong>.</strong> This is the fork from the section above, expressed as one property access. Reading <code>json.access_token</code> here would compile, run, store a token, and quietly give every user of your agent the same bot identity.</p>
<p>Normalizing the result keeps the rest of the codebase provider-agnostic:</p>
<pre><code class="language-javascript">function normalizeSlackTokens(authedUser) {
  return {
    accessToken: authedUser.access_token,
    refreshToken: authedUser.refresh_token ?? null,
    expiresAt: authedUser.expires_in
      ? Date.now() + authedUser.expires_in * 1000
      : null,
    scope: authedUser.scope,
  };
}
</code></pre>
<p>GitHub's version of the same function differs in two ways worth noting:</p>
<pre><code class="language-javascript">async exchangeCode(code) {
  const response = await fetch(this.tokenUrl, {
    method: 'POST',
    // Without this header GitHub answers with a form-encoded body.
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      Accept: 'application/json',
    },
    body: new URLSearchParams({
      code,
      client_id: process.env.GITHUB_CLIENT_ID,
      client_secret: process.env.GITHUB_CLIENT_SECRET,
      redirect_uri: REDIRECT_URI,
    }),
  });

  const json = await response.json();
  if (json.error) {
    throw new Error(
      `GitHub token exchange failed: ${json.error_description ?? json.error}`
    );
  }

  // OAuth App tokens carry no expiry, so there is nothing to refresh.
  return {
    accessToken: json.access_token,
    refreshToken: null,
    expiresAt: null,
    scope: json.scope,
  };
}
</code></pre>
<p>The <code>Accept: application/json</code> header is easy to skip and produces a confusing failure: <code>response.json()</code> throws on a body that came back as <code>access_token=gho_...&amp;scope=repo</code>.</p>
<h3 id="heading-step-3-catch-the-redirect">Step 3: Catch the Redirect</h3>
<p>OAuth needs somewhere to land. For a command-line tool, a server that starts, handles one callback per provider, and exits is enough. Because Slack demands HTTPS, the scheme in <code>OAUTH_REDIRECT_URI</code> decides which kind of server to start:</p>
<pre><code class="language-javascript">function createCallbackServer(handler) {
  if (redirect.protocol !== 'https:') {
    return createHttpServer(handler);
  }

  try {
    return createHttpsServer(
      {
        cert: readFileSync(process.env.TLS_CERT_PATH),
        key: readFileSync(process.env.TLS_KEY_PATH),
      },
      handler
    );
  } catch (err) {
    throw new Error(
      `Could not read the TLS certificate (${err.code ?? err.message}).\n` +
        'Generate a locally-trusted one with mkcert:\n' +
        '  mkcert -install\n' +
        '  mkcert localhost\n' +
        'then point TLS_CERT_PATH and TLS_KEY_PATH at the two files it writes.'
    );
  }
}
</code></pre>
<p>A missing certificate is going to happen to somebody, and <code>ENOENT</code> on its own explains nothing about OAuth. The catch block spends four lines saying what to run instead.</p>
<p>The handler itself is where <code>state</code> gets checked:</p>
<pre><code class="language-javascript">const pending = new Map();

function handleCallback(request, response) {
  const url = new URL(request.url, redirect.origin);

  if (url.pathname !== redirect.pathname) {
    response.writeHead(404).end('Not found');
    return;
  }

  const state = url.searchParams.get('state');
  const entry = pending.get(state);

  if (!entry) {
    response.writeHead(400).end('State mismatch. Start the flow again.');
    return;
  }

  pending.delete(state);

  const error = url.searchParams.get('error');
  if (error) {
    response.writeHead(400).end(`Authorization denied: ${error}`);
    entry.reject(new Error(`[${entry.provider}] authorization denied: ${error}`));
    return;
  }

  entry.finish(url.searchParams.get('code'), response);
}
</code></pre>
<p><strong>The</strong> <code>pending</code> <strong>map is the</strong> <code>state</code> <strong>check.</strong> A state value gets into that map only when this process generated it, and it's deleted the moment it's used. An unrecognized state means the callback didn't come from a flow you started, and a state that arrives twice means a replay. Both fall out of one <code>Map</code> lookup.</p>
<p>Generating the state and waiting for its callback:</p>
<pre><code class="language-javascript">function connect(providerName) {
  const provider = providers[providerName];
  const state = randomBytes(16).toString('hex');

  console.log(`\n[${providerName}] authorize as "${IDENTIFIER}":`);
  console.log(provider.buildAuthorizeUrl(state));

  return new Promise((resolve, reject) =&gt; {
    pending.set(state, {
      provider: providerName,
      reject,
      async finish(code, response) {
        const tokens = await provider.exchangeCode(code);
        saveGrant(IDENTIFIER, providerName, tokens);
        response
          .writeHead(200, { 'Content-Type': 'text/html' })
          .end(`&lt;p&gt;${provider.label} connected. You can close this tab.&lt;/p&gt;`);
        resolve();
      },
    });
  });
}
</code></pre>
<p><code>randomBytes(16)</code> and not <code>Math.random()</code>. A predictable state parameter is the same as no state parameter.</p>
<p>Running it walks each unconnected provider in turn:</p>
<pre><code class="language-text">[slack] authorize as "alice@example.com":
https://slack.com/oauth/v2/authorize?client_id=123.456&amp;user_scope=channels%3Ahistory%2Cchat%3Awrite%2Cusers%3Aread&amp;redirect_uri=https%3A%2F%2Flocalhost%3A3000%2Fcallback&amp;state=1159699dbf1a808fd33ba31c7b643505
</code></pre>
<p>Notice what that URL doesn't contain: any <code>scope</code> parameter. Slack has no instruction to mint a bot token, so it won't.</p>
<h2 id="heading-how-to-store-tokens-encrypted-keyed-by-user">How to Store Tokens Encrypted, Keyed by User</h2>
<p>The store answers one question: which token belongs to this user, for this provider? Everything else about it follows from keeping that answer safe.</p>
<p><code>node:sqlite</code> has shipped with Node since v22.5, and stopped requiring a flag in v22.13. That makes a real database available with nothing to install:</p>
<pre><code class="language-javascript">import { DatabaseSync } from 'node:sqlite';
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';

const KEY = Buffer.from(process.env.TOKEN_ENCRYPTION_KEY ?? '', 'base64');

if (KEY.length !== 32) {
  throw new Error(
    'TOKEN_ENCRYPTION_KEY must be 32 bytes, base64-encoded. ' +
      `Got ${KEY.length} bytes.`
  );
}

const db = new DatabaseSync(
  process.env.TOKEN_DB_PATH ?? new URL('../tokens.db', import.meta.url).pathname
);

// One row per user, per provider. expires_at stays outside the ciphertext so
// a token's freshness can be checked without decrypting it.
db.exec(`
  CREATE TABLE IF NOT EXISTS grants (
    identifier TEXT    NOT NULL,
    provider   TEXT    NOT NULL,
    ciphertext BLOB    NOT NULL,
    iv         BLOB    NOT NULL,
    auth_tag   BLOB    NOT NULL,
    expires_at INTEGER,
    PRIMARY KEY (identifier, provider)
  )
`);
</code></pre>
<p><strong>The composite primary key is the isolation guarantee, written down.</strong> <code>(identifier, provider)</code> means Alice's Slack row and Bob's Slack row can't collide, and no query that supplies both parts can return somebody else's grant.</p>
<p><code>expires_at</code> <strong>sits outside the ciphertext deliberately.</strong> Checking whether a token needs refreshing is something you do before every call. Decrypting to find out would mean decrypting constantly, so the one field that isn't a secret stays readable.</p>
<p>Encryption is AES-256-GCM, which authenticates as well as encrypts:</p>
<pre><code class="language-javascript">function encrypt(payload) {
  const iv = randomBytes(12);
  const cipher = createCipheriv('aes-256-gcm', KEY, iv);
  const ciphertext = Buffer.concat([
    cipher.update(JSON.stringify(payload), 'utf8'),
    cipher.final(),
  ]);
  return { ciphertext, iv, authTag: cipher.getAuthTag() };
}

function decrypt({ ciphertext, iv, authTag }) {
  const decipher = createDecipheriv('aes-256-gcm', KEY, iv);
  decipher.setAuthTag(authTag);
  const plaintext = Buffer.concat([
    decipher.update(ciphertext),
    decipher.final(),
  ]);
  return JSON.parse(plaintext.toString('utf8'));
}
</code></pre>
<p>Three rules govern that pair, and breaking any one of them is worse than not encrypting at all, because it looks like it worked:</p>
<ol>
<li><p><strong>A fresh IV per encryption:</strong> Reusing an initialization vector with GCM is a catastrophic failure, not a minor one. <code>randomBytes(12)</code> on every call, stored beside the ciphertext.</p>
</li>
<li><p><strong>Keep the auth tag:</strong> GCM produces a tag that proves the ciphertext wasn't altered. Without <code>setAuthTag</code> on the way back, you have encryption without integrity, and <code>decipher.final()</code> won't complain.</p>
</li>
<li><p><strong>Encrypt the whole token object, not each field.</strong> One ciphertext for <code>{ accessToken, refreshToken, scope }</code> means one IV and one tag to manage rather than three of each.</p>
</li>
</ol>
<p>Writing and reading are then unremarkable:</p>
<pre><code class="language-javascript">export function saveGrant(identifier, provider, tokens) {
  const { ciphertext, iv, authTag } = encrypt(tokens);
  db.prepare(
    `INSERT INTO grants (identifier, provider, ciphertext, iv, auth_tag, expires_at)
     VALUES (?, ?, ?, ?, ?, ?)
     ON CONFLICT (identifier, provider) DO UPDATE SET
       ciphertext = excluded.ciphertext,
       iv         = excluded.iv,
       auth_tag   = excluded.auth_tag,
       expires_at = excluded.expires_at`
  ).run(identifier, provider, ciphertext, iv, authTag, tokens.expiresAt ?? null);
}
</code></pre>
<p>The <code>ON CONFLICT</code> clause matters more than it looks. Re-consenting has to replace a grant rather than fail or duplicate it, and re-consenting is exactly what a user does after a revocation or a scope change.</p>
<p>The encryption key itself lives in <code>.env</code> here, which is right for a tutorial and wrong for production, where it belongs in a secrets manager or a KMS. Losing it makes every stored grant unreadable and forces every user to consent again. That is a real outage, but it's a better one than the alternative: a stolen database file that hands over working tokens for every user of your agent.</p>
<h2 id="heading-how-to-run-tool-calls-as-the-current-user">How to Run Tool Calls as the Current User</h2>
<p>Runtime has three moves: resolve the identifier, fetch a token with it, and wrap the whole thing as a tool.</p>
<h3 id="heading-step-1-resolve-the-identifier-then-authorize">Step 1: Resolve the Identifier, Then Authorize</h3>
<p>An identifier is <strong>any stable string</strong> that represents one user, an email address, a user ID, a tenant-scoped key.</p>
<pre><code class="language-javascript">// In a real app this comes from your authenticated session, resolved
// server-side. Never accept it from client input.
const IDENTIFIER = process.argv[2] ?? 'channel-watcher-agent';
</code></pre>
<p>Reading the identifier from <code>argv</code> keeps the demo runnable without a login, and it makes the isolation test later in this tutorial a single command. A real application replaces the line:</p>
<pre><code class="language-javascript">// Real app: resolve from your authenticated session, server-side.
const session = await getSession(request);                  // your auth
const identifier = await lookupIdentifier(session.userId);  // your database
</code></pre>
<p><strong>Order matters in those two lines.</strong> Authenticate the caller first, then look up which identifier the caller may act as. An identifier arriving from a client turns the endpoint into a reader of any user's Slack.</p>
<h3 id="heading-step-2-turn-the-identifier-into-a-token-late">Step 2: Turn the Identifier into a Token, Late</h3>
<p>One function stands between the identifier and every provider call:</p>
<pre><code class="language-javascript">const REFRESH_WINDOW_MS = 60_000;

export async function getAccessToken(identifier, providerName) {
  const grant = readGrant(identifier, providerName);

  if (!grant) {
    throw new Error(
      `[${providerName}] no grant for "${identifier}".\n` +
        `Connect it first: node src/connect.js ${identifier}`
    );
  }

  const expiringSoon =
    grant.expiresAt !== null &amp;&amp;
    grant.expiresAt !== undefined &amp;&amp;
    grant.expiresAt - Date.now() &lt; REFRESH_WINDOW_MS;

  if (!expiringSoon) {
    return grant.accessToken;
  }

  if (!grant.refreshToken) {
    throw new Error(
      `[${providerName}] token for "${identifier}" expired and no refresh ` +
        'token is stored. The user has to consent again.'
    );
  }

  const refreshed = await providers[providerName].refresh(grant.refreshToken);
  saveGrant(identifier, providerName, refreshed);
  return refreshed.accessToken;
}
</code></pre>
<p><strong>Call this immediately before the API call, not once at startup.</strong> A long agent run can outlive a twelve-hour token, and resolving tokens up front means discovering that at the least convenient moment. Fetching late costs one cheap database read and removes the whole class of problem.</p>
<p>Also, the <strong>sixty-second window isn't padding for its own sake.</strong> A token with four seconds left passes a naive expiry check and then expires in flight. Refreshing anything inside the window means the token handed back is good for at least a minute of work.</p>
<p>Finally, a missing grant raises an error rather than falling back. There's nothing sensible to fall back to. The correct outcome for an unconnected user is a stop, with a message saying how to connect.</p>
<h3 id="heading-step-3-wrap-provider-calls-as-tools">Step 3: Wrap Provider Calls as Tools</h3>
<p>Identity gets injected here, one layer below anything the model can influence:</p>
<pre><code class="language-javascript">export function buildTools(identifier) {
  const [owner, repo] = process.env.GITHUB_REPO.split('/');

  const fileGithubIssue = tool({
    description: 'File a GitHub issue for an actionable Slack message',
    inputSchema: z.object({
      title: z.string(),
      body: z.string(),
    }),
    execute: async ({ title, body }) =&gt; {
      const token = await getAccessToken(identifier, 'github');
      return createIssue(token, owner, repo, { title, body });
    },
  });

  const replyInSlackThread = tool({
    description:
      'Reply in the original Slack thread (e.g. with the created issue link)',
    inputSchema: z.object({
      text: z.string(),
      thread_ts: z.string(),
    }),
    execute: async ({ text, thread_ts }) =&gt; {
      const token = await getAccessToken(identifier, 'slack');
      return postThreadReply(
        token,
        process.env.SLACK_CHANNEL_ID,
        text,
        thread_ts
      );
    },
  });

  return { fileGithubIssue, replyInSlackThread };
}
</code></pre>
<p>Compare what the model controls against what it can't. The model chooses <code>title</code>, <code>body</code>, and <code>text</code>. <strong>The model can't choose the user.</strong> <code>identifier</code> is a closure argument, fixed before the model ran, and it appears in no <code>inputSchema</code>. There's no input that makes the model file an issue as somebody else, because the account isn't one of its inputs.</p>
<p>Return values deserve one audit each. <code>createIssue</code> returns the issue number, URL, and title. <code>postThreadReply</code> returns a timestamp. Neither returns a token, and neither returns the raw provider response, which is where a token would hide if one were going to.</p>
<p>The provider calls themselves are ordinary HTTP:</p>
<pre><code class="language-javascript">export async function createIssue(token, owner, repo, { title, body }) {
  const response = await fetch(
    `https://api.github.com/repos/${owner}/${repo}/issues`,
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${token}`,
        Accept: 'application/vnd.github+json',
        'X-GitHub-Api-Version': '2022-11-28',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ title, body }),
    }
  );

  const json = await response.json();

  if (!response.ok) {
    // 403 here usually means the grant is missing the `repo` scope.
    throw new Error(
      `GitHub issue creation failed (${response.status}): ${json.message}`
    );
  }

  return { number: json.number, url: json.html_url, title: json.title };
}
</code></pre>
<h3 id="heading-step-4-read-the-channel">Step 4: Read the Channel</h3>
<p>Slack's <a href="https://docs.slack.dev/reference/methods/conversations.history"><code>conversations.history</code></a> returns clean JSON, with one gap: messages carry a user ID, never a display name. Turning those into names means a <a href="https://docs.slack.dev/reference/methods/users.info"><code>users.info</code></a> call each, which is what <code>users:read</code> was in the scope list for.</p>
<pre><code class="language-javascript">export async function readChannel(token, channelId, limit = 20) {
  const { messages } = await slackCall(token, 'conversations.history', {
    channel: channelId,
    limit: String(limit),
  });

  const authors = await resolveAuthors(
    token,
    messages.filter((m) =&gt; m.user).map((m) =&gt; m.user)
  );

  return messages
    .filter((message) =&gt; message.text)
    .map((message) =&gt; ({
      author: authors.get(message.user) ?? 'unknown',
      userId: message.user,
      text: message.text,
      ts: message.ts,
    }))
    .reverse(); // oldest first
}
</code></pre>
<p>Three small decisions in that function:</p>
<ol>
<li><p><strong>Names cost one</strong> <code>users.info</code> <strong>call per unique author.</strong> Caching them per run keeps a channel full of one person's messages from producing twenty identical lookups. A lookup that fails falls back to the user ID rather than throwing, since an unresolvable name isn't a reason to abandon the run.</p>
</li>
<li><p><strong>Messages without</strong> <code>text</code> <strong>get dropped.</strong> Channel joins and purpose changes arrive as message objects with no body, and there's nothing for the model to triage in them.</p>
</li>
<li><p><code>.reverse()</code> <strong>isn't cosmetic.</strong> Slack returns newest first. A model reading a conversation backwards will misread which message answered which.</p>
</li>
</ol>
<p>The <code>ts</code> field then does double duty. It identifies a message, which makes it both the thread anchor for replies and the key for remembering what the agent already handled:</p>
<pre><code class="language-javascript">const state = await loadState();
const processed = new Set(state[IDENTIFIER]?.processedTs ?? []);
const newMessages = messages.filter((m) =&gt; !processed.has(m.ts));
</code></pre>
<p><strong>Key that state by identifier</strong>, as the snippet does. A single flat list lets one user's processed messages hide another's, which reintroduces cross-user bleed in the one place the whole design exists to prevent.</p>
<h3 id="heading-step-5-run-the-tool-loop">Step 5: Run the Tool Loop</h3>
<p>Hand the model both tools and let it decide:</p>
<pre><code class="language-javascript">const { text } = await generateText({
  model: anthropic(process.env.MODEL),
  tools,
  stopWhen: stepCountIs(5),
  prompt: `You triage messages from a dev team's Slack channel.

Message from ${message.author}: "${message.text}"
Message timestamp (thread_ts): ${message.ts}

Decide if this message is actionable (a bug report or concrete action item) or just noise (chit-chat, join notices, already-resolved chatter).

If actionable: file a GitHub issue with a clear title and body drafted from the message, then reply in the original Slack thread (use the exact thread_ts above) with a short note and the created issue's URL.

If not actionable: do nothing and briefly say why.`,
});
</code></pre>
<p>The <strong>loop</strong> is what makes the second step possible. The model reads the message and may call <code>fileGithubIssue</code>. The AI SDK runs the tool, feeds the result back into context along with the new issue URL, and calls the model again. Now the model can reply in the thread with a URL it couldn't have known on the first pass. Then it stops.</p>
<p><code>stopWhen: stepCountIs(5)</code> caps the rounds. Without a bound, a confused model can retry a failing tool indefinitely. Five rounds is generous for two tools.</p>
<p>A deterministic version is also reasonable: classify with a structured-output call, then call both tools yourself in a fixed order when the message qualifies.</p>
<p>The fixed sequence is easier to test and gives up real flexibility. A loop lets the model skip the reply, or file without replying, and adding a third tool needs no new branching. Choose the loop when the set of actions varies per input, and the fixed sequence when it never does.</p>
<p>One note on the provider line, for accuracy about what ran. The snippet above uses <code>@ai-sdk/anthropic</code>, which suits a direct Anthropic API key. My own tests went through an OpenAI-compatible gateway, which changes only the provider construction:</p>
<pre><code class="language-javascript">import { createOpenAICompatible } from '@ai-sdk/openai-compatible';

const gateway = createOpenAICompatible({
  name: 'gateway',
  baseURL: `${process.env.GATEWAY_BASE_URL}/v1`,
  apiKey: process.env.GATEWAY_API_KEY,
});
// then: model: gateway(process.env.MODEL)
</code></pre>
<p>The tools, the loop, and the token handling are identical either way. Only the <code>model</code> argument changes.</p>
<h2 id="heading-how-to-handle-refresh-and-revocation">How to Handle Refresh and Revocation</h2>
<p><strong>Tokens end in two different ways,</strong> and only one of them is your code's problem**.** Expiry is routine and recoverable. Revocation is a decision somebody made, and the correct response is to ask for consent again.</p>
<p>The two providers in this tutorial sit at opposite ends of the range, which makes them a useful pair.</p>
<h3 id="heading-github-tokens-that-dont-expire-until-they-do">GitHub: Tokens That Don't Expire, Until They Do</h3>
<p>An OAuth App user token has no expiry timestamp. There's no refresh token to store and no refresh call to make, which is why <code>github.refresh()</code> in this project does nothing but explain itself:</p>
<pre><code class="language-javascript">async refresh() {
  throw new Error(
    'GitHub OAuth App tokens do not expire. A failure here means the ' +
      'grant was revoked — send the user through consent again.'
  );
}
</code></pre>
<p>"Does not expire" is not the same as "lasts forever," and GitHub <a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/token-expiration-and-revocation">revokes tokens</a> for several reasons worth knowing:</p>
<ul>
<li><p>The user revokes the authorization from their account settings.</p>
</li>
<li><p>The token goes unused for one year.</p>
</li>
<li><p>The token gets pushed to a public repository or gist, at which point GitHub revokes it automatically.</p>
</li>
<li><p>The app accumulates more than ten tokens for the same user and scope combination, and the oldest are revoked.</p>
</li>
</ul>
<p>The third one deserves a moment. GitHub scans public pushes for its own token formats and kills what it finds. That is a safety net, not a strategy, and the one thing it can't protect is a token in a private repository or a log file.</p>
<p><strong>GitHub Apps behave differently from OAuth Apps</strong>, which is a common source of confusion when reading GitHub's documentation. A GitHub App's user access token expires in eight hours and comes with a refresh token good for six months. If you build on GitHub Apps instead, the Slack-shaped refresh path below is the one you want.</p>
<h3 id="heading-slack-rotation-is-opt-in-and-permanent">Slack: Rotation is Opt-in and Permanent</h3>
<p>By default, a Slack user token doesn't expire either. <a href="https://docs.slack.dev/authentication/using-token-rotation">Token rotation</a> changes that, and it comes with a warning worth repeating: <strong>rotation can't be turned off once it's turned on.</strong> Enable it on a test app first.</p>
<p>With rotation on, tokens live twelve hours and arrive with a refresh token. The refresh call reuses the same endpoint as the initial exchange, with a different grant type:</p>
<pre><code class="language-javascript">async refresh(refreshToken) {
  const response = await fetch(this.tokenUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: process.env.SLACK_CLIENT_ID,
      client_secret: process.env.SLACK_CLIENT_SECRET,
    }),
  });

  const json = await response.json();
  if (!json.ok) {
    throw new Error(`Slack token refresh failed: ${json.error}`);
  }

  return normalizeSlackTokens(json.authed_user ?? json);
}
</code></pre>
<p><strong>Store the new refresh token, not just the new access token.</strong> Refresh tokens rotate too. Writing back only the access token leaves you holding a spent refresh token, and the failure arrives twelve hours later, which is a long time to wait to learn something.</p>
<p>That write-back is why <code>getAccessToken</code> calls <code>saveGrant</code> after refreshing rather than returning the token and moving on.</p>
<h3 id="heading-treat-a-dead-grant-as-a-normal-state">Treat a Dead Grant as a Normal State</h3>
<p>A revoked grant isn't an exception in the exceptional sense. Users leave, administrators tighten scopes, and people change their minds about what an agent may do.</p>
<p>The shape that works is the one <code>getAccessToken</code> already uses: catch the failure, and surface a fresh authorization link rather than a stack trace. <code>connect.js</code> with the same identifier lets the user re-consent, <code>ON CONFLICT</code> overwrites the dead row, and nothing else in your user record changes.</p>
<h2 id="heading-how-to-add-a-second-provider">How to Add a Second Provider</h2>
<p><strong>A second provider costs one OAuth app, one entry in the providers object, and one tool.</strong> Keeping identity in a single string is what buys the discount.</p>
<p>The agent has used two providers all along. Worth noticing is what the second one didn't require: no second identity, no second consent server, and no second token table.</p>
<pre><code class="language-javascript">export const providers = {
  slack: { /* ... */ },
  github: { /* ... */ },
};
</code></pre>
<p>Google Calendar as a third means a third entry with its own <code>authorizeUrl</code>, <code>tokenUrl</code>, scopes, and <code>exchangeCode</code>. The consent server loops over <code>Object.keys(providers)</code>, so it picks the new one up without modification. The store already keys on <code>(identifier, provider)</code>, so it needs no migration. Then one more tool:</p>
<pre><code class="language-javascript">const createCalendarEvent = tool({
  description: 'Create a calendar event',
  inputSchema: z.object({ summary: z.string(), start: z.string() }),
  execute: async ({ summary, start }) =&gt; {
    const token = await getAccessToken(identifier, 'google-calendar');
    // ...one more provider call
  },
});
</code></pre>
<p>The identifier doesn't change, your user table doesn't change, and the model's view of the world grows by exactly one tool.</p>
<p>The cost that doesn't scale down is the <strong>provider-specific knowledge.</strong> Each new provider brings its own scope vocabulary, its own error format, and its own answer to whether tokens expire. Slack and GitHub disagreed on all three, and a third will disagree differently. The registry pattern contains that knowledge in one object per provider rather than spreading it through your agent, but it doesn't make the knowledge unnecessary.</p>
<p><strong>One caveat on consent:</strong> A grant is per user, per provider. Alice connecting Slack but not Calendar means her calendar tool calls fail, and failure is correct there, since she never consented. Treat it as a prompt to connect rather than an error, a point the Failure Modes section returns to.</p>
<h2 id="heading-full-walkthrough">Full Walkthrough</h2>
<p>Clone the repository, install, and fill in <code>.env</code>:</p>
<pre><code class="language-bash">git clone https://github.com/saif-shines/channel-watcher-agent.git
cd channel-watcher-agent
npm install
cp .env.example .env
# fill in both client IDs and secrets, the encryption key, channel ID, repo
</code></pre>
<p>Then connect. The command starts the callback server and prints one link per unconnected provider:</p>
<pre><code class="language-bash">npm run connect
</code></pre>
<pre><code class="language-text">[slack] authorize as "channel-watcher-agent":
https://slack.com/oauth/v2/authorize?client_id=123.456&amp;user_scope=channels%3Ahistory%2Cchat%3Awrite%2Cusers%3Aread&amp;redirect_uri=https%3A%2F%2Flocalhost%3A3000%2Fcallback&amp;state=1159699dbf1a808fd33ba31c7b643505
[slack] connected.

[github] authorize as "channel-watcher-agent":
https://github.com/login/oauth/authorize?client_id=Iv1.abc&amp;scope=repo&amp;redirect_uri=https%3A%2F%2Flocalhost%3A3000%2Fcallback&amp;state=e6d13461099c391367266235f8313630
[github] connected.

All providers connected. Run: node src/index.js channel-watcher-agent
</code></pre>
<p>Open each link, consent, and the tab confirms. The state parameter in those URLs is checked on the way back. A callback carrying anything else gets a 400 and never reaches the token exchange.</p>
<p>Then run the agent against a channel holding ordinary chatter:</p>
<pre><code class="language-bash">node src/index.js
</code></pre>
<p>The output from that run, against a channel with three unremarkable messages:</p>
<pre><code class="language-text">[channel-watcher-agent] 3 messages fetched, 3 new.

--- Alex: "Sending draft message" ---
The message "Sending draft message" is noise — it appears to be a test or
accidental send, not a bug report or concrete action item.

...
</code></pre>
<p><strong>No tools called, and no issues filed.</strong> The negative case matters more than it looks. An agent with write access that can't say no is a liability, and a run over ordinary chatter is the cheapest available test of its restraint.</p>
<p>Now post an actual bug report in the channel:</p>
<blockquote>
<p>hey the /export endpoint is timing out for any file over 50MB, been happening since yesterday's deploy</p>
</blockquote>
<p>Run the agent again, and the state file keeps the earlier messages from being triaged twice.</p>
<p><strong>Authorship is the part that matters.</strong> The GitHub account behind the identifier opens the issue, using that user's own OAuth grant, not a shared bot. The Slack reply comes from that person too. Revoke their access and the next run fails at <code>getAccessToken</code>, which is the correct outcome.</p>
<h3 id="heading-what-changes-for-a-second-user">What Changes for a Second User</h3>
<p>The identifier comes from the command line, so isolation is testable without building a login first:</p>
<pre><code class="language-bash">node src/index.js                     # the identifier you already authorized
node src/index.js alice@example.com   # a different user entirely
</code></pre>
<p>The second command never reads the channel. It stops:</p>
<pre><code class="language-text">[slack] no grant for "alice@example.com".
Connect it first: node src/connect.js alice@example.com
</code></pre>
<p><strong>The refusal is the whole point.</strong> Nothing about the agent changed between the two commands. Same providers, tools, and code. Only the identifier differed, and Alice hasn't consented, so no row exists to decrypt and the run stops before touching Slack.</p>
<p>A shared-bot version behaves differently. The second command would read the channel and file an issue as the bot, because no per-user grant was ever involved.</p>
<p>Once Alice consents, everything downstream follows her grant. <code>readGrant</code> returns her row. <code>getAccessToken</code> decrypts her token. The Slack read returns the channels she can see, and her GitHub account authors the issue.</p>
<p>Production replaces <code>argv</code> with a session lookup:</p>
<pre><code class="language-javascript">const identifier = await lookupIdentifier(session.userId);
</code></pre>
<h3 id="heading-testing-the-isolation-without-credentials">Testing the Isolation Without Credentials</h3>
<p>The repository includes a test suite that replaces <code>fetch</code> with stand-in Slack and GitHub endpoints, so the request building, response parsing, storage, and refresh logic all run without a single OAuth app registered:</p>
<pre><code class="language-bash">npm test
</code></pre>
<p>Three of those tests are worth naming, because they check the claims this tutorial makes rather than the code's internals:</p>
<ul>
<li><p><strong>Slack exchange keeps the user token and discards the bot token.</strong> The fixture returns both. The test asserts the stored value is the <code>xoxp-</code> one.</p>
</li>
<li><p><strong>Two users get two different tokens from identical tool inputs.</strong> Same <code>text</code>, same <code>thread_ts</code>, two identifiers, two different <code>Authorization</code> headers reaching the provider.</p>
</li>
<li><p><strong>A tool built for an unconnected user fails instead of falling back.</strong> It also asserts that zero provider calls were attempted, since failing after leaking a request isn't much of a failure.</p>
</li>
</ul>
<p>Tests that pass on the first run are worth distrusting, so I checked these by breaking the code on purpose. Substituting the bot token for the user token, ignoring the identifier in <code>buildTools</code>, and exposing <code>identifier</code> in the model-visible schema each fail at least one test.</p>
<h2 id="heading-how-to-apply-the-pattern-to-other-use-cases">How to Apply the Pattern to Other Use Cases</h2>
<p><strong>Nothing in the pattern is specific to Slack triage.</strong> The shape is: read from one app, decide with a model, write to another app, all as one user.</p>
<p>Swapping the providers produces a different product:</p>
<table>
<thead>
<tr>
<th>Read from</th>
<th>Write to</th>
<th>Result</th>
</tr>
</thead>
<tbody><tr>
<td>Slack</td>
<td>GitHub</td>
<td>Triage channel chatter into issues, as in this tutorial</td>
</tr>
<tr>
<td>Gmail</td>
<td>Linear</td>
<td>Turn support email into tracked work</td>
</tr>
<tr>
<td>Google Calendar</td>
<td>Notion</td>
<td>Meeting prep notes, drafted before the meeting</td>
</tr>
<tr>
<td>Zendesk</td>
<td>Salesforce</td>
<td>Log support signals against the right account</td>
</tr>
<tr>
<td>GitHub</td>
<td>Slack</td>
<td>A digest of what changed, in the channel that cares</td>
</tr>
</tbody></table>
<p>Every row uses the same three pieces: a provider entry, a token lookup by identifier, and a tool. Only three things change: the OAuth app registrations, the API calls inside <code>execute</code>, and the input you write for the model.</p>
<p><strong>The input is where your product lives.</strong> OAuth is plumbing. Deciding which messages deserve an issue, and what the issue should say, is judgment, and judgment is the part worth your weeks.</p>
<p>The same code supports two deployment shapes:</p>
<ul>
<li><p><strong>Internal team agent:</strong> The identifier is the teammate who triggered the run. Runs on a schedule or a command.</p>
</li>
<li><p><strong>Customer-facing agent:</strong> The identifier comes from your tenant and user records. Runs on customer data, inside customer accounts.</p>
</li>
</ul>
<p>The code stays identical. The consequences of a wrong identifier do not.</p>
<h2 id="heading-what-went-wrong-when-i-built-this">What Went Wrong When I Built This</h2>
<p>These are problems I hit while building the project, in roughly the order they showed up. If you hit the same ones, the fix is usually small.</p>
<h3 id="heading-slack-wont-save-the-redirect-url">Slack Won't Save the Redirect URL</h3>
<p>The symptom arrives before any code runs: the Slack app configuration page refuses to accept <code>http://localhost:3000/callback</code>.</p>
<p>Slack requires HTTPS on redirect URLs with no exception for <code>localhost</code>. Issue a local certificate with <code>mkcert</code>, register the <code>https://</code> form, and point <code>TLS_CERT_PATH</code> and <code>TLS_KEY_PATH</code> at the files it wrote. GitHub accepts either scheme, so the same HTTPS URL works for both apps.</p>
<h3 id="heading-the-browser-warns-that-the-certificate-isnt-trusted">The Browser Warns That the Certificate Isn't Trusted</h3>
<p><code>mkcert -install</code> is the step that adds mkcert's local authority to your system trust store, and skipping it leaves a certificate no browser recognises.</p>
<p>Running it once fixes every certificate mkcert issues afterwards. A self-signed certificate made with <code>openssl</code> will always warn, since nothing trusts it.</p>
<h3 id="heading-the-redirect-uri-doesnt-match">The Redirect URI Doesn't Match</h3>
<p>Both providers compare the <code>redirect_uri</code> you send against the one registered with the app, and the comparison is exact. A trailing slash, <code>127.0.0.1</code> in place of <code>localhost</code>, <code>http</code> where you registered <code>https</code>, or a different port all fail.</p>
<p>The error arrives before consent, on the provider's own page, which at least makes it easy to spot. Keep <code>OAUTH_REDIRECT_URI</code> as the single source and pass it in both the authorize URL and the token exchange, as the provider registry does.</p>
<h3 id="heading-the-callback-port-is-already-in-use">The Callback Port is Already in Use</h3>
<p><code>connect.js</code> binds the port from <code>OAUTH_REDIRECT_URI</code>, and port 3000 is popular. An unhandled <code>EADDRINUSE</code> produces a stack trace that says nothing about OAuth, so the project catches it and says what to do instead.</p>
<p>Changing the port means changing it in three places: <code>.env</code>, the Slack app's redirect URLs, and the GitHub app's callback URL. Missing one produces the previous failure.</p>
<h3 id="heading-the-state-check-rejects-a-legitimate-callback">The State Check Rejects a Legitimate Callback</h3>
<p>State values live in memory and are deleted once used. Restarting <code>connect.js</code> after opening the link, or refreshing the callback tab, both produce a state that's no longer in the map.</p>
<p>Both are correct rejections. Generate a fresh link and start again.</p>
<h3 id="heading-tool-calls-return-permission-errors-or-empty-results">Tool Calls Return Permission Errors or Empty Results</h3>
<p>A missing scope or a revoked grant causes both.</p>
<p>GitHub answers <code>403</code> with "Resource not accessible" when the grant lacks <code>repo</code>. Slack answers <code>200</code> with <code>ok: false</code> and an error like <code>missing_scope</code>. Fix the scope list, then send the user through consent again, since an existing grant doesn't gain scopes retroactively.</p>
<p><strong>A partially-scoped grant fails at the point of use rather than at connection time</strong>, which is what makes the symptom look mysterious. The consent screen succeeded, the token stored fine, and the failure arrives during a tool call hours later.</p>
<h3 id="heading-the-agent-reads-channels-it-shouldnt">The Agent Reads Channels it Shouldn't</h3>
<p>The single most likely cause is storing <code>json.access_token</code> instead of <code>json.authed_user.access_token</code> during the Slack exchange. Both are strings, both are truthy, and both work (one works as the app rather than the person).</p>
<p>The tell is the scope of what comes back. A user token returns only that person's channels. If <code>conversations.history</code> returns a channel the current user was never in, a bot token is in the store.</p>
<h3 id="heading-a-tool-call-runs-as-the-wrong-user">A Tool Call Runs as the Wrong User</h3>
<p>Passing a token or identifier belonging to somebody else will do the wrong thing correctly.</p>
<p>Two habits prevent it. Resolve the identifier server-side after authenticating the caller, never from client input. Then take the identifier as a closure argument in <code>buildTools</code> and let each <code>execute</code> fetch its own token, so no code path can pass a stray credential.</p>
<h3 id="heading-refresh-works-once-and-then-stops">Refresh Works Once and Then Stops</h3>
<p>Refresh tokens rotate. A refresh that writes back the new access token but keeps the old refresh token succeeds immediately and fails on the following cycle, which puts twelve hours between the bug and its symptom.</p>
<p><code>saveGrant</code> takes the whole normalized token object for this reason. Write back everything the refresh returned.</p>
<h3 id="heading-the-agent-files-duplicate-issues">The Agent Files Duplicate Issues</h3>
<p>Two causes. A missing or unwritten state file makes every run triage everything again. Or <code>stopWhen</code> allows enough rounds for a confused model to retry a tool that already succeeded.</p>
<p>Check the state file first. Then check whether the tool's return value clearly signals success, because an ambiguous result invites a retry.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You've built an agent that reads a Slack channel, judges which messages describe real work, files GitHub issues for those, and closes the loop with a threaded reply. Every call ran as one specific user's own OAuth grant, through an OAuth flow and a token store you wrote yourself.</p>
<p>Five ideas carry over to any provider:</p>
<ul>
<li><p><strong>A tool is an API call plus an explanation for a model</strong>, and the explanation is most of the work.</p>
</li>
<li><p><strong>The identifier replaces the token in your agent code.</strong> Everything above one small function handles a reference to a user rather than a credential, so tokens never reach your model inputs or your logs.</p>
</li>
<li><p><strong>Connection time and runtime are separate flows.</strong> Consent happens once per user, per app. Runtime resolves an identifier and fetches a token late.</p>
</li>
<li><p><strong>Authorization stays yours.</strong> A token store answers which tokens belong to an identifier. Whether a caller may act as that identifier is a question only your code can answer.</p>
</li>
<li><p><strong>Multi-provider support is a registry problem, not an architecture problem</strong>, once identity lives in one string.</p>
</li>
</ul>
<p>The detail that carries the most weight is also the smallest: <code>authed_user.access_token</code> rather than <code>access_token</code>. One property access decides whether your agent respects the permissions your workspace already has or quietly routes around them.</p>
<p>From here, keep the shape and swap the providers. Point the read half at Gmail and the write half at Linear, then rewrite the input for the model. The identity plumbing doesn't change.</p>
<p>The full source is at <a href="https://github.com/saif-shines/channel-watcher-agent">github.com/saif-shines/channel-watcher-agent</a>.</p>
<p><em>This write-up reconstructs what we learned building</em> <a href="https://www.scalekit.com/"><em>Scalekit</em></a><em>, a hosted version of the token vault you just built.</em></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ CSRF from Scratch: Browser Mechanics, Attacks, and Spring Security Implementation [Full Handbook] ]]>
                </title>
                <description>
                    <![CDATA[ If you've ever built a web application or configured Spring Security, you've almost certainly encountered Cross-Site Request Forgery (CSRF). In my previous guide, How OAuth 2.0 Works: A Practical Guid ]]>
                </description>
                <link>https://www.freecodecamp.org/news/csrf-from-scratch-browser-mechanics-attacks-and-spring-security-implementation-handbook/</link>
                <guid isPermaLink="false">6a74fe284ef5707f2879423d</guid>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                    <category>
                        <![CDATA[ csrf ]]>
                    </category>
                
                    <category>
                        <![CDATA[ spring-boot ]]>
                    </category>
                
                    <category>
                        <![CDATA[ spring security ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Java ]]>
                    </category>
                
                    <category>
                        <![CDATA[ authentication ]]>
                    </category>
                
                    <category>
                        <![CDATA[ cookies ]]>
                    </category>
                
                    <category>
                        <![CDATA[ handbook ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Ashutosh Krishna ]]>
                </dc:creator>
                <pubDate>Thu, 06 Aug 2026 21:35:36 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/20e903c5-9011-4f14-b714-974e32d43f3c.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you've ever built a web application or configured Spring Security, you've almost certainly encountered Cross-Site Request Forgery (CSRF).</p>
<p>In my previous guide, <a href="https://medium.com/@ashutoshkrris/how-oauth-2-0-works-a-practical-guide-for-backend-developers-630977209476"><strong>How OAuth 2.0 Works: A Practical Guide for Backend Developers</strong></a>, I briefly touched on the mysterious <code>state</code> parameter and noted that its core purpose is protecting authorization flows against CSRF attacks.</p>
<p>At the time, we treated CSRF as a quick prerequisite concept. Today, we're taking a much deeper dive.</p>
<p>Perhaps you were building a REST API in Spring Boot, ran into unexpected HTTP 403 Forbidden errors on every <code>POST</code> request, and "fixed" it by adding <code>.csrf(csrf -&gt; csrf.disable())</code> to your Security Filter Chain.</p>
<p>Most tutorials treat CSRF as a checkbox item or a framework toggle. They immediately jump to code:</p>
<pre><code class="language-java">// What most tutorials show on line 1:
http.csrf(Customizer.withDefaults());
</code></pre>
<p>Starting with framework configuration hides how web security actually operates. Spring Security doesn't invent security rules out of thin air. It responds to the fundamental mechanics of web browsers, HTTP protocols, and cookies.</p>
<p>In this handbook, we'll take a bottom-up, first-principles approach. We won't talk about Spring Security until we've thoroughly explored browsers, HTTP headers, session management, and the underlying mechanics of Cross-Site Request Forgery.</p>
<p>By the end of this guide, you'll understand:</p>
<ul>
<li><p>Why browsers automatically attach credentials to outgoing requests.</p>
</li>
<li><p>Why that automatic behavior creates a fundamental vulnerability.</p>
</li>
<li><p>Why attackers never need to steal or read your cookies to exploit CSRF.</p>
</li>
<li><p>Why Same Origin Policy (SOP) and CORS don't prevent CSRF.</p>
</li>
<li><p>How modern defenses, from CSRF Tokens to <code>SameSite</code> cookies, work under the hood.</p>
</li>
<li><p>How Spring Security implements these defenses internally and how to configure them effectively.</p>
</li>
</ul>
<p>Let’s begin by stripping away frameworks and looking at how the web actually works.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-the-problem-before-csrf">The Problem Before CSRF</a></p>
</li>
<li><p><a href="#heading-why-browsers-automatically-send-cookies">Why Browsers Automatically Send Cookies</a></p>
</li>
<li><p><a href="#heading-when-automatic-cookies-become-dangerous">When Automatic Cookies Become Dangerous</a></p>
</li>
<li><p><a href="#heading-visualize-the-attack">Visualize the Attack</a></p>
</li>
<li><p><a href="#heading-why-the-browser-isnt-broken">Why the Browser Isn't Broken</a></p>
</li>
<li><p><a href="#heading-same-origin-policy-sop">Same Origin Policy (SOP)</a></p>
</li>
<li><p><a href="#heading-why-cors-does-not-prevent-csrf">Why CORS Does NOT Prevent CSRF</a></p>
</li>
<li><p><a href="#heading-safe-methods-and-state-mutation">Safe Methods and State Mutation</a></p>
</li>
<li><p><a href="#heading-csrf-tokens-synchronizer-token-pattern">CSRF Tokens (Synchronizer Token Pattern)</a></p>
</li>
<li><p><a href="#heading-double-submit-cookie-pattern">Double Submit Cookie Pattern</a></p>
</li>
<li><p><a href="#heading-samesite-cookies">SameSite Cookies</a></p>
</li>
<li><p><a href="#heading-origin-and-referer-headers">Origin and Referer Headers</a></p>
</li>
<li><p><a href="#heading-jwt-and-csrf-the-token-storage-dilemma">JWT and CSRF: The Token Storage Dilemma</a></p>
</li>
<li><p><a href="#heading-spring-security-csrf-internals">Spring Security CSRF Internals</a></p>
</li>
<li><p><a href="#heading-implement-csrf-protection-yourself">Implement CSRF Protection Yourself</a></p>
</li>
<li><p><a href="#heading-testing-csrf-protections">Testing CSRF Protections</a></p>
</li>
<li><p><a href="#heading-common-misconceptions">Common Misconceptions</a></p>
</li>
<li><p><a href="#heading-production-best-practices-checklist">Production Best Practices Checklist</a></p>
</li>
<li><p><a href="#heading-final-summary-amp-defense-matrix">Final Summary &amp; Defense Matrix</a></p>
</li>
</ul>
<h2 id="heading-the-problem-before-csrf">The Problem Before CSRF</h2>
<p>To understand security, we must first understand state.</p>
<p>The Hypertext Transfer Protocol (HTTP) is inherently <strong>stateless</strong>. This means that if Alice sends an HTTP request to <code>travelbuddy.com</code> (our example) at 10:00 AM, and sends another HTTP request to <code>travelbuddy.com</code> at 10:01 AM, the server treats those two requests as completely isolated, unrelated events.</p>
<img src="https://cdn.hashnode.com/uploads/covers/61c1acb4a90dea775da8262b/c5c24ee5-450d-4252-8e79-3744f9814fbd.png" alt="Sequence diagram showing Alice’s browser making a successful GET request to the TravelBuddy Server, followed 1 minute later by a second GET request that returns a 401 Unauthorized error." style="display:block;margin:0 auto" width="1071" height="860" loading="lazy">

<p>Without a mechanism to remember Alice between requests, Alice would have to send her username and password inside <em>every single HTTP request</em> she makes. That would be horrific for both user experience and performance.</p>
<p>Before session mechanisms were standard, developers tried passing credentials via query parameters or basic authentication headers on every click. This led to credential exposure in server logs, browser histories, and URL shares.</p>
<h3 id="heading-how-do-sessions-and-cookies-solve-this">How Do Sessions and Cookies Solve This?</h3>
<p>To solve this, web engineers introduced the concept of <strong>Server-Side Sessions</strong> and <strong>HTTP Cookies</strong>.</p>
<p>When Alice logs into <code>TravelBuddy</code> by sending her username and password via a POST request to <code>https://travelbuddy.com/login</code>, the server verifies her credentials. Instead of asking Alice to log in again on the next page, the server creates a <strong>Session</strong> in its memory (or in a database/Redis cache) and assigns it a unique, unpredictable identifier: a <strong>Session ID</strong>.</p>
<p>The server then sends this Session ID back to Alice’s browser using a special HTTP response header: <code>Set-Cookie</code>.</p>
<pre><code class="language-plaintext">HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: JSESSIONID=abc123xyz789; Path=/; Secure; HttpOnly
</code></pre>
<p>When Alice’s browser receives this response, it sees the <code>Set-Cookie</code> header. It extracts <code>JSESSIONID=abc123xyz789</code> and stores it inside its internal storage unit: the <strong>Browser Cookie Jar</strong>.</p>
<p>Now, Alice is "logged in". The server remembers her via that session record, and the browser holds the key (<code>JSESSIONID</code>) to that session.</p>
<h2 id="heading-why-browsers-automatically-send-cookies">Why Browsers Automatically Send Cookies</h2>
<p>Now we arrive at the pivotal design choice made in the early days of the web.</p>
<p>Once the browser stores <code>JSESSIONID=abc123xyz789</code> in its cookie jar for the domain <code>travelbuddy.com</code>, how does that cookie get sent back to the server on subsequent requests?</p>
<p>Does the developer have to write custom JavaScript to attach the cookie? <strong>No.</strong></p>
<p>Browsers are explicitly designed to handle cookie management <strong>automatically</strong>.</p>
<h3 id="heading-the-request-lifecycle-and-automatic-cookie-attachment">The Request Lifecycle and Automatic Cookie Attachment</h3>
<p>Every time Alice's browser prepares an HTTP request to <code>https://travelbuddy.com</code> (whether caused by Alice clicking a link, submitting an HTML form, or JavaScript triggering a <code>fetch()</code> call), the browser follows this exact process:</p>
<ol>
<li><p><strong>URL Inspection:</strong> The browser examines the destination URL (for example, <code>https://travelbuddy.com/api/connections</code>).</p>
</li>
<li><p><strong>Cookie Jar Lookup:</strong> The browser scans its cookie jar for any stored cookies whose domain and path match <code>travelbuddy.com</code>.</p>
</li>
<li><p><strong>Validation Check:</strong> It verifies if the cookie has expired, and if flags like <code>Secure</code> (requires HTTPS) are respected.</p>
</li>
<li><p><strong>Header Injection:</strong> If valid cookies match, the browser automatically injects a <code>Cookie</code> header into the outgoing HTTP request payload.</p>
</li>
</ol>
<p>Here's what the outgoing request looks like as it leaves Alice's machine:</p>
<pre><code class="language-shell">POST /api/connections/add HTTP/1.1
Host: travelbuddy.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
Accept: text/html,application/xhtml+xml
Cookie: JSESSIONID=abc123xyz789
Content-Type: application/x-www-form-urlencoded

service=SkyScanner
</code></pre>
<p>Notice something critical: <strong>Neither Alice nor any custom frontend JavaScript explicitly attached</strong> <code>Cookie: JSESSIONID=abc123xyz789</code><strong>.</strong></p>
<p>The browser's internal engine attached it automatically before sending the byte stream across the network. From the server's perspective, receiving <code>Cookie: JSESSIONID=abc123xyz789</code> is proof that the request originated from an authenticated session belonging to Alice.</p>
<p>This automatic behavior is convenient. It makes web browsing seamless across page reloads and link navigation. But as we'll soon see, this convenience leaves a backdoor wide open.</p>
<h2 id="heading-when-automatic-cookies-become-dangerous">When Automatic Cookies Become Dangerous</h2>
<p>Is automatic cookie inclusion a vulnerability by itself?</p>
<p><strong>No.</strong> If Alice only visits <code>travelbuddy.com</code>, automatic cookie inclusion works exactly as intended.</p>
<p>The vulnerability emerges because of a simple web reality: <strong>Alice visits multiple websites in the same browser session.</strong></p>
<h3 id="heading-enter-evilcom">Enter <code>evil.com</code></h3>
<p>Suppose Alice is logged into <code>TravelBuddy</code> in Tab 1. Her session cookie (<code>JSESSIONID=abc123xyz789</code>) sits safely inside her browser's cookie jar for <code>travelbuddy.com</code>.</p>
<p>In Tab 2, Alice visits an unrelated website: <code>https://evil.com</code> (perhaps she clicked a link in a phishing email or a forum post).</p>
<p><code>evil.com</code> is controlled by an attacker. The attacker knows that <code>TravelBuddy</code> has a feature located at <code>POST</code> <code>[https://travelbuddy.com/api/connections/add</code> that connects third-party services. The attacker wants to trick Alice into connecting the attacker's malicious service to her account.</p>
<p>The attacker embeds the following hidden HTML form inside the HTML page served by <code>evil.com</code>:</p>
<pre><code class="language-html">&lt;!-- Hosted on https://evil.com/win-a-car.html --&gt;
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;body&gt;
  &lt;h1&gt;You won a free trip! Click below to claim.&lt;/h1&gt;
  
  &lt;!-- Hidden Form targeting TravelBuddy --&gt;
  &lt;form id="maliciousForm" action="https://travelbuddy.com/api/connections/add" method="POST"&gt;
    &lt;input type="hidden" name="service" value="MaliciousAttackerService" /&gt;
  &lt;/form&gt;

  &lt;script&gt;
    // Automatically submit the form as soon as the page loads
    document.getElementById('maliciousForm').submit();
  &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<h3 id="heading-walkthrough-of-the-attack-execution">Walkthrough of the Attack Execution</h3>
<p>Let's trace step-by-step what happens when Alice opens <code>https://evil.com/win-a-car.html</code>:</p>
<ol>
<li><p>Alice's browser fetches and parses HTML from <code>evil.com</code>.</p>
</li>
<li><p>The browser encounters the <code>&lt;script&gt;</code> tag and executes <code>document.getElementById('maliciousForm').submit()</code>.</p>
</li>
<li><p>The browser prepares an outgoing <code>POST</code> request targeting <code>https://travelbuddy.com/api/connections/add</code>.</p>
</li>
<li><p>The browser looks at the target destination: <code>travelbuddy.com</code>.</p>
</li>
<li><p>The browser checks its Cookie Jar: <em>"Do I have any active cookies for</em> <code>travelbuddy.com</code><em>?"</em></p>
</li>
<li><p><strong>Yes!</strong> It finds <code>JSESSIONID=abc123xyz789</code> (Alice's active session cookie from Tab 1).</p>
</li>
<li><p>The browser automatically injects <code>Cookie: JSESSIONID=abc123xyz789</code> into the outgoing request payload heading to <code>travelbuddy.com</code>.</p>
</li>
<li><p>The request lands on the <code>TravelBuddy</code> Spring Boot backend server.</p>
</li>
</ol>
<h3 id="heading-the-servers-perspective">The Server's Perspective</h3>
<p>Here's what the <code>TravelBuddy</code> backend sees when processing the request:</p>
<pre><code class="language-shell">POST /api/connections/add HTTP/1.1
Host: travelbuddy.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
Content-Type: application/x-www-form-urlencoded
Cookie: JSESSIONID=abc123xyz789

service=MaliciousAttackerService
</code></pre>
<p>The <code>TravelBuddy</code> server checks the <code>Cookie</code> header. It validates <code>JSESSIONID=abc123xyz789</code> against its session store. The session is valid: it belongs to Alice!</p>
<p>The server assumes: <em>"Alice sent a POST request to add</em> <code>MaliciousAttackerService</code><em>. She is authenticated, so I will grant this request."</em></p>
<p>The server updates Alice's account state. <code>MaliciousAttackerService</code> is now connected to her profile.</p>
<h3 id="heading-the-core-realization-of-csrf">The Core Realization of CSRF</h3>
<p>Take a step back and examine what just happened:</p>
<ol>
<li><p><strong>The attacker NEVER saw or stole Alice’s session cookie.</strong> The attacker on <code>evil.com</code> can't read cookies belonging to <code>travelbuddy.com</code> due to browser isolation rules.</p>
</li>
<li><p><strong>The attacker did NOT break encryption.</strong> HTTPS was active the entire time.</p>
</li>
<li><p><strong>The attacker simply induced Alice's browser to make a request.</strong> The browser, faithfully executing its automatic cookie attachment rules, provided the credentials on behalf of the attacker. You could say the attacker got caught with their hand in Alice's cookie jar!</p>
</li>
</ol>
<p>This is <strong>Cross-Site Request Forgery in action</strong>: An attacker tricks a victim's browser into executing an unwanted, state-changing HTTP request to a trusted site where the victim is currently authenticated.</p>
<h2 id="heading-visualize-the-attack">Visualize the Attack</h2>
<p>Visualizing the interaction between Alice, the browser, <code>evil.com</code>, and <code>TravelBuddy</code> makes the underlying request flow clear.</p>
<h3 id="heading-1-the-complete-csrf-sequence">1. The Complete CSRF Sequence</h3>
<img src="https://cdn.hashnode.com/uploads/covers/61c1acb4a90dea775da8262b/0431c42a-815b-482c-900e-7985c3f5ace1.png" alt="Sequence diagram illustrating a Cross-Site Request Forgery (CSRF) attack where an attacker site (evil.com) uses an auto-submitting form to trick a logged-in user’s browser into sending an authenticated request to travelbuddy.com." style="display:block;margin:0 auto" width="2614" height="2116" loading="lazy">

<p>The attack unfolds across three distinct phases involving four main actors: Alice, her web browser, the TravelBuddy backend server, and the attacker site running on <code>evil.com</code>.</p>
<p>In the first phase, Alice authenticates with TravelBuddy. She submits her login credentials through her browser, which sends a POST request to the TravelBuddy backend. The backend verifies her credentials and responds with an HTTP 200 OK status alongside a <code>Set-Cookie</code> header containing <code>JSESSIONID=abc123xyz</code>.</p>
<p>Upon receiving this response, Alice's browser automatically saves this session identifier inside its cookie jar for the <code>travelbuddy.com</code> domain.</p>
<p>In the second phase, the attacker sets a trap. While keeping her TravelBuddy tab active, Alice opens a second browser tab and visits <code>evil.com</code>. Her browser requests the page <code>win-a-car.html</code> from <code>evil.com</code>. In response, <code>evil.com</code> serves an HTML document containing an invisible form targeting TravelBuddy, paired with an embedded JavaScript script designed to trigger immediately upon loading.</p>
<p>In the final phase, the attack executes automatically. The malicious JavaScript on <code>evil.com</code> calls <code>form.submit()</code>, commanding the browser to send a POST request to <code>https://travelbuddy.com/api/connections/add</code>.</p>
<p>Before sending the request across the network, the browser checks its cookie jar for any cookies matching <code>travelbuddy.com</code>. It finds Alice's active session cookie and automatically attaches <code>Cookie: JSESSIONID=abc123xyz</code> to the outgoing request payload. The TravelBuddy server receives the request, inspects the valid session cookie, assumes Alice intended to perform this action, and attaches the attacker's service to her account.</p>
<h3 id="heading-2-browser-decision-tree-during-outgoing-request">2. Browser Decision Tree during Outgoing Request</h3>
<p>When any request is fired, the browser follows a decision path regarding cookie attachment:</p>
<img src="https://cdn.hashnode.com/uploads/covers/61c1acb4a90dea775da8262b/59876902-2a43-4082-81f8-83e2c198e0c6.png" alt="Flowchart showing how a web browser automatically checks its Cookie Jar and attaches valid cookies to an outgoing HTTP request targeting travelbuddy.com." style="display:block;margin:0 auto" width="1168" height="2635" loading="lazy">

<p>This diagram outlines the automatic evaluation loop executed by a browser whenever an HTTP request is triggered from any tab or script.</p>
<p>The process begins as soon as an outgoing HTTP request is initiated. The browser first inspects the target URL to extract the destination domain, such as <code>travelbuddy.com</code>. Once the domain is identified, the browser queries its internal cookie storage to check whether any cookies are mapped to that target domain. If no matching cookies exist, the browser immediately skips credential attachment and dispatches the raw HTTP request across the network.</p>
<p>If matching cookies are found, the browser evaluates their validity. It checks whether the cookies have expired, whether the request path matches the path defined in the cookie, and whether security constraints like the <code>Secure</code> HTTPS flag are satisfied. If any validation check fails, the cookie is discarded, and the request proceeds without credentials. But if the cookies are valid and active, the browser constructs a <code>Cookie</code> header containing the stored session key and attaches it to the outgoing HTTP request payload before dispatching it across the network to the server.</p>
<h3 id="heading-3-session-and-cookie-lifecycle-state-diagram">3. Session and Cookie Lifecycle State Diagram</h3>
<img src="https://cdn.hashnode.com/uploads/covers/61c1acb4a90dea775da8262b/b244543c-2ad6-455c-949d-eefe219eb4a0.png" alt="State diagram showing a user transitioning from an unauthenticated state to an authenticated state with automatic cookie management, and how maintaining an active session leaves the application vulnerable to CSRF when visiting a malicious site." style="display:block;margin:0 auto" width="902" height="2096" loading="lazy">

<p>This state diagram tracks how a user moves between secure, authenticated, and vulnerable conditions during a web session.</p>
<p>When a user first opens their web browser, they begin in an unauthenticated state with no cookies stored for the target application. Submitting valid credentials via a login form transitions the user into an authenticated state. Inside this authenticated state, the server issues a <code>Set-Cookie</code> header, causing the browser to save the session ID in its cookie storage. For every subsequent request directed to that application, the browser automatically attaches the cookie while keeping the user logged in.</p>
<p>A vulnerability window opens when an authenticated user opens a second tab and navigates to an untrusted website while their application session remains active. This action shifts the browser context into a state vulnerable to Cross-Site Request Forgery. If the untrusted site fires a cross-site request back to the original application, the browser's automatic cookie attachment mechanism triggers, executing an unauthorized state change on the server. The cycle ends only when the user logs out or the server session expires, returning the client to the initial unauthenticated state.</p>
<h2 id="heading-why-the-browser-isnt-broken">Why the Browser Isn't Broken</h2>
<p>When developers first grasp CSRF, their immediate reaction is often: <em>"This is a terrible browser flaw! Why don't browser vendors fix this by disabling automatic cookie sending entirely?"</em></p>
<p>To understand why browsers behave this way, we must look at <strong>Web Compatibility</strong> and a concept known in security engineering as <strong>Ambient Authority</strong>.</p>
<h3 id="heading-the-principle-of-ambient-authority">The Principle of Ambient Authority</h3>
<p>When a system automatically applies a user's identity or credentials to every action without requiring explicit user intent for <em>that specific action</em>, the system is using <strong>ambient authority</strong>.</p>
<p>HTTP cookies are an ambient credential. If you're logged in, every request carrying a destination URL automatically includes your credential.</p>
<h3 id="heading-why-browser-vendors-dont-just-fix-it">Why Browser Vendors Don't Just "Fix" It</h3>
<p>The World Wide Web was created as a web of interconnected hypermedia documents. Cross-site interactions are a fundamental design feature of the web, not an accidental bug:</p>
<ul>
<li><p><strong>Images and assets:</strong> When <code>news.com</code> embeds an image hosted on <code>cdn.com</code>, your browser makes a cross-site request to <code>cdn.com</code>.</p>
</li>
<li><p><strong>Cross-site form submissions:</strong> In the early web (and still today), paying with PayPal meant an HTML form on <code>e-commerce.com</code> submitted data directly to <code>paypal.com</code>.</p>
</li>
<li><p><strong>Hyperlinks:</strong> Clicking a link on <code>google.com</code> takes you to <code>wikipedia.org</code> via a cross-site GET request.</p>
</li>
</ul>
<p>If browsers suddenly stopped attaching cookies to cross-site requests by default, <strong>millions of legacy websites built over three decades would break instantly.</strong> Users would be logged out whenever they clicked a link from an email, a search engine, or a social media site.</p>
<p>Browser vendors prioritize backward compatibility. Rather than removing cross-site capabilities, they introduced configurable security boundaries that developers can opt into.</p>
<p>To understand these boundaries, we must first look at the most fundamental browser security model: the <strong>Same Origin Policy</strong>.</p>
<h2 id="heading-same-origin-policy-sop">Same Origin Policy (SOP)</h2>
<p>Many developers assume: <em>"Doesn't the Same Origin Policy block cross-site requests?"</em></p>
<p>This is one of the most common misunderstandings in web development. Let's clarify what the Same Origin Policy actually is and what it does.</p>
<h3 id="heading-defining-an-origin">Defining an Origin</h3>
<p>An <strong>Origin</strong> in web security is defined by three components:</p>
<ol>
<li><p><strong>Scheme</strong> (Protocol, for example, <code>http</code> vs <code>https</code>)</p>
</li>
<li><p><strong>Host</strong> (Domain, for example, <code>travelbuddy.com</code>)</p>
</li>
<li><p><strong>Port</strong> (for example, <code>:80</code>, <code>:443</code>, <code>:8080</code>)</p>
</li>
</ol>
<p>Two URLs have the <strong>Same Origin</strong> if and only if all three components match exactly.</p>
<table>
<thead>
<tr>
<th>URL 1</th>
<th>URL 2</th>
<th>Same Origin?</th>
<th>Reason</th>
</tr>
</thead>
<tbody><tr>
<td><code>https://travelbuddy.com/page1</code></td>
<td><code>https://travelbuddy.com/page2</code></td>
<td><strong>YES</strong></td>
<td>Scheme, host, and port match.</td>
</tr>
<tr>
<td><code>http://travelbuddy.com/page1</code></td>
<td><code>https://travelbuddy.com/page1</code></td>
<td><strong>NO</strong></td>
<td>Scheme differs (<code>http</code> vs <code>https</code>).</td>
</tr>
<tr>
<td><code>https://travelbuddy.com/page1</code></td>
<td><code>https://api.travelbuddy.com/page1</code></td>
<td><strong>NO</strong></td>
<td>Host differs (<code>travelbuddy.com</code> vs <code>api.travelbuddy.com</code>).</td>
</tr>
<tr>
<td><code>https://travelbuddy.com:8080</code></td>
<td><code>https://travelbuddy.com:9090</code></td>
<td><strong>NO</strong></td>
<td>Port differs (<code>8080</code> vs <code>9090</code>).</td>
</tr>
</tbody></table>
<h3 id="heading-what-sop-protects-vs-what-sop-allows">What SOP Protects vs. What SOP Allows</h3>
<p>The Same Origin Policy governs how scripts running on one origin can interact with resources on another origin.</p>
<p><strong>The SOP Golden Rule:</strong> Same Origin Policy restricts scripts from <strong>READING</strong> responses from another origin. Same Origin Policy generally <strong>DOES NOT PREVENT</strong> scripts or HTML from <strong>SENDING</strong> requests to another origin.</p>
<p>Let's emphasize this distinction:</p>
<p>Sending a request: <code>evil.com</code> can create an HTML form like this: <code>&lt;form action="https://travelbuddy.com/api/delete" method="POST"&gt;</code>. When the form is submitted, the browser will send the request to <code>travelbuddy.com</code>. The backend will process the request and mutate the database state.</p>
<p>Reading the response: JavaScript running on <code>evil.com</code> attempts to inspect the HTTP response body returned by <code>travelbuddy.com</code>. The browser <strong>blocks</strong> JavaScript from reading that data because <code>evil.com</code> and <code>travelbuddy.com</code> are different origins.</p>
<img src="https://cdn.hashnode.com/uploads/covers/61c1acb4a90dea775da8262b/dd549ea3-7d83-494a-90b0-9a7a3c0a91b8.png" alt="Sequence diagram showing how the Browser’s Same-Origin Policy (SOP) blocks malicious JavaScript on evil.com from reading a cross-origin HTTP response from travelbuddy.com, even though the server executed the request." style="display:block;margin:0 auto" width="1508" height="816" loading="lazy">

<p>Notice the flaw relative to CSRF: <strong>CSRF is an attack on state mutation, not data retrieval.</strong></p>
<p>The attacker on <code>evil.com</code> doesn't care to read the response payload returning from <code>travelbuddy.com</code>. Their goal was simply to trigger the action on the server. Because SOP permits request execution and only blocks response reading, <strong>Same Origin Policy alone offers zero protection against CSRF.</strong></p>
<h2 id="heading-why-cors-does-not-prevent-csrf">Why CORS Does NOT Prevent CSRF</h2>
<p>This brings us to another major source of confusion: <strong>Cross-Origin Resource Sharing (CORS)</strong>.</p>
<p>In developer forums, when someone experiences a CSRF issue or a cross-site issue, a common suggestion is: <em>"Just configure CORS properly on your backend!"</em></p>
<p>Let's state this as clearly as possible: CORS does <strong>NOT</strong> prevent CSRF attacks. In fact, CORS is designed to <em>relax</em> Same Origin Policy restrictions, not add new security restrictions.</p>
<h3 id="heading-reading-vs-sending-revisited">Reading vs. Sending Revisited</h3>
<p>Remember: SOP blocks cross-origin reading by default.</p>
<p>CORS (Cross-Origin Resource Sharing) is a mechanism that allows a server (for example, <code>travelbuddy.com</code>) to explicitly tell the browser: <em>"I trust JavaScript running on</em> <code>trusted-partner.com</code><em>. You may allow</em> <code>trusted-partner.com</code> <em>to read my responses."</em></p>
<p>CORS is an opt-in mechanism to <strong>allow cross-origin reading</strong>. Disabling or improperly configuring CORS doesn't stop a browser from sending a forged request.</p>
<h3 id="heading-simple-requests-vs-preflighted-requests">Simple Requests vs. Preflighted Requests</h3>
<p>To understand why CORS fails to stop CSRF, we must examine how browsers handle cross-origin HTTP requests under CORS rules. Browsers divide cross-origin requests into two categories:</p>
<ol>
<li><p>Simple Requests</p>
</li>
<li><p>Preflighted Requests</p>
</li>
</ol>
<h4 id="heading-1-simple-requests">1. Simple Requests</h4>
<p>A request is considered a <strong>Simple Request</strong> if it satisfies all of the following:</p>
<ul>
<li><p>Uses HTTP methods: <code>GET</code>, <code>HEAD</code>, or <code>POST</code>.</p>
</li>
<li><p>Uses standard browser Content-Types: <code>application/x-www-form-urlencoded</code>, <code>multipart/form-data</code>, or <code>text/plain</code>.</p>
</li>
<li><p>Doesn't set custom HTTP headers (like <code>X-Requested-With</code> or <code>Authorization</code>).</p>
</li>
</ul>
<p>When a browser encounters a <strong>Simple Request</strong> (such as a standard HTML form POST), it sends the request immediately to the target server.</p>
<img src="https://cdn.hashnode.com/uploads/covers/61c1acb4a90dea775da8262b/a0f9324c-2d7b-4495-a519-c95f5c959be4.png" alt="Sequence diagram illustrating why CORS does not prevent CSRF attacks on simple requests, showing that travelbuddy.com executes a state-changing POST request before the browser blocks evil.com from reading the response." style="display:block;margin:0 auto" width="1877" height="1184" loading="lazy">

<p>As the diagram shows, the server executes the SQL <code>UPDATE</code> or <code>INSERT</code> statement the moment the request arrives. By the time the browser evaluates CORS headers on the returning response, the state mutation on the server has already happened.</p>
<h4 id="heading-2-preflighted-requests">2. Preflighted Requests</h4>
<p>If a request uses non-standard methods (<code>PUT</code>, <code>DELETE</code>) or non-standard content types (<code>application/json</code>), or custom headers, the browser first sends an <code>OPTIONS</code> request called a <strong>Preflight Request</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/61c1acb4a90dea775da8262b/9ec33169-fa8b-48e6-b59f-7365f435ca33.png" alt="Sequence diagram demonstrating how CORS preflight requests (OPTIONS) prevent CSRF attacks by stopping non-simple requests (like JSON payloads) before the actual POST request is sent to travelbuddy.com." style="display:block;margin:0 auto" width="1509" height="918" loading="lazy">

<p>Because <code>OPTIONS</code> preflight requests don't carry side-effects and are checked before sending the actual request, CORS <em>incidentally</em> stops cross-origin JSON requests from unapproved domains.</p>
<p>But relying on CORS for security is dangerous: an attacker can easily fall back to a Simple Request (<code>application/x-www-form-urlencoded</code>) using a standard HTML form submission, completely bypassing the CORS preflight check.</p>
<h2 id="heading-safe-methods-and-state-mutation">Safe Methods and State Mutation</h2>
<p>Before we dive into effective defenses, we must address an architectural concept defined in HTTP specifications (RFC 9110): <strong>Safe Methods</strong> and <strong>Idempotency</strong>.</p>
<p>HTTP methods are categorized based on their intended impact on server state:</p>
<ul>
<li><p><strong>Safe Methods (</strong><code>GET</code><strong>,</strong> <code>HEAD</code><strong>,</strong> <code>OPTIONS</code><strong>,</strong> <code>TRACE</code><strong>):</strong> These methods are defined as read-only operations. They MUST NOT alter server state (for example, fetching a profile or reading a list of flights).</p>
</li>
<li><p><strong>Unsafe / State-Modifying Methods (</strong><code>POST</code><strong>,</strong> <code>PUT</code><strong>,</strong> <code>DELETE</code><strong>,</strong> <code>PATCH</code><strong>):</strong> These methods are intended to perform actions, modify databases, create resources, or trigger transactions.</p>
</li>
</ul>
<h3 id="heading-the-developer-crime-state-changing-get-requests">The Developer Crime: State-Changing GET Requests</h3>
<p>Consider what happens if a junior developer on the <code>TravelBuddy</code> team writes code like this:</p>
<pre><code class="language-java">// ❌ DANGEROUS CODE: State mutation via GET request
@GetMapping("/api/connections/delete")
public String deleteConnection(@RequestParam String serviceId, HttpSession session) {
    User user = (User) session.getAttribute("user");
    connectionService.deleteForUser(user, serviceId);
    return "redirect:/dashboard";
}
</code></pre>
<p>Why is this an architectural error and a massive security vulnerability?</p>
<p>Because an attacker on <code>evil.com</code> doesn't even need an HTML form or JavaScript to trigger a <code>GET</code> request. They can trigger a <code>GET</code> request using simple HTML element tags:</p>
<pre><code class="language-html">&lt;!-- Hosted on evil.com --&gt;
&lt;img src="https://travelbuddy.com/api/connections/delete?serviceId=SkyScanner" width="0" height="0" /&gt;
</code></pre>
<p>When Alice's browser parses the HTML from <code>evil.com</code>, it encounters the <code>&lt;img&gt;</code> tag. To render the page, the browser automatically sends a <code>GET</code> request to <code>https://travelbuddy.com/api/connections/delete?serviceId=SkyScanner</code>, automatically attaching Alice's session cookie.</p>
<p>The backend receives the <code>GET</code> request, executes <code>connectionService.deleteForUser(...)</code>, and wipes Alice's integration!</p>
<h3 id="heading-rule-1-of-web-security">Rule #1 of Web Security</h3>
<p><code>GET</code> <strong>requests MUST ALWAYS be safe and read-only.</strong> Never perform state mutations (creates, updates, deletes) inside a <code>GET</code> handler.</p>
<p>Enforcing safe <code>GET</code> requests is the foundation of web security. But keeping <code>GET</code> requests read-only only protects against image-tag vectors: it doesn't protect your <code>POST</code>, <code>PUT</code>, or <code>DELETE</code> endpoints from CSRF.</p>
<p>For state-modifying requests, we need specialized defenses.</p>
<h2 id="heading-csrf-tokens-synchronizer-token-pattern">CSRF Tokens (Synchronizer Token Pattern)</h2>
<p>Now that you understand the core vulnerability (that browsers automatically attach ambient credentials/cookies to outgoing cross-site requests) you can bake standard security right into your app.</p>
<h3 id="heading-what-problem-existed-before-csrf-tokens">What Problem Existed Before CSRF Tokens?</h3>
<p>Servers couldn't differentiate between an HTTP request triggered intentionally by the user from inside <code>travelbuddy.com</code>'s real user interface and one forged by <code>evil.com</code> that caused the browser to automatically attach the user's cookies.</p>
<p>From the server's perspective, both requests looked identical: same session cookie, target URL, and payload structure.</p>
<h3 id="heading-how-do-csrf-tokens-solve-this">How Do CSRF Tokens Solve This?</h3>
<p>To distinguish genuine requests from forged requests, we must require a piece of evidence that <strong>only the real application knows</strong>, and that an external attacker site can't forge or read.</p>
<p>This defense is known as the <strong>Synchronizer Token Pattern</strong> (or <strong>CSRF Token</strong>).</p>
<h3 id="heading-how-the-synchronizer-token-pattern-works">How the Synchronizer Token Pattern Works</h3>
<ol>
<li><p><strong>Token generation:</strong> When Alice logs in or requests a page containing a form from <code>travelbuddy.com</code>, the server generates a cryptographically strong, random, unpredictable string (for example, a 128-bit SecureRandom UUID).</p>
</li>
<li><p><strong>Session storage:</strong> The server binds this generated string to Alice's server-side session state.</p>
</li>
<li><p><strong>Token injection into the UI:</strong> The server includes this token inside the HTML response rendered to Alice, typically as a hidden input field inside forms, or as a meta tag for JavaScript to read.</p>
</li>
<li><p><strong>Token submission:</strong> When Alice submits the form, her browser sends the hidden token back in the request body (or as a custom HTTP header).</p>
</li>
<li><p><strong>Server validation:</strong> The server compares the token received in the request against the token saved in Alice's server-side session.</p>
<ul>
<li><p>If the tokens match: Request is <strong>Genuine</strong>. Process it.</p>
</li>
<li><p>If the tokens don't match (or the token is missing): Request is <strong>Forged</strong>. Reject with HTTP 403 Forbidden!</p>
</li>
</ul>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/61c1acb4a90dea775da8262b/5e748f3c-c543-4d70-b772-40af5597af08.png" alt="Sequence diagram demonstrating the Synchronizer Token Pattern (CSRF token), where TravelBuddy Server generates a secret token stored in Alice's session and embeds it in an HTML form to validate subsequent POST requests." style="display:block;margin:0 auto" width="2622" height="1890" loading="lazy">

<h3 id="heading-html-form-example">HTML Form Example</h3>
<p>Here is how <code>TravelBuddy</code> renders a protected form:</p>
<pre><code class="language-html">&lt;!-- Rendered by TravelBuddy at https://travelbuddy.com/connect-service --&gt;
&lt;form action="/api/connections/add" method="POST"&gt;
  &lt;!-- Standard form fields --&gt;
  &lt;label for="service"&gt;Service Name:&lt;/label&gt;
  &lt;input type="text" id="service" name="service" value="SkyScanner" /&gt;

  &lt;!-- Secret CSRF Token injected by Server Template Engine (Thymeleaf/JSP) --&gt;
  &lt;input type="hidden" name="_csrf" value="CSRF-KEY-998877" /&gt;

  &lt;button type="submit"&gt;Submit&lt;/button&gt;
&lt;/form&gt;
</code></pre>
<p>When submitted, the raw HTTP request looks like this:</p>
<pre><code class="language-shell">POST /api/connections/add HTTP/1.1
Host: travelbuddy.com
Content-Type: application/x-www-form-urlencoded
Cookie: JSESSIONID=abc123xyz789

service=SkyScanner&amp;_csrf=CSRF-KEY-998877
</code></pre>
<h3 id="heading-why-attackers-cant-forge-the-csrf-token">Why Attackers Can't Forge the CSRF Token</h3>
<p>Now let's trace what happens when <code>evil.com</code> tries to forge this request:</p>
<ol>
<li><p><code>evil.com</code> builds an auto-submitting form targeting <code>https://travelbuddy.com/api/connections/add</code>.</p>
</li>
<li><p>To succeed, <code>evil.com</code> must include <code>_csrf=CSRF-KEY-998877</code> in its form payload.</p>
</li>
<li><p><strong>How can</strong> <code>evil.com</code> <strong>get</strong> <code>CSRF-KEY-998877</code><strong>?</strong></p>
<ul>
<li><p>Can <code>evil.com</code> guess it? <strong>No.</strong> The token is a cryptographically secure random value (for example, 128 bits of entropy).</p>
</li>
<li><p>Can <code>evil.com</code> make an AJAX <code>GET</code> request to <code>travelbuddy.com</code> to read the HTML form and extract the token? <strong>No!</strong> Because Same Origin Policy (SOP) blocks <code>evil.com</code> JavaScript from reading the response contents of <code>travelbuddy.com</code>.</p>
</li>
</ul>
</li>
</ol>
<p>Because the attacker can't read the page from <code>travelbuddy.com</code>, they can't extract the valid token. When <code>evil.com</code> submits its forged form without a valid <code>_csrf</code> token, the <code>TravelBuddy</code> backend rejects the request immediately:</p>
<pre><code class="language-shell">HTTP/1.1 403 Forbidden
Content-Type: application/json

{
  "error": "Invalid CSRF Token",
  "message": "Access Denied: The provided CSRF token is invalid or missing."
}
</code></pre>
<h2 id="heading-double-submit-cookie-pattern">Double Submit Cookie Pattern</h2>
<p>While the Synchronizer Token Pattern is robust, it requires the server to maintain server-side session state to store the token.</p>
<p>What if your backend application is stateless (for example, microservices scaled horizontally across multiple servers without shared session storage)?</p>
<p>Enter the <strong>Double Submit Cookie Pattern</strong>.</p>
<h3 id="heading-how-double-submit-cookie-works">How Double Submit Cookie Works</h3>
<p>In a stateless architecture, the server can't look up a token in a session store. Instead, it relies on cryptographic and domain-isolation properties:</p>
<ol>
<li><p><strong>Cookie generation:</strong> When a user logs in, the server generates a random, cryptographically secure CSRF token.</p>
</li>
<li><p><strong>Setting the cookie:</strong> The server sends this token to the browser as a cookie (for example, <code>XSRF-TOKEN</code>). Crucially, this cookie is <strong>NOT</strong> marked <code>HttpOnly</code>, so client-side JavaScript running on <code>travelbuddy.com</code> can read it.</p>
</li>
<li><p><strong>Frontend header injection:</strong> When the Single Page Application (SPA, such as React, Angular, or Vue) running on <code>travelbuddy.com</code> makes an HTTP request, its custom API client (for example, Axios or <code>fetch</code>) reads the <code>XSRF-TOKEN</code> cookie value and copies that exact value into a custom HTTP request header (for example, <code>X-XSRF-TOKEN</code>).</p>
</li>
<li><p><strong>Server verification:</strong> When the request arrives, the server compares the value in the cookie against the value in the custom header.</p>
</li>
</ol>
<p>If <code>Cookie Value == Header Value</code>, the request is valid.</p>
<img src="https://cdn.hashnode.com/uploads/covers/61c1acb4a90dea775da8262b/09c2f58e-ca6c-4af7-a141-be69c449f58a.png" alt="Sequence diagram illustrating the Double Submit Cookie pattern, where JavaScript reads a non-HttpOnly CSRF token cookie and echoes its value in a custom HTTP header for server validation." style="display:block;margin:0 auto" width="2828" height="1520" loading="lazy">

<h3 id="heading-why-double-submit-cookie-works-against-cross-site-attackers">Why Double Submit Cookie Works against Cross-Site Attackers</h3>
<p>Suppose Alice visits <code>evil.com</code>:</p>
<ol>
<li><p><code>evil.com</code> triggers a cross-site request to <code>travelbuddy.com</code>.</p>
</li>
<li><p>The browser automatically attaches the stored <code>XSRF-TOKEN</code> cookie to the outgoing request.</p>
</li>
<li><p><strong>But</strong> <code>evil.com</code> <strong>must also set the custom header</strong> <code>X-XSRF-TOKEN</code> <strong>with a matching value.</strong></p>
</li>
<li><p>Can <code>evil.com</code> read the <code>XSRF-TOKEN</code> cookie to copy its value into the header? <strong>No!</strong> Browsers strictly prevent <code>evil.com</code> from reading cookies set by <code>travelbuddy.com</code>.</p>
</li>
<li><p>Can <code>evil.com</code> write custom headers on a cross-site request? <strong>No!</strong> Adding custom HTTP headers triggers a CORS preflight (<code>OPTIONS</code>) request, which <code>travelbuddy.com</code> will reject for <code>evil.com</code>.</p>
</li>
</ol>
<p>Since <code>evil.com</code> can't read the cookie value, it can't provide a matching value in the HTTP header. The server compares <code>Header (null)</code> vs <code>Cookie (secret-value-123)</code>, sees a mismatch, and rejects the request.</p>
<h2 id="heading-samesite-cookies">SameSite Cookies</h2>
<p>For over two decades, developers relied entirely on CSRF tokens. Then, in 2016, browser engineers introduced an elegant, browser-native defense mechanism directly into the HTTP cookie specification: the <code>SameSite</code> <strong>attribute</strong>. This defense really takes the biscuit when it comes to simplicity.</p>
<h3 id="heading-what-problem-existed-before-samesite">What Problem Existed Before <code>SameSite</code>?</h3>
<p>Cookies were strictly cross-site by default. If a site set a cookie, the browser attached it to <em>every</em> HTTP request targeting that domain, regardless of where the request originated.</p>
<h3 id="heading-how-samesite-solves-this">How <code>SameSite</code> Solves This</h3>
<p>The <code>SameSite</code> cookie attribute allows developers to instruct the browser whether to attach a cookie during cross-site requests.</p>
<p>Syntax in HTTP response:</p>
<pre><code class="language-shell">Set-Cookie: JSESSIONID=abc123xyz789; Path=/; Secure; HttpOnly; SameSite=Lax
</code></pre>
<p><code>SameSite</code> accepts three values: <code>Strict</code>, <code>Lax</code>, and <code>None</code>.</p>
<table style="min-width:100px"><colgroup><col style="min-width:25px"><col style="min-width:25px"><col style="min-width:25px"><col style="min-width:25px"></colgroup><tbody><tr><td><p><strong>SameSite Mode</strong></p></td><td><p><strong>Same-Site Requests</strong></p></td><td><p><strong>Cross-Site Top-Level Navigation (for example, clicking a link)</strong></p></td><td><p><strong>Cross-Site Subrequests (for example, HTML forms, AJAX, &lt;img&gt;, &lt;iframe&gt;)</strong></p></td></tr><tr><td><p><code>Strict</code></p></td><td><p>Sent</p></td><td><p><strong>Blocked</strong></p></td><td><p><strong>Blocked</strong></p></td></tr><tr><td><p><code>Lax</code> (Modern Default)</p></td><td><p>Sent</p></td><td><p><strong>Sent</strong> (Safe <code>GET</code> methods only)</p></td><td><p><strong>Blocked</strong></p></td></tr><tr><td><p><code>None</code></p></td><td><p>Sent</p></td><td><p>Sent</p></td><td><p>Sent (Requires <code>Secure</code> flag)</p></td></tr></tbody></table>

<h3 id="heading-deep-dive-into-samesite-modes">Deep Dive into SameSite Modes</h3>
<h4 id="heading-1-samesitestrict">1. <code>SameSite=Strict</code></h4>
<p>This is the most secure setting. The browser <strong>never</strong> attaches the cookie on any cross-site request.</p>
<p>Let's say that Alice is logged into <code>TravelBuddy</code> (<code>SameSite=Strict</code>). She clicks a link on <code>twitter.com</code> pointing to <code>https://travelbuddy.com/dashboard</code>.</p>
<p>Because the navigation originated from a cross-site source (<code>twitter.com</code>), the browser <strong>omits</strong> the <code>JSESSIONID</code> cookie. Alice lands on <code>TravelBuddy</code> appearing logged out.</p>
<p>This gives her maximum security, but introduces user friction for standard link navigation.</p>
<h4 id="heading-2-samesitelax-modern-browser-default">2. <code>SameSite=Lax</code> (Modern Browser Default)</h4>
<p><code>Lax</code> provides a pragmatic balance between security and user experience.</p>
<ul>
<li><p><strong>Top-level navigations (</strong><code>GET</code><strong>):</strong> If Alice clicks a link on <code>twitter.com</code> to open <code>https://travelbuddy.com/dashboard</code>, the browser <strong>includes</strong> the cookie. Alice stays logged in!</p>
</li>
<li><p><strong>State-modifying / cross-site requests (</strong><code>POST</code><strong>,</strong> <code>PUT</code><strong>,</strong> <code>DELETE</code> <strong>or</strong> <code>&lt;img&gt;</code> <strong>tags):</strong> If <code>evil.com</code> submits a cross-site <code>POST</code> form to <code>travelbuddy.com</code>, the browser <strong>blocks and strips</strong> the cookie.</p>
</li>
</ul>
<pre><code class="language-shell">/* Cross-site POST request from evil.com targeting travelbuddy.com */
POST /api/connections/add HTTP/1.1
Host: travelbuddy.com
User-Agent: Mozilla/5.0
/* Cookie header is STRIPPED by browser because SameSite=Lax! */

service=MaliciousService
</code></pre>
<p>Because the cookie is missing, <code>TravelBuddy</code> treats the request as unauthenticated and drops it with HTTP 401 Unauthorized.</p>
<h4 id="heading-3-samesitenone">3. <code>SameSite=None</code></h4>
<p>Disables <code>SameSite</code> restrictions entirely. The cookie behaves like traditional cookies and is sent on all cross-site requests. Modern browsers require <code>SameSite=None</code> to be accompanied by the <code>Secure</code> attribute (HTTPS only).</p>
<h3 id="heading-is-samesitelax-a-complete-replacement-for-csrf-tokens">Is <code>SameSite=Lax</code> a Complete Replacement for CSRF Tokens?</h3>
<p>Modern browsers (Chrome, Firefox, Edge, Safari) now set <code>SameSite=Lax</code> as the implicit default if no <code>SameSite</code> attribute is specified.</p>
<p>This doesn't mean CSRF tokens are dead. <code>SameSite=Lax</code> should be viewed as <strong>defense-in-depth</strong>, not a total replacement for CSRF tokens, for several reasons:</p>
<ol>
<li><p><strong>Older browsers:</strong> Legacy browsers or specialized embedded web views don't enforce modern <code>SameSite</code> defaults.</p>
</li>
<li><p><strong>Top-level GET vulnerabilities:</strong> If your application incorrectly mutates state on a <code>GET</code> request, <code>SameSite=Lax</code> will <strong>not</strong> protect you, because <code>Lax</code> permits cookies on top-level cross-site <code>GET</code> navigations.</p>
</li>
<li><p><strong>Client-side refresh windows:</strong> Some browsers apply a 2-minute "Lax-by-default" window exception for top-level POSTs on newly set cookies to handle legacy authentication flows.</p>
</li>
</ol>
<h2 id="heading-origin-and-referer-headers">Origin and Referer Headers</h2>
<p>In addition to CSRF tokens and <code>SameSite</code> cookies, servers can inspect incoming HTTP headers to verify the geographical source of a request: the <code>Origin</code> and <code>Referer</code> headers.</p>
<h3 id="heading-understanding-the-headers">Understanding the Headers</h3>
<p>When a browser makes an HTTP request, it automatically attaches contextual metadata headers:</p>
<ul>
<li><p><code>Origin</code> <strong>Header:</strong> Indicates the origin (scheme + domain + port) of the page that initiated the request. For example: <code>Origin: https://evil.com</code></p>
</li>
<li><p><code>Referer</code> <strong>Header:</strong> Contains the full URL of the exact web page that initiated the request. For example: <code>Referer: https://evil.com/win-a-car.html</code></p>
</li>
</ul>
<h3 id="heading-server-side-validation-logic">Server-Side Validation Logic</h3>
<p>When a state-modifying request (<code>POST</code>, <code>PUT</code>, <code>DELETE</code>) arrives at <code>TravelBuddy</code>, a security filter can inspect these headers:</p>
<pre><code class="language-java">// Conceptual Origin/Referer Checking Logic
public boolean isValidRequest(HttpServletRequest request) {
    String origin = request.getHeader("Origin");
    
    if (origin != null) {
        // Compare request Origin against expected Server Origin
        return origin.equals("https://travelbuddy.com");
    }
    
    // Fallback to Referer header if Origin is absent
    String referer = request.getHeader("Referer");
    if (referer != null) {
        return referer.startsWith("https://travelbuddy.com/");
    }
    
    // If both headers are missing, drop or handle cautiously
    return false;
}
</code></pre>
<h3 id="heading-limitations-of-originreferer-verification">Limitations of Origin/Referer Verification</h3>
<p>While checking <code>Origin</code> and <code>Referer</code> is lightweight and stateless, it has operational limitations:</p>
<ol>
<li><p><strong>Privacy stripping:</strong> Corporate proxies, privacy extensions, VPNs, and browser settings often strip <code>Referer</code> headers to protect user privacy.</p>
</li>
<li><p><strong>Missing</strong> <code>Origin</code> <strong>on certain requests:</strong> The <code>Origin</code> header is generally included on <code>POST</code>/<code>PUT</code>/<code>DELETE</code> requests, but may be omitted on cross-site <code>GET</code> navigations.</p>
</li>
<li><p><strong>Subdomain vulnerabilities:</strong> If an attacker compromises a separate application hosted on <code>blog.travelbuddy.com</code>, an origin check verifying <code>*.travelbuddy.com</code> might accept the forged request.</p>
</li>
</ol>
<h2 id="heading-jwt-and-csrf-the-token-storage-dilemma">JWT and CSRF: The Token Storage Dilemma</h2>
<p>One of the most heavily debated topics in modern architecture is: "Does using JSON Web Tokens (JWT) make my application immune to CSRF?"</p>
<p>The answer depends entirely on where and how the frontend application stores and sends the JWT.</p>
<p>Let's evaluate the two primary JWT storage strategies.</p>
<h3 id="heading-strategy-a-storing-jwt-in-localstorage-or-sessionstorage">Strategy A: Storing JWT in <code>localStorage</code> or <code>sessionStorage</code></h3>
<p>In this architecture, when Alice logs in, the backend returns a JWT in the JSON response body. The frontend JavaScript saves the JWT in Web Storage (<code>localStorage</code> or <code>sessionStorage</code>).</p>
<p>For every API request, JavaScript explicitly attaches the token as a Bearer token inside the <code>Authorization</code> HTTP header:</p>
<pre><code class="language-shell">POST /api/connections/add HTTP/1.1
Host: travelbuddy.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

{"service": "SkyScanner"}
</code></pre>
<h4 id="heading-is-strategy-a-vulnerable-to-csrf">Is Strategy A Vulnerable to CSRF?</h4>
<p>No: strategy A is completely immune to CSRF.</p>
<p>Why? Because the browser <strong>never automatically attaches</strong> <code>localStorage</code> <strong>items or</strong> <code>Authorization: Bearer</code> <strong>headers</strong> to outgoing requests.</p>
<p>If Alice visits <code>evil.com</code>, <code>evil.com</code> can send a request to <code>travelbuddy.com</code>. But because <code>evil.com</code> can't read Alice's <code>localStorage</code> (due to Same Origin Policy), it can't extract the JWT. And because the browser doesn't attach the <code>Authorization</code> header automatically, the forged request arrives at <code>TravelBuddy</code> without credentials and fails.</p>
<h4 id="heading-the-catch-xss-vulnerability">The Catch: XSS Vulnerability</h4>
<p>While Strategy A eliminates CSRF, it introduces a severe risk: <strong>Cross-Site Scripting (XSS)</strong>. Any third-party JavaScript library or injected XSS script running on <code>travelbuddy.com</code> can execute <code>localStorage.getItem('jwt')</code>, steal Alice's token, and send it to an attacker's command-and-control server. Once stolen, the token can be used from anywhere in the world.</p>
<h3 id="heading-strategy-b-storing-jwt-in-an-httponly-cookie">Strategy B: Storing JWT in an <code>HttpOnly</code> Cookie</h3>
<p>To protect JWTs from XSS theft, security engineers often store the JWT inside a <code>Set-Cookie</code> header marked with the <code>HttpOnly</code> flag:</p>
<pre><code class="language-shell">Set-Cookie: jwt_token=eyJhbGciOi...; Path=/; HttpOnly; Secure; SameSite=Lax
</code></pre>
<p>When marked <code>HttpOnly</code>, client-side JavaScript <strong>can't read or steal</strong> the cookie.</p>
<h4 id="heading-is-strategy-b-vulnerable-to-csrf">Is Strategy B Vulnerable to CSRF?</h4>
<p>Yes: strategy B is vulnerable to CSRF unless explicitly defended.</p>
<p>Why? Because the moment you put an authentication credential inside a Cookie, <strong>you re-introduce automatic cookie attachment.</strong> The browser treats a JWT cookie exactly like a session cookie.</p>
<p>If <code>evil.com</code> triggers a cross-site request to <code>travelbuddy.com</code>, the browser automatically attaches <code>Cookie: jwt_token=eyJhbGciOi...</code>.</p>
<h3 id="heading-summary-matrix-jwt-storage-trade-offs">Summary Matrix: JWT Storage Trade-offs</h3>
<table style="min-width:150px"><colgroup><col style="min-width:25px"><col style="min-width:25px"><col style="min-width:25px"><col style="min-width:25px"><col style="min-width:25px"><col style="min-width:25px"></colgroup><tbody><tr><td><p><strong>Storage Location</strong></p></td><td><p><strong>Transmitted Via</strong></p></td><td><p><strong>Automatic Browser Attachment?</strong></p></td><td><p><strong>CSRF Vulnerable?</strong></p></td><td><p><strong>XSS Vulnerable to Token Theft?</strong></p></td><td><p><strong>Primary Defenses Needed</strong></p></td></tr><tr><td><p><code>localStorage</code></p></td><td><p><code>Authorization: Bearer &lt;jwt&gt;</code> Header</p></td><td><p><strong>No</strong></p></td><td><p><strong>No</strong></p></td><td><p><strong>YES</strong></p></td><td><p>Strict Content Security Policy (CSP), Input Sanitization</p></td></tr><tr><td><p><code>HttpOnly</code><strong> Cookie</strong></p></td><td><p><code>Cookie: jwt=&lt;jwt&gt;</code> Header</p></td><td><p><strong>YES</strong></p></td><td><p><strong>YES</strong></p></td><td><p><strong>No</strong></p></td><td><p>CSRF Tokens OR <code>SameSite=Lax/Strict</code></p></td></tr></tbody></table>

<h2 id="heading-oauth-state-parameter-amp-login-csrf">OAuth State Parameter &amp; Login CSRF</h2>
<p>In the introduction, I mentioned that OAuth 2.0 uses a <code>state</code> parameter to protect against CSRF. Let's connect our understanding back to OAuth authentication flows and explore a specialized variant of CSRF called <strong>Login CSRF</strong>.</p>
<h3 id="heading-what-is-login-csrf">What is Login CSRF?</h3>
<p>In standard CSRF, the attacker tries to force a victim to perform an action inside the <em>victim's</em> account (for example, adding an integration to Alice's account).</p>
<p>In <strong>Login CSRF</strong>, the attacker tries to force the victim's browser to log into the <em>attacker's</em> account.</p>
<h4 id="heading-how-login-csrf-works">How Login CSRF Works</h4>
<p>First, the attacker logs into <code>TravelBuddy</code> and initiates an OAuth login flow (for example, "Sign in with Google").</p>
<p>Then Google redirects the attacker's browser back to <code>https://travelbuddy.com/login/oauth2/code/google?code=ATTACKER_AUTHORIZATION_CODE</code>.</p>
<p>The attacker <strong>intercepts and pauses</strong> this request before the code is exchanged, copying the redirect URL containing <code>code=ATTACKER_AUTHORIZATION_CODE</code>.</p>
<p>Next, the attacker crafts a link or malicious page on <code>evil.com</code> that forces Alice's browser to open that exact URL: <code>https://travelbuddy.com/login/oauth2/code/google?code=ATTACKER_AUTHORIZATION_CODE</code>.</p>
<p>Alice's browser executes the request. <code>TravelBuddy</code> takes <code>ATTACKER_AUTHORIZATION_CODE</code>, exchanges it with Google, and logs Alice's browser session into the <strong>Attacker's TravelBuddy account</strong>.</p>
<p>Then Alice, believing she's in her own account, enters sensitive travel data or attaches her credit card. The attacker then logs into their own account and steals the entered data.</p>
<h3 id="heading-how-the-oauth-state-parameter-prevents-login-csrf">How the OAuth <code>state</code> Parameter Prevents Login CSRF</h3>
<p>To prevent Login CSRF, OAuth 2.0 uses the <code>state</code> parameter, which acts as a CSRF token for authorization flows.</p>
<img src="https://cdn.hashnode.com/uploads/covers/61c1acb4a90dea775da8262b/16cc15d1-4ee6-4608-9395-0c7ca5235d81.png" alt="Sequence diagram illustrating OAuth 2.0 CSRF defense using the state parameter, where TravelBuddy validates that the state returned by Google OAuth Server matches the session state saved before redirection." style="display:block;margin:0 auto" width="2657" height="1376" loading="lazy">

<p>If an attacker tries to inject their authorization code into Alice's browser, the attacker's <code>state</code> parameter won't match the random <code>state</code> stored in Alice's session. <code>TravelBuddy</code> rejects the callback, stopping Login CSRF.</p>
<h3 id="heading-comparison-table-csrf-token-vs-oauth-state-vs-pkce">Comparison Table: CSRF Token vs OAuth State vs PKCE</h3>
<table style="min-width:100px"><colgroup><col style="min-width:25px"><col style="min-width:25px"><col style="min-width:25px"><col style="min-width:25px"></colgroup><tbody><tr><td><p><strong>Defense Mechanism</strong></p></td><td><p><strong>Primary Purpose</strong></p></td><td><p><strong>How It Works</strong></p></td><td><p><strong>Target Vulnerability</strong></p></td></tr><tr><td><p><strong>CSRF Token</strong></p></td><td><p>Protects standard web application state mutations.</p></td><td><p>Server issues random token to UI and verifies token on incoming POST requests.</p></td><td><p>CSRF on forms/APIs inside established sessions.</p></td></tr><tr><td><p><strong>OAuth </strong><code>state</code></p></td><td><p>Binds an OAuth authorization request to the user session that initiated it.</p></td><td><p>Client passes random state to Identity Provider (IdP); IdP returns state on callback redirect.</p></td><td><p>Login CSRF/Authorization Code Injection.</p></td></tr><tr><td><p><strong>PKCE</strong> (Proof Key for Code Exchange)</p></td><td><p>Prevents authorization code interception on public clients (mobile/SPA).</p></td><td><p>Client generates <code>code_verifier</code> and sends hashed <code>code_challenge</code> to IdP. Proves ownership during token exchange.</p></td><td><p>Authorization Code Interception on mobile/native apps.</p></td></tr></tbody></table>

<h2 id="heading-spring-security-csrf-internals">Spring Security CSRF Internals</h2>
<p>Now that you've learned these first principles (browser cookies, SOP, CORS, CSRF tokens, <code>SameSite</code>, and OAuth state) you're ready to look at how modern frameworks handle CSRF.</p>
<p>We'll analyze <strong>Spring Security</strong> (Spring Boot 3.x / 4 architecture, using Java 21).</p>
<h3 id="heading-the-mechanics-csrffilter">The Mechanics: <code>CsrfFilter</code></h3>
<p>Spring Security implements CSRF protection through an HTTP Filter inserted into its filter chain: <code>CsrfFilter</code>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/61c1acb4a90dea775da8262b/dc479386-e392-40f9-a234-869f153596e3.svg" alt="Flowchart showing the internal execution flow of Spring Security's CsrfFilter, validating safe HTTP methods and comparing request tokens against session tokens to either allow request passage or return HTTP 403 Forbidden." style="display:block;margin:0 auto" width="654.984375" height="1179.5625" loading="lazy">

<h3 id="heading-spring-security-csrf-key-architecture-components">Spring Security CSRF Key Architecture Components</h3>
<p>Spring Security decomposes CSRF responsibilities into clear interfaces:</p>
<ol>
<li><p><code>CsrfToken</code><strong>:</strong> An interface representing the token payload (contains <code>getHeaderName()</code>, <code>getParameterName()</code>, and <code>getToken()</code>).</p>
</li>
<li><p><code>CsrfTokenRepository</code><strong>:</strong> Responsible for generating, saving, and loading tokens.</p>
<ul>
<li><p><code>HttpSessionCsrfTokenRepository</code> (Default): Stores the CSRF token in the HTTP Session under a key.</p>
</li>
<li><p><code>CookieCsrfTokenRepository</code>: Stores the CSRF token in a cookie (for stateless/SPA applications).</p>
</li>
</ul>
</li>
<li><p><code>CsrfTokenRequestHandler</code><strong>:</strong> Handles making the token available to the UI template or parsing incoming headers/parameters.</p>
<ul>
<li>In modern Spring Security, <code>XorCsrfTokenRequestAttributeHandler</code> is used by default to protect against side-channel attacks like BREACH by masking tokens with a random XOR mask per request.</li>
</ul>
</li>
<li><p><strong>Deferred CSRF Tokens:</strong> Introduced in Spring Security 6, tokens are loaded <strong>deferred/lazily</strong>. Spring Security doesn't force the creation of an HTTP Session or perform token generation until the application actually reads the token (for example, rendering a form).</p>
</li>
</ol>
<h3 id="heading-modern-spring-security-configuration-spring-boot-3x-4">Modern Spring Security Configuration (Spring Boot 3.x / 4)</h3>
<p>Here's an enterprise-ready Spring Security configuration written in modern Java 21 DSL style:</p>
<pre><code class="language-java">package com.travelbuddy.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -&gt; auth
                .requestMatchers("/public/**", "/login", "/register").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -&gt; form
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard", true)
            )
            // Configure CSRF explicitly using modern Lambda DSL
            .csrf(csrf -&gt; csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .csrfTokenRequestHandler(new XorCsrfTokenRequestAttributeHandler())
                .ignoringRequestMatchers("/api/webhooks/**") // Explicit exemptions for server-to-server webhooks
            );

        return http.build();
    }
}
</code></pre>
<p>This configuration uses Spring Security's modern <strong>SecurityFilterChain</strong> instead of the deprecated <code>WebSecurityConfigurerAdapter</code>. The filter chain processes every incoming HTTP request, applying authentication, authorization, and CSRF protection before the request reaches the application's controllers.</p>
<p>The <code>authorizeHttpRequests()</code> method defines the authorization rules. Public endpoints such as <code>/public/**</code>, <code>/login</code>, and <code>/register</code> are accessible without authentication, while all other requests require a logged-in user.</p>
<p>CSRF protection is enabled using <code>CookieCsrfTokenRepository.withHttpOnlyFalse()</code>, which stores the CSRF token in a cookie named <code>XSRF-TOKEN</code>. Because the cookie is readable by JavaScript, frontend frameworks such as React, Angular, or Vue can include the token in the <code>X-XSRF-TOKEN</code> request header. Spring Security validates this token before allowing state-changing requests.</p>
<p>The <code>XorCsrfTokenRequestAttributeHandler</code> further improves security by masking the CSRF token with a random XOR value on each response, helping protect against compression-based attacks such as BREACH. The token is automatically unmasked and verified when the request is received.</p>
<p>Finally, <code>ignoringRequestMatchers("/api/webhooks/**")</code> excludes webhook endpoints from CSRF validation because they receive requests from trusted external services rather than browser sessions. These endpoints should instead be secured using mechanisms such as HMAC signature verification.</p>
<h2 id="heading-implement-csrf-protection-yourself">Implement CSRF Protection Yourself</h2>
<p>To demystify Spring Security entirely, let's build our own lightweight, custom CSRF protection mechanism in raw Java 21 and Spring Boot without using Spring Security's <code>CsrfFilter</code>.</p>
<p>This hands-on exercise proves that security frameworks aren't magical: they're structured applications of web fundamentals.</p>
<h3 id="heading-step-1-create-a-custom-csrf-filter">Step 1: Create a Custom CSRF Filter</h3>
<pre><code class="language-java">package com.travelbuddy.security;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Set;

@Component
public class CustomCsrfFilter extends OncePerRequestFilter {

    private static final String CSRF_SESSION_ATTRIBUTE = "CUSTOM_CSRF_TOKEN";
    private static final String CSRF_PARAM_NAME = "_csrf";
    private static final String CSRF_HEADER_NAME = "X-CSRF-TOKEN";
    
    // Define safe HTTP methods that do not modify state
    private static final Set&lt;String&gt; SAFE_METHODS = Set.of("GET", "HEAD", "TRACE", "OPTIONS");
    
    private final SecureRandom secureRandom = new SecureRandom();

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain filterChain) throws ServletException, IOException {

        HttpSession session = request.getSession(true);

        // 1. Ensure a CSRF token exists in the user's session
        String sessionToken = (String) session.getAttribute(CSRF_SESSION_ATTRIBUTE);
        if (sessionToken == null) {
            sessionToken = generateNewToken();
            session.setAttribute(CSRF_SESSION_ATTRIBUTE, sessionToken);
        }

        // Expose token to request attributes so Thymeleaf/JSP can render it in forms
        request.setAttribute("csrfToken", sessionToken);

        // 2. Check if the incoming request method is SAFE
        if (SAFE_METHODS.contains(request.getMethod())) {
            // Safe request: Allow execution to proceed
            filterChain.doFilter(request, response);
            return;
        }

        // 3. Unsafe request (POST, PUT, DELETE): Extract actual token from Header or Parameter
        String actualToken = request.getHeader(CSRF_HEADER_NAME);
        if (actualToken == null || actualToken.isBlank()) {
            actualToken = request.getParameter(CSRF_PARAM_NAME);
        }

        // 4. Validate Token
        if (actualToken != null &amp;&amp; actualToken.equals(sessionToken)) {
            // Token matches! Proceed to controller handler
            filterChain.doFilter(request, response);
        } else {
            // Token missing or mismatched! Reject forged request
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            response.setContentType("application/json");
            response.getWriter().write("""
                {
                    "error": "Forbidden",
                    "message": "Custom CSRF Filter: Invalid or missing CSRF token."
                }
                """);
        }
    }

    private String generateNewToken() {
        byte[] randomBytes = new byte[32];
        secureRandom.nextBytes(randomBytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
    }
}
</code></pre>
<p>The <code>CustomCsrfFilter</code> extends Spring's <code>OncePerRequestFilter</code>, ensuring the filter executes only once for each HTTP request. When a request arrives, it checks the user's session for a CSRF token. If no token exists, a new 256-bit cryptographically secure random token is generated using <code>SecureRandom</code> and stored in the session.</p>
<p>The filter then exposes the token as a request attribute using <code>request.setAttribute("csrfToken", sessionToken)</code>, allowing server-side template engines such as Thymeleaf to include it in hidden form fields. For safe HTTP methods (<code>GET</code>, <code>HEAD</code>, <code>OPTIONS</code>, and <code>TRACE</code>), the filter skips CSRF validation and immediately passes the request to the next filter since these methods shouldn't modify server state.</p>
<p>For state-changing requests such as <code>POST</code>, <code>PUT</code>, and <code>DELETE</code>, the filter retrieves the submitted CSRF token from either the <code>X-CSRF-TOKEN</code> request header (used by JavaScript clients) or the <code>_csrf</code> form parameter (used by HTML forms). It then compares this value with the token stored in the user's session. If the tokens match, the request proceeds normally. If the token is missing or invalid, the filter blocks the request by returning an <strong>HTTP 403 Forbidden</strong> response with a JSON error message.</p>
<h3 id="heading-step-2-register-the-custom-filter">Step 2: Register the Custom Filter</h3>
<pre><code class="language-java">package com.travelbuddy.config;

import com.travelbuddy.security.CustomCsrfFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class WebFilterConfig {

    @Bean
    public FilterRegistrationBean&lt;CustomCsrfFilter&gt; loggingFilter(CustomCsrfFilter filter) {
        FilterRegistrationBean&lt;CustomCsrfFilter&gt; registrationBean = new FilterRegistrationBean&lt;&gt;();
        registrationBean.setFilter(filter);
        registrationBean.addUrlPatterns("/api/*"); // Protect API endpoints
        return registrationBean;
    }
}
</code></pre>
<p>The <code>WebFilterConfig</code> class registers the custom <code>CustomCsrfFilter</code> using Spring Boot's <code>FilterRegistrationBean</code>, allowing the filter to be added to the underlying Servlet container without relying on Spring Security's filter chain. The <code>setFilter(filter)</code> method attaches the <code>CustomCsrfFilter</code> instance to the registration, while <code>addUrlPatterns("/api/*")</code> limits its execution to requests targeting <code>/api/*</code> endpoints. As a result, only API requests pass through the custom CSRF validation before reaching the application's <code>@RestController</code> methods.</p>
<h3 id="heading-compare-custom-filter-vs-spring-securitys-csrffilter">Compare Custom Filter vs. Spring Security's <code>CsrfFilter</code></h3>
<table style="min-width:75px"><colgroup><col style="min-width:25px"><col style="min-width:25px"><col style="min-width:25px"></colgroup><tbody><tr><td><p><strong>Feature</strong></p></td><td><p><strong>Our Custom Filter</strong></p></td><td><p><strong>Spring Security CsrfFilter</strong></p></td></tr><tr><td><p><strong>Token Generation</strong></p></td><td><p>Basic <code>SecureRandom</code> Base64 string</p></td><td><p>Cryptographically secure UUID / Custom generators</p></td></tr><tr><td><p><strong>BREACH Defense</strong></p></td><td><p>None (Raw token matching)</p></td><td><p>Masked Tokens (<code>XorCsrfTokenRequestAttributeHandler</code>)</p></td></tr><tr><td><p><strong>Storage Strategy</strong></p></td><td><p>Fixed <code>HttpSession</code></p></td><td><p>Pluggable (<code>HttpSession</code>, Cookie, Custom Repositories)</p></td></tr><tr><td><p><strong>Performance</strong></p></td><td><p>Immediate session creation</p></td><td><p>Lazy / Deferred token generation (Spring Security 6+)</p></td></tr><tr><td><p><strong>SPA Integration</strong></p></td><td><p>Manual header handling</p></td><td><p>Built-in <code>CookieCsrfTokenRepository</code></p></td></tr></tbody></table>

<p>Building this filter manually shows that Spring Security isn't magic. It performs the exact steps we built: checking HTTP methods, extracting tokens, and comparing request attributes against stored session state.</p>
<h2 id="heading-testing-csrf-protections">Testing CSRF Protections</h2>
<p>To verify that CSRF defenses are working correctly, you should know how to inspect, attack, and test your applications using various tools.</p>
<h3 id="heading-1-browser-devtools-inspection">1. Browser DevTools Inspection</h3>
<p>Open Chrome or Firefox DevTools (<code>F12</code>), navigate to the <strong>Application</strong> tab, and select <strong>Cookies</strong>:</p>
<ul>
<li><p>Inspect <code>JSESSIONID</code>: Verify that <code>HttpOnly</code> and <code>Secure</code> flags are set.</p>
</li>
<li><p>Inspect <code>SameSite</code> column: Verify whether <code>Lax</code> or <code>Strict</code> is active.</p>
</li>
</ul>
<p>In the <strong>Network</strong> tab, inspect a submitted <code>POST</code> request payload:</p>
<ul>
<li>Look for <code>_csrf</code> under Form Data, or <code>X-XSRF-TOKEN</code> under Request Headers.</li>
</ul>
<h3 id="heading-2-testing-via-curl">2. Testing via <code>curl</code></h3>
<p>Let's attempt a forged request using command-line <code>curl</code>.</p>
<h4 id="heading-test-attempt-a-submit-post-without-csrf-token-simulating-attacker">Test Attempt A: Submit POST without CSRF Token (Simulating Attacker)</h4>
<pre><code class="language-shell">curl -i -X POST https://travelbuddy.com/api/connections/add \
     -H "Cookie: JSESSIONID=abc123xyz789" \
     -d "service=SkyScanner"
</code></pre>
<p>Expected Response:</p>
<pre><code class="language-shell">HTTP/1.1 403 Forbidden
Content-Type: application/json

{"error":"Forbidden","message":"Invalid CSRF Token"}
</code></pre>
<h4 id="heading-test-attempt-b-fetch-token-and-submit-valid-request-legitimate-client-flow">Test Attempt B: Fetch Token and Submit Valid Request (Legitimate Client Flow)</h4>
<pre><code class="language-shell"># Step 1: Fetch session cookie and CSRF token from page
curl -i -c cookies.txt https://travelbuddy.com/connect-service

# Step 2: Extract token value from HTML, then submit POST request with Cookie + Token
curl -i -b cookies.txt -X POST https://travelbuddy.com/api/connections/add \
     -H "X-CSRF-TOKEN: CSRF-KEY-998877" \
     -d "service=SkyScanner"
</code></pre>
<p>Expected Response:</p>
<pre><code class="language-shell">HTTP/1.1 200 OK
Content-Type: application/json

{"status":"success","message":"Service connected successfully"}
</code></pre>
<h3 id="heading-3-why-postman-can-mislead-developers">3. Why Postman Can Mislead Developers</h3>
<p>Developers frequently report: <em>"I enabled CSRF protection in Spring Boot, but when I test my POST request in Postman, it succeeds without sending a CSRF token! Why?"</em></p>
<p>Postman is an API client, <strong>not a web browser</strong>. When you run a request in Postman, Postman doesn't maintain a cross-site sandbox, nor does it enforce Same Origin Policy or automatic ambient cookie injection unless explicitly configured.</p>
<p>If you don't manually attach a session cookie in Postman, the backend treats the Postman request as unauthenticated. If you use Postman's Interceptor cookie sync, Postman acts like a client explicitly sending parameters. Postman tests API contracts, but it doesn't simulate the browser's ambient authorization rules.</p>
<h3 id="heading-4-automated-integration-testing-with-spring-security-test">4. Automated Integration Testing with Spring Security Test</h3>
<p>In Java unit/integration tests, Spring Security provides test mock builders to simulate CSRF tokens effortlessly:</p>
<pre><code class="language-java">package com.travelbuddy.controller;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
class ConnectionControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    @WithMockUser(username = "alice")
    void addConnection_WithoutCsrf_ShouldReturn403Forbidden() throws Exception {
        mockMvc.perform(post("/api/connections/add")
                .param("service", "SkyScanner"))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(username = "alice")
    void addConnection_WithCsrf_ShouldSucceed() throws Exception {
        mockMvc.perform(post("/api/connections/add")
                .param("service", "SkyScanner")
                .with(csrf())) // Injects a valid mock CSRF token into request
                .andExpect(status().isOk());
    }
}
</code></pre>
<h2 id="heading-common-misconceptions">Common Misconceptions</h2>
<p>Let's dispel the seven most persistent myths surrounding CSRF.</p>
<h3 id="heading-myth-1-csrf-and-xss-are-the-same-thing">Myth 1: "CSRF and XSS are the same thing."</h3>
<p><strong>Fact:</strong> CSRF and XSS are completely different vulnerability vectors with opposite mechanisms:</p>
<ul>
<li><p><strong>XSS (Cross-Site Scripting):</strong> Attacker injects malicious JavaScript <em>into</em> your site to execute scripts inside your origin (stealing data, reading DOM, extracting local storage).</p>
</li>
<li><p><strong>CSRF (Cross-Site Request Forgery):</strong> Attacker tricks a victim's browser <em>on a different origin</em> into sending an HTTP request to your site. The attacker cannot read your site's DOM or steal cookies.</p>
</li>
</ul>
<h3 id="heading-myth-2-https-prevents-csrf-attacks">Myth 2: "HTTPS prevents CSRF attacks."</h3>
<p><strong>Fact:</strong> HTTPS encrypts the transport channel between the browser and server. It prevents wiretapping and man-in-the-middle attacks. But in a CSRF attack, the browser itself sends encrypted, valid HTTPS requests. Encrypting the pipe doesn't stop the browser from sending a forged request down that pipe.</p>
<h3 id="heading-myth-3-our-app-requires-authentication-so-were-safe-from-csrf">Myth 3: "Our app requires authentication, so we're safe from CSRF."</h3>
<p><strong>Fact:</strong> Authentication is what <strong>enables</strong> CSRF. CSRF specifically targets authenticated users because the browser automatically attaches their authenticated session cookies.</p>
<h3 id="heading-myth-4-our-api-uses-jwts-so-we-dont-have-to-worry-about-csrf">Myth 4: "Our API uses JWTs, so we don't have to worry about CSRF."</h3>
<p><strong>Fact:</strong> If your JWT is stored in an <code>HttpOnly</code> Cookie, you're fully vulnerable to CSRF because cookies are attached automatically. CSRF is a function of credential transmission mechanism (cookies), not credential payload structure (JWT vs Session ID).</p>
<h3 id="heading-myth-5-cors-blocks-cross-site-attacks">Myth 5: "CORS blocks cross-site attacks."</h3>
<p><strong>Fact:</strong> CORS controls response reading, not request execution. Simple requests (<code>application/x-www-form-urlencoded</code> HTML forms) execute state modifications on the backend long before CORS checks evaluate response headers.</p>
<h3 id="heading-myth-6-samesitelax-makes-csrf-tokens-obsolete">Myth 6: "SameSite=Lax makes CSRF tokens obsolete."</h3>
<p><strong>Fact:</strong> <code>SameSite=Lax</code> is an excellent defense, but top-level GET navigations still carry cookies, legacy browsers don't support it properly, and edge-case refresh windows exist. CSRF tokens remain necessary as defense-in-depth.</p>
<h3 id="heading-myth-7-attackers-can-read-our-csrf-token-from-the-html-form">Myth 7: "Attackers can read our CSRF token from the HTML form."</h3>
<p><strong>Fact:</strong> Same Origin Policy (SOP) strictly prevents JavaScript running on <code>evil.com</code> from fetching and reading HTML DOM nodes rendered from <code>travelbuddy.com</code>.</p>
<h2 id="heading-production-best-practices-checklist">Production Best Practices Checklist</h2>
<p>When deploying Spring Boot applications to production, follow this architectural security checklist:</p>
<h3 id="heading-1-identify-your-architecture-type">1. Identify Your Architecture Type</h3>
<ul>
<li><p><strong>Monolithic HTML Rendering (Thymeleaf, JSP):</strong> Use Synchronizer Token Pattern stored in <code>HttpSession</code>. Ensure all HTML forms include <code>_csrf</code> hidden fields.</p>
</li>
<li><p><strong>Single Page Application (React/Angular + Spring Boot API):</strong> Use Double Submit Cookie pattern (<code>CookieCsrfTokenRepository.withHttpOnlyFalse()</code>) combined with custom frontend request interceptors.</p>
</li>
<li><p><strong>Stateless Pure REST API (Machine-to-Machine / Native Mobile Apps using</strong> <code>Authorization: Bearer</code> <strong>headers):</strong> Disable CSRF (<code>.csrf(csrf -&gt; csrf.disable())</code>), because clients explicitly manage non-cookie tokens.</p>
</li>
</ul>
<h3 id="heading-2-cookie-security-flags">2. Cookie Security Flags</h3>
<p>Ensure every authentication cookie sets these attributes:</p>
<ul>
<li><p><code>Secure</code> = <code>true</code> (HTTPS only)</p>
</li>
<li><p><code>HttpOnly</code> = <code>true</code> (Prevents XSS token theft)</p>
</li>
<li><p><code>SameSite</code> = <code>Lax</code> or <code>Strict</code> (Browser-native cross-site blocking)</p>
</li>
</ul>
<h3 id="heading-3-keep-get-requests-read-only">3. Keep GET Requests Read-Only</h3>
<p>Audit your codebase to ensure no <code>@GetMapping</code> or <code>HttpServletRequest.getMethod().equals("GET")</code> handles database updates, account deletions, or password resets.</p>
<h3 id="heading-4-cross-origin-defense-layers">4. Cross-Origin Defense Layers</h3>
<p>Implement strict <code>Origin</code> and <code>Referer</code> header validation filters on state-modifying endpoints.</p>
<p>Also, deploy a robust Content Security Policy (CSP) header to reduce XSS risk (since XSS can be used to bypass CSRF defenses).</p>
<h3 id="heading-5-webhooks-and-external-callbacks">5. Webhooks and External Callbacks</h3>
<p>For server-to-server endpoints (such as Stripe or GitHub webhooks):</p>
<ul>
<li><p>Explicitly exempt webhook endpoints from standard CSRF filters in Spring Security (<code>ignoringRequestMatchers("/api/webhooks/**")</code>).</p>
</li>
<li><p>Secure webhooks using <strong>HMAC Signature Verification</strong> (<code>X-Hub-Signature-256</code>) instead of session cookies.</p>
</li>
</ul>
<h2 id="heading-final-summary-amp-defense-matrix">Final Summary &amp; Defense Matrix</h2>
<p>Cross-Site Request Forgery (CSRF) isn't a bug in browser design. It's an unintended consequence of web convenience: <strong>browsers automatically attach stored domain cookies to every outgoing request.</strong></p>
<p>When an attacker tricks a user into visiting a malicious origin (<code>evil.com</code>), the attacker relies on the browser's ambient authority to attach authenticated session credentials to a forged, state-changing request targeting your application (<code>travelbuddy.com</code>).</p>
<p>To prevent CSRF, modern web applications employ multi-layered security defenses working in tandem:</p>
<h3 id="heading-comprehensive-defense-matrix">Comprehensive Defense Matrix</h3>
<table style="min-width:125px"><colgroup><col style="min-width:25px"><col style="min-width:25px"><col style="min-width:25px"><col style="min-width:25px"><col style="min-width:25px"></colgroup><tbody><tr><td><p><strong>Defense Mechanism</strong></p></td><td><p><strong>Mechanism Layer</strong></p></td><td><p><strong>Primary Target / Action</strong></p></td><td><p><strong>Advantages</strong></p></td><td><p><strong>Limitations</strong></p></td></tr><tr><td><p><strong>Synchronizer Token Pattern</strong></p></td><td><p>Application Server</p></td><td><p>Binds unpredictable random token to server session. Verifies hidden form parameter.</p></td><td><p>Cryptographically bulletproof. Complete protection against cross-site forged requests.</p></td><td><p>Requires server-side session state (or state management).</p></td></tr><tr><td><p><strong>Double Submit Cookie Pattern</strong></p></td><td><p>Client + Server</p></td><td><p>Cookie value copied into custom HTTP header by JS. Verified server-side.</p></td><td><p>Fully stateless; ideal for SPAs (React/Angular) and microservices.</p></td><td><p>Requires non-HttpOnly cookie readable by JS. Vulnerable if subdomains are compromised.</p></td></tr><tr><td><p><code>SameSite=Lax / Strict</code><strong> Cookies</strong></p></td><td><p>Browser Engine</p></td><td><p>Instructs browser to strip cookies from cross-site requests.</p></td><td><p>Native browser enforcement. Zero server token storage required.</p></td><td><p>Legacy browser gaps. Doesn't protect state-modifying <code>GET</code> operations.</p></td></tr><tr><td><p><code>Origin</code><strong> / </strong><code>Referer</code><strong> Validation</strong></p></td><td><p>Application / Gateway</p></td><td><p>Checks incoming source headers against known server origins.</p></td><td><p>Stateless and extremely fast execution.</p></td><td><p>Headers can be stripped by privacy software/proxies.</p></td></tr><tr><td><p><strong>Bearer Tokens (</strong><code>Authorization</code><strong> Header)</strong></p></td><td><p>API Client</p></td><td><p>Token stored in <code>localStorage</code>. Attached explicitly via JS headers.</p></td><td><p>Completely immune to CSRF (no automatic browser attachment).</p></td><td><p>High risk of XSS token theft if <code>localStorage</code> is accessed by malicious scripts.</p></td></tr></tbody></table>

<p>By mastering these fundamental concepts (how browsers handle cookies, how origins operate, and how frameworks implement token validation) you can build backend architectures that are secure by design.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How OAuth 2.0 Works: A Practical Guide for Backend Developers ]]>
                </title>
                <description>
                    <![CDATA[ If you ask ten junior developers how OAuth 2.0 works, nine of them will start reciting terminology like "Authorization Server", "Bearer Tokens", "PKCE", and "Implicit Grant". They might also draw a se ]]>
                </description>
                <link>https://www.freecodecamp.org/news/oauth-2-0-guide-for-backend-developers/</link>
                <guid isPermaLink="false">6a69c4a5ad8b0e89e727f8a5</guid>
                
                    <category>
                        <![CDATA[ oauth ]]>
                    </category>
                
                    <category>
                        <![CDATA[ backend ]]>
                    </category>
                
                    <category>
                        <![CDATA[ authorization ]]>
                    </category>
                
                    <category>
                        <![CDATA[ authentication ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Ashutosh Krishna ]]>
                </dc:creator>
                <pubDate>Wed, 29 Jul 2026 09:15:17 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5fc16e412cae9c5b190b6cdd/a789ad3d-c2a7-43d6-9c14-ae13705072a4.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you ask ten junior developers how OAuth 2.0 works, nine of them will start reciting terminology like "Authorization Server", "Bearer Tokens", "PKCE", and "Implicit Grant". They might also draw a sequence diagram with six arrows crossing back and forth.</p>
<p>But if you ask them why a specific HTTP request exists or what breaks if you remove it, they often can't give a good explanation.</p>
<p>That's because OAuth is usually taught backward, in my opinion. Most tutorials start with definitions and sequence diagrams before establishing why the protocol was designed that way in the first place.</p>
<p>In this guide, we're going to fix that. We'll build up OAuth 2.0 concept by concept, starting from a real engineering problem and arriving at the protocol solutions naturally. By the time we write the code in Spring Boot, every parameter, redirect, and token will make total sense.</p>
<h3 id="heading-heres-what-well-cover">Here's What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-the-problem-before-oauth">The Problem Before OAuth</a></p>
</li>
<li><p><a href="#heading-what-oauth-20-actually-is-and-isnt">What OAuth 2.0 Actually Is (And Isn't)</a></p>
</li>
<li><p><a href="#heading-application-registration-where-credentials-come-from">Application Registration: Where Credentials Come From</a></p>
</li>
<li><p><a href="#heading-the-four-roles-in-oauth-20">The Four Roles in OAuth 2.0</a></p>
</li>
<li><p><a href="#heading-access-tokens-and-scopes">Access Tokens and Scopes</a></p>
</li>
<li><p><a href="#heading-the-authorization-code-flow">The Authorization Code Flow</a></p>
</li>
<li><p><a href="#heading-why-the-two-step-redirect-exists">Why The Two-Step Redirect Exists</a></p>
</li>
<li><p><a href="#heading-token-expiration-and-refresh-tokens">Token Expiration and Refresh Tokens</a></p>
</li>
<li><p><a href="#heading-pkce-protecting-public-clients">PKCE: Protecting Public Clients</a></p>
</li>
<li><p><a href="#heading-state-vs-pkce-stopping-different-attacks">state vs. PKCE: Stopping Different Attacks</a></p>
</li>
<li><p><a href="#heading-oauth-20-vs-openid-connect-oidc-amp-jwts">OAuth 2.0 vs. OpenID Connect (OIDC) &amp; JWTs</a></p>
</li>
<li><p><a href="#heading-what-is-oauth-21">What is OAuth 2.1?</a></p>
</li>
<li><p><a href="#heading-production-security-pitfalls-to-avoid">Production Security Pitfalls to Avoid</a></p>
</li>
<li><p><a href="#heading-final-thoughts">Final Thoughts</a></p>
</li>
</ul>
<h2 id="heading-the-problem-before-oauth">The Problem Before OAuth</h2>
<p>Imagine we're building <strong>TravelBuddy</strong>, a Spring Boot application that helps users plan trips.</p>
<p>TravelBuddy has a feature that automatically detects scheduling conflicts and inserts trip itineraries directly into the user's Google Calendar.</p>
<p>To do this, TravelBuddy needs access to Google Calendar's API. Specifically, it needs to read existing events and write new ones on behalf of the user, Alice.</p>
<p>How would we have solved this back in 2005 before OAuth existed?</p>
<h3 id="heading-the-password-sharing-anti-pattern">The Password Sharing Anti-Pattern</h3>
<p>Without a protocol like OAuth, TravelBuddy would've to ask Alice for her Google username and password.</p>
<img src="https://cdn.hashnode.com/uploads/covers/61c1acb4a90dea775da8262b/71470751-1c68-45b9-8ab1-14b229124670.png" alt="A linear flow showing Alice sending her full account credentials directly to TravelBuddy, which then forwards those credentials to the Google Calendar API. This pattern forces users to hand over total account control to third-party applications." style="display:block;margin:0 auto" width="1469" height="140" loading="lazy">

<p>Alice would type her Google password directly into TravelBuddy's UI. TravelBuddy would store her password in its database and use those credentials to log into Google whenever it needed to fetch or create calendar events.</p>
<p>This approach works, but it creates massive security and operational problems:</p>
<ol>
<li><p><strong>Over-privileged access:</strong> TravelBuddy only needs to manage calendar events. But because it has Alice's actual Google password, it can also read her Gmail, look at her Google Drive files, delete her photos, or change her account password. There's no way to give TravelBuddy <em>limited</em> access.</p>
</li>
<li><p><strong>No revocation granular control:</strong> If Alice wants to stop TravelBuddy from accessing her calendar, her only option is to change her Google password. Doing so breaks every other application she previously authorized.</p>
</li>
<li><p><strong>Storage liability for TravelBuddy:</strong> TravelBuddy is now storing plaintext or decryptable passwords for thousands of Google accounts. A single SQL injection or database leak on TravelBuddy's side compromises the master keys to its users' entire digital lives on Google.</p>
</li>
<li><p><strong>Phishing normalization:</strong> Training users to enter their primary Google credentials into third-party apps teaches them terrible security habits.</p>
</li>
</ol>
<p>We need a way for Alice to give TravelBuddy permission to perform specific actions on Google Calendar <em>without ever giving TravelBuddy her Google password</em>.</p>
<p>That capability is called <strong>delegated authorization</strong>, and that's precisely what OAuth 2.0 provides.</p>
<h2 id="heading-what-oauth-20-actually-is-and-isnt">What OAuth 2.0 Actually Is (And Isn't)</h2>
<p>OAuth 2.0 is an open standard for <strong>delegated authorization</strong>.</p>
<p>It provides a framework that allows a user to grant a third-party application limited access to their resources on another service without sharing their credentials.</p>
<p>Before moving forward, we must address the single most common misconception in web development.</p>
<h3 id="heading-authentication-vs-authorization">Authentication vs. Authorization</h3>
<p>Developers swap these terms constantly, but they answer two fundamentally different questions:</p>
<ul>
<li><p><strong>Authentication (AuthN):</strong> <em>Who are you?</em> (Identity)</p>
</li>
<li><p><strong>Authorization (AuthZ):</strong> <em>What are you allowed to do?</em> (Permissions)</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/61c1acb4a90dea775da8262b/f03b89c7-79e8-489e-9607-f5b2f5a65a5e.png" alt="A flowchart illustrating how Authentication precedes Authorization. The first box establishes identity (&quot;You are Alice&quot;), which feeds into the second box establishing permissions (&quot;Alice can read/write events, but cannot delete the calendar&quot;)." style="display:block;margin:0 auto" width="702" height="916" loading="lazy">

<p>OAuth 2.0 is <strong>strictly an authorization framework.</strong> It doesn't specify how to authenticate a user, how to issue identity details, or how to store user accounts. It only cares about issuing permission keys (tokens) so one service can talk to another on a user's behalf.</p>
<p>When you click "Log in with Google" on a website, that interaction uses an extension built <em>on top</em> of OAuth called OpenID Connect (OIDC), which we'll cover later. But core OAuth 2.0 is entirely about authorization.</p>
<h2 id="heading-application-registration-where-credentials-come-from">Application Registration: Where Credentials Come From</h2>
<p>Before TravelBuddy can initiate an OAuth flow, we must register TravelBuddy in the <strong>Google Cloud Console</strong>.</p>
<p>During registration, Google prompts TravelBuddy for two key details:</p>
<ol>
<li><p><strong>Application Name &amp; Logo:</strong> Presented on the user consent screen.</p>
</li>
<li><p><strong>Redirect URIs:</strong> The exact callback URLs (for example, <code>https://travelbuddy.com/login/oauth2/code/google</code>) where Google is permitted to send authorization codes.</p>
</li>
</ol>
<p>Once registered, Google issues two credentials to TravelBuddy:</p>
<ul>
<li><p><code>client_id</code><strong>:</strong> A public identifier (like a username) that identifies TravelBuddy. It;s safe to embed in public links or frontend code.</p>
</li>
<li><p><code>client_secret</code><strong>:</strong> A confidential key (like a password) used by TravelBuddy's backend server to authenticate itself when exchanging authorization codes for tokens.</p>
</li>
</ul>
<h2 id="heading-the-four-roles-in-oauth-20">The Four Roles in OAuth 2.0</h2>
<p>OAuth 2.0 defines four roles. Let's map them directly to our TravelBuddy example so these terms stop being abstract definitions.</p>
<img src="https://cdn.hashnode.com/uploads/covers/61c1acb4a90dea775da8262b/615ddf00-a380-45c7-a49b-2b613a7b36ff.png" alt="A diagram connecting the four entities: Alice (Resource Owner) grants permission to TravelBuddy (Client). Alice authenticates with Google's Authorization Server, which issues a token to TravelBuddy. TravelBuddy uses that token to request data from Google Calendar (Resource Server), which validates the token with the Authorization Server." style="display:block;margin:0 auto" width="2816" height="1536" loading="lazy">

<ul>
<li><p><strong>Resource Owner:</strong> The user who owns the data. In our example, this is <strong>Alice</strong>. She owns her Google Calendar.</p>
</li>
<li><p><strong>Client:</strong> The third-party application trying to access the user's data. In our example, this is <strong>TravelBuddy</strong> (our Spring Boot backend). It's called a "client" because it acts as a client to the API.</p>
</li>
<li><p><strong>Authorization Server:</strong> The server that authenticates the user, obtains their consent, and issues access tokens. In our example, this is <strong>Google's OAuth server</strong> (<code>accounts.google.com</code>).</p>
</li>
<li><p><strong>Resource Server:</strong> The server hosting the protected user data. In our example, this is the <strong>Google Calendar API</strong> (<code>www.googleapis.com/calendar</code>).</p>
</li>
</ul>
<p>Notice how Google's responsibilities are split into two separate roles: the Authorization Server (which issues tokens) and the Resource Server (which hosts the API). In large organizations, these are frequently separate services maintained by different teams.</p>
<h2 id="heading-access-tokens-and-scopes">Access Tokens and Scopes</h2>
<p>Instead of handing TravelBuddy her password, Alice approves the issuance of an <strong>Access Token</strong>.</p>
<p>An access token is a string of characters that acts like a temporary keycard. When TravelBuddy makes an HTTP request to Google Calendar, it presents this token in the headers.</p>
<p>An access token has three critical properties that passwords lack:</p>
<ol>
<li><p><strong>Limited Scope:</strong> It can only be used for specific permissions.</p>
</li>
<li><p><strong>Limited Lifetime:</strong> It expires automatically after a short period (typically minutes or hours).</p>
</li>
<li><p><strong>Revocable:</strong> Alice or Google can revoke the token at any point without impacting Alice's account password.</p>
</li>
</ol>
<h3 id="heading-what-is-a-scope">What is a Scope?</h3>
<p>A scope is a string that defines the exact permission being requested. Instead of asking for "Google account access", TravelBuddy asks for specific scopes.</p>
<p>When TravelBuddy redirects Alice to Google, it specifies the requested scopes:</p>
<ul>
<li><p>Read calendar events: <a href="https://www.googleapis.com/auth/calendar.events.readonly">https://www.googleapis.com/auth/calendar.events.readonly</a></p>
</li>
<li><p>Create/Edit calendar events: <a href="https://www.googleapis.com/auth/calendar.events">https://www.googleapis.com/auth/calendar.events</a></p>
</li>
</ul>
<p>When TravelBuddy redirects Alice to Google, it specifies the requested scopes. Google displays these exact permissions to Alice:</p>
<p>"TravelBuddy would like permission to view and edit your Google Calendar events."</p>
<p>If Alice agrees, the access token Google issues will be bound strictly to those requested scopes. If TravelBuddy tries to use that same token to read Alice's emails, Google's Resource Server will reject the request with a <code>403 Forbidden</code> status code.</p>
<h2 id="heading-the-authorization-code-flow">The Authorization Code Flow</h2>
<p>Now that we know the roles and tokens, how does TravelBuddy actually get an access token?</p>
<p>The standard, most secure flow for server-side applications (like our Spring Boot app) is the <strong>Authorization Code Flow</strong>.</p>
<p>Here's the sequence of events. We'll walk through every step in detail immediately after the diagram.</p>
<img src="https://cdn.hashnode.com/uploads/covers/61c1acb4a90dea775da8262b/1d792e3f-3d4b-4596-852b-b917e63c4c2d.png" alt="A nine-step sequence diagram detailing the authorization process. The user initiates the sync, gets redirected to Google to log in and consent, and receives a temporary Auth Code via browser redirect. TravelBuddy's backend exchanges that code and its Client Secret for an Access Token directly with Google, then calls the Calendar API using the Bearer token." style="display:block;margin:0 auto" width="2372" height="1278" loading="lazy">

<p>Let's break this down step-by-step.</p>
<h3 id="heading-step-1-user-initiates-action">Step 1: User Initiates Action</h3>
<p>Alice is using TravelBuddy's UI and clicks "Connect Google Calendar."</p>
<h3 id="heading-step-2-travelbuddy-constructs-redirect-url">Step 2: TravelBuddy Constructs Redirect URL</h3>
<p>TravelBuddy's backend doesn't prompt for credentials. Instead, it generates a URL pointing to Google's Authorization Server and instructs Alice's browser to redirect there.</p>
<p>This URL looks like this:</p>
<pre><code class="language-shell">GET https://accounts.google.com/o/oauth2/v2/auth?response_type=code&amp;client_id=TRAVELBUDDY_CLIENT_ID&amp;redirect_uri=https://travelbuddy.com/login/oauth2/code/google&amp;scope=https://www.googleapis.com/auth/calendar.events&amp;state=xyz123
</code></pre>
<p>Let's analyze what each parameter does:</p>
<ul>
<li><p><code>response_type=code</code>: Tells Google we're using the Authorization Code flow.</p>
</li>
<li><p><code>client_id</code>: A public identifier Google gave to TravelBuddy when TravelBuddy registered as a developer app.</p>
</li>
<li><p><code>redirect_uri</code>: The URL where Google should send Alice back once she completes consent.</p>
</li>
<li><p><code>scope</code>: The permissions TravelBuddy is asking for.</p>
</li>
<li><p><code>state</code>: A random string generated by TravelBuddy to prevent Cross-Site Request Forgery (CSRF) attacks.</p>
</li>
</ul>
<h3 id="heading-step-3-alice-authenticates-and-consents">Step 3: Alice Authenticates and Consents</h3>
<p>Alice's browser lands on Google's domain (<a href="http://accounts.google.com"><code>accounts.google.com</code></a>).</p>
<p>Google verifies whether Alice is logged in. If not, Google asks her to log in. <strong>TravelBuddy never sees this interaction.</strong></p>
<p>Once authenticated, Google displays the consent screen listing TravelBuddy's name and the requested scopes.</p>
<h3 id="heading-step-4-amp-5-google-issues-an-authorization-code">Step 4 &amp; 5: Google Issues an Authorization Code</h3>
<p>Alice clicks "Approve." Google's authorization server redirects Alice's browser back to TravelBuddy's registered <code>redirect_uri</code>, attaching a short-lived <strong>Authorization Code</strong> and the <code>state</code> parameter in the query string:</p>
<pre><code class="language-shell">GET https://travelbuddy.com/login/oauth2/code/google?code=4/0AX4XfWh...&amp;state=xyz123
</code></pre>
<p>TravelBuddy's backend verifies that the returned <code>state</code> matches what it originally sent. If it matches, TravelBuddy takes the <code>code</code>.</p>
<h3 id="heading-step-6-amp-7-travelbuddy-exchanges-the-code-for-a-token">Step 6 &amp; 7: TravelBuddy Exchanges the Code for a Token</h3>
<p>Now TravelBuddy's <strong>backend server</strong> makes a direct, server-to-server POST request to Google's token endpoint (<code>https://oauth2.googleapis.com/token</code>):</p>
<pre><code class="language-shell">POST /token HTTP/1.1
Host: oauth2.googleapis.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&amp;
code=4/0AX4XfWh...&amp;
redirect_uri=https://travelbuddy.com/login/oauth2/code/google&amp;
client_id=TRAVELBUDDY_CLIENT_ID&amp;
client_secret=TRAVELBUDDY_CLIENT_SECRET
</code></pre>
<p>Google validates the authorization code and TravelBuddy's <code>client_secret</code>. If valid, Google responds with a JSON payload containing the access token:</p>
<pre><code class="language-json">{
  "access_token": "ya29.a0ARrdaM...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "1//04rG...",
  "scope": "https://www.googleapis.com/auth/calendar.events"
}
</code></pre>
<h3 id="heading-step-8-amp-9-calling-the-api">Step 8 &amp; 9: Calling the API</h3>
<p>TravelBuddy now stores this access token securely and uses it to call the Google Calendar API on Alice's behalf:</p>
<pre><code class="language-shell">GET /calendar/v3/users/me/calendarList HTTP/1.1
Host: www.googleapis.com
Authorization: Bearer ya29.a0ARrdaM...
</code></pre>
<p>Google Calendar receives the request, extracts the Bearer token, checks with Google's auth infrastructure to verify it is valid and scoped correctly, and returns Alice's calendar data.</p>
<h2 id="heading-why-the-two-step-redirect-exists">Why The Two-Step Redirect Exists</h2>
<p>At this point, a junior developer almost always asks a great question:</p>
<blockquote>
<p><em>"Why do we have Step 4 and 6? Why doesn't Google just return the access token directly in the redirect back to the browser in Step 4?"</em></p>
</blockquote>
<p>Why bother returning a temporary <code>authorization_code</code> to the browser, only to immediately make another backend call to exchange it for the actual <code>access_token</code>?</p>
<p>The answer boils down to <strong>Front-Channel vs. Back-Channel security</strong>.</p>
<ul>
<li><p><strong>The Front-Channel (the browser):</strong> The browser is an untrusted, highly exposed environment. Redirect URIs pass through browser histories, system logs, referrer headers, and browser extensions. If Google returned an access token directly in the browser's URL, that high-privilege token could easily leak or be intercepted by malicious extensions.</p>
</li>
<li><p><strong>The Back-Channel (server-to-server):</strong> The direct HTTPS network call between TravelBuddy's backend server and Google's auth server is private and encrypted. It bypasses the browser completely.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/61c1acb4a90dea775da8262b/3771a305-01b5-4e0f-8732-5c74fbe2a01b.png" alt="A structural diagram separating the two network channels. The top box shows the Front-Channel, where the browser passes the exposed Authorization Code through URL redirects. The bottom box shows the secure Back-Channel, where TravelBuddy's server directly exchanges the Auth Code and Client Secret for tokens over encrypted server-to-server HTTPS." style="display:block;margin:0 auto" width="1563" height="1044" loading="lazy">

<p>The Authorization Code acts as a temporary, single-use ticket (usually expiring in under 60 seconds). Even if an attacker steals the authorization code from the browser's URL history, <strong>they can't exchange it for an access token because they don't possess TravelBuddy's</strong> <code>client_secret</code><strong>.</strong></p>
<h2 id="heading-token-expiration-and-refresh-tokens">Token Expiration and Refresh Tokens</h2>
<p>Access tokens are intentionally designed to be short-lived, typically expiring after one hour (<code>expires_in: 3600</code>).</p>
<p>Why? Because if an access token leaks, the window of opportunity for an attacker is strictly limited to whatever time remains before expiration.</p>
<p>But having Alice re-authenticate and click "Approve" every hour would offer a terrible user experience. TravelBuddy needs to sync calendars in the background while Alice is asleep.</p>
<p>To solve this, OAuth 2.0 introduces <strong>Refresh Tokens</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/61c1acb4a90dea775da8262b/0f235e3b-59ff-4973-8a3b-adc3517caec8.png" alt="A sequence diagram showing error recovery. TravelBuddy attempts an API call with an expired token and gets a 401 response. TravelBuddy sends a POST request with its Refresh Token to the Auth Server, receives a fresh Access Token, and retries the original API request successfully." style="display:block;margin:0 auto" width="1868" height="966" loading="lazy">

<h3 id="heading-how-refresh-tokens-work">How Refresh Tokens Work</h3>
<p>Depending on provider configuration (for example, passing <code>access_type=offline</code> and <code>prompt=consent</code> for Google), Google returns both an <code>access_token</code> and a long-lived <code>refresh_token</code> during the initial code exchange.</p>
<p>Then TravelBuddy encrypts and stores the <code>refresh_token</code> securely in its database.</p>
<p>When the <code>access_token</code> expires, TravelBuddy makes a background request directly to Google's token endpoint, presenting the <code>refresh_token</code> and <code>client_secret</code>.</p>
<p>Finally, Google validates the refresh token and issues a brand-new <code>access_token</code> without involving Alice at all.</p>
<table style="min-width:75px"><colgroup><col style="min-width:25px"><col style="min-width:25px"><col style="min-width:25px"></colgroup><tbody><tr><td><p><strong>Feature</strong></p></td><td><p><strong>Access Token</strong></p></td><td><p><strong>Refresh Token</strong></p></td></tr><tr><td><p><strong>Primary Purpose</strong></p></td><td><p>Used to access protected APIs</p></td><td><p>Used to obtain new access tokens</p></td></tr><tr><td><p><strong>Lifetime</strong></p></td><td><p>Very short (15 mins to 1 hour)</p></td><td><p>Long-lived (days, months, or until revoked)</p></td></tr><tr><td><p><strong>Sent Where?</strong></p></td><td><p>Sent with every API call to Resource Server</p></td><td><p>Sent ONLY to Authorization Server token endpoint</p></td></tr><tr><td><p><strong>Storage Security</strong></p></td><td><p>Can be kept in temporary server memory</p></td><td><p>Must be stored encrypted in secure storage</p></td></tr></tbody></table>

<h2 id="heading-pkce-protecting-public-clients">PKCE: Protecting Public Clients</h2>
<p>The Authorization Code flow we just discussed relies on TravelBuddy keeping its <code>client_secret</code> confidential. That is why TravelBuddy is classified as a <strong>Confidential Client</strong>. It runs on a server where developers can safely store environment variables and secrets.</p>
<p>But what if TravelBuddy is a Single Page Application (React/Vue running directly in the browser) or a Native Mobile App (iOS/Android)?</p>
<p>These are <strong>Public Clients</strong>. Anyone can open browser developer tools or decompile an Android <code>.apk</code> file to extract any embedded <code>client_secret</code>.</p>
<p>Without a secret, how can public clients safely use the Authorization Code flow? If a malicious app on a mobile device intercepts the authorization code, it could exchange that code for tokens because there is no <code>client_secret</code> stopping it.</p>
<p>To solve this, OAuth 2.0 introduced <strong>PKCE</strong> (Proof Key for Code Exchange, pronounced "pixie").</p>
<img src="https://cdn.hashnode.com/uploads/covers/61c1acb4a90dea775da8262b/05e945d7-a9a8-4de2-8ff3-20c955f704a5.png" alt="A sequence diagram illustrating PKCE. The client generates a secret code_verifier and hashes it into a code_challenge. It sends the challenge during authorization. When exchanging the Auth Code for tokens, it reveals the original code_verifier. The Auth Server hashes the verifier and compares it against the challenge to verify client identity without requiring a client secret." style="display:block;margin:0 auto" width="2156" height="1108" loading="lazy">

<h3 id="heading-how-pkce-works">How PKCE Works</h3>
<p>Before starting the flow, the client generates a cryptographic, random string called the <code>code_verifier</code>. The client hashes this string (typically using SHA-256) to produce the <code>code_challenge</code>.</p>
<p>In Step 2 of the Auth flow, the client sends the <code>code_challenge</code> and hash method <code>code_challenge_method=S256</code>) to the Authorization Server. The Authorization Server stores the <code>code_challenge</code> and returns the authorization code as usual.</p>
<p>In Step 6, when exchanging the code for tokens, the client sends the original <code>unhashed code_verifier</code>.</p>
<p>The Authorization Server hashes the provided <code>code_verifier</code> using SHA-256 and checks if it matches the stored <code>code_challenge</code>. If it matches, it proves that the app requesting the token is the exact same app that initiated the request.</p>
<p><strong>Note</strong>: Modern OAuth security guidelines recommend using PKCE for all applications, including confidential backend applications like Spring Boot.</p>
<h2 id="heading-state-vs-pkce-stopping-different-attacks"><code>state</code> vs. PKCE: Stopping Different Attacks</h2>
<p>Developers often confuse <code>state</code> and <code>PKCE</code> because both involve random strings sent during the OAuth flow. But they protect against completely different attack vectors:</p>
<table style="min-width:75px"><colgroup><col style="min-width:25px"><col style="min-width:25px"><col style="min-width:25px"></colgroup><tbody><tr><td><p><strong>Property</strong></p></td><td><p><code>state</code><strong> Parameter</strong></p></td><td><p><code>PKCE</code><strong> (code_verifier)</strong></p></td></tr><tr><td><p><strong>Primary Threat</strong></p></td><td><p><strong>Login CSRF:</strong> An attacker tricks a victim into completing an OAuth flow using the <em>attacker's</em> authorization code.</p></td><td><p><strong>Code Interception:</strong> An attacker steals a victim's authorization code and exchanges it for a token.</p></td></tr><tr><td><p><strong>How It Protects</strong></p></td><td><p>Binds the authorization callback to the user's specific browser session.</p></td><td><p>Proves that the entity exchanging the code is the same entity that requested it.</p></td></tr><tr><td><p><strong>Validation Point</strong></p></td><td><p>Checked by the <strong>Client Application Backend</strong> upon callback.</p></td><td><p>Checked by the <strong>Authorization Server</strong> at the <code>/token</code> endpoint.</p></td></tr></tbody></table>

<h2 id="heading-oauth-20-vs-openid-connect-oidc-amp-jwts">OAuth 2.0 vs. OpenID Connect (OIDC) &amp; JWTs</h2>
<p>Earlier, I emphasized that OAuth 2.0 is purely for <strong>authorization</strong> (permissions), not <strong>authentication</strong> (identity).</p>
<p>Yet, almost every app you use has a "Sign in with Google" button. How does that work?</p>
<h3 id="heading-enter-openid-connect-oidc">Enter OpenID Connect (OIDC)</h3>
<p>OpenID Connect is an identity layer built directly on top of OAuth 2.0.</p>
<p>While core OAuth issues an <code>access_token</code> meant for an API, requesting the <code>openid</code> scope instructs the Authorization Server to issue an <strong>ID Token</strong> alongside the access token:</p>
<img src="https://cdn.hashnode.com/uploads/covers/61c1acb4a90dea775da8262b/0089d542-a859-45c0-874e-19f54d85e431.png" alt="A nested architecture diagram showing OpenID Connect as an outer identity layer wrapping core OAuth 2.0. OAuth 2.0 handles Access Tokens for API permissions, while OIDC adds the ID Token (JWT) to convey user identity information." style="display:block;margin:0 auto" width="940" height="688" loading="lazy">

<p>When TravelBuddy requests the <code>openid</code> scope alongside calendar permissions:</p>
<pre><code class="language-shell">scope=openid profile email https://www.googleapis.com/auth/calendar.events
</code></pre>
<p>Google's token endpoint responds with both an <code>access_token</code> AND an <code>id_token</code>.</p>
<ul>
<li><p><strong>Access Token:</strong> Intended for the Resource Server (Google Calendar). TravelBuddy doesn't need to read its contents. It just passes it along in headers.</p>
</li>
<li><p><strong>ID Token:</strong> Intended specifically for TravelBuddy. It contains cryptographically signed information about Alice (for example, her Google User ID, full name, email, and profile picture URL).</p>
</li>
</ul>
<h3 id="heading-what-is-a-jwt">What is a JWT?</h3>
<p>An ID Token is almost always formatted as a <strong>JWT</strong> (JSON Web Token, pronounced "jot").</p>
<p>A JWT is a compact, URL-safe string containing three parts separated by dots: <code>Header.Payload.Signature</code></p>
<pre><code class="language-shell">eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
</code></pre>
<p>Decoding the middle section (Payload) reveals plain JSON:</p>
<pre><code class="language-json">{
  "sub": "google-user-id-98765",
  "iss": "https://accounts.google.com",
  "aud": "TRAVELBUDDY_CLIENT_ID",
  "email": "alice@gmail.com",
  "exp": 1711900000
}
</code></pre>
<p>Base64URL encoding is <strong>not encryption</strong>. Anyone who holds a JWT can read its contents. But because the token is signed using Google's private key, TravelBuddy's backend can verify Google's public signature locally without needing to call Google's servers to confirm Alice's identity on every request.</p>
<h3 id="heading-is-an-access-token-always-a-jwt">Is an Access Token always a JWT?</h3>
<p><strong>No.</strong> OAuth 2.0 intentionally doesn't mandate any specific format for access tokens.</p>
<p>An access token can be:</p>
<ol>
<li><p><strong>An Opaque Token:</strong> A completely random string (for example, <code>ya29.a0ARrdaM...</code>). The client and resource server must look up its meaning by querying the authorization server.</p>
</li>
<li><p><strong>A Structured Token (like a JWT):</strong> Contains embedded claims so the resource server can validate it self-sufficiently.</p>
</li>
</ol>
<h2 id="heading-what-is-oauth-21">What is OAuth 2.1?</h2>
<p>If you work with modern security guidelines, you'll hear about <strong>OAuth 2.1</strong>.</p>
<p>OAuth 2.1 is not an overhaul of OAuth 2.0. It's a consolidation draft that incorporates years of security best practices into a single specification:</p>
<ol>
<li><p><strong>Mandatory PKCE:</strong> PKCE is required for <em>all</em> Authorization Code flows, including confidential server-side apps like Spring Boot.</p>
</li>
<li><p><strong>Deprecation of Legacy Grants:</strong> The Implicit Grant (which returned tokens directly in browser URLs) and the Resource Owner Password Credentials Grant (which collected passwords directly) are removed entirely.</p>
</li>
<li><p><strong>Exact Redirect URI Matching:</strong> Wildcards in redirect URIs are prohibited to prevent open-redirect exploits.</p>
</li>
</ol>
<h2 id="heading-production-security-pitfalls-to-avoid">Production Security Pitfalls to Avoid</h2>
<p>Building OAuth integrations in production requires careful attention to detail. Here are five of the most common security mistakes backend engineers make and how to avoid them:</p>
<h3 id="heading-1-storing-tokens-in-browser-localstorage">1. Storing Tokens in Browser <code>localStorage</code></h3>
<p>If you're building a SPA client that receives access or refresh tokens, never store tokens in <code>localStorage</code> or <code>sessionStorage</code>.</p>
<p>Any script running on your page, including third-party analytics, chat widgets, or compromised npm dependencies, can read <code>localStorage</code> through a Cross-Site Scripting (XSS) vulnerability.</p>
<p><strong>Fix:</strong> Store tokens inside HTTP-Only, Secure, SameSite cookies managed by your backend, or use a backend-for-frontend (BFF) architecture where tokens never reach the browser at all.</p>
<h3 id="heading-2-leaking-the-client-secret">2. Leaking the Client Secret</h3>
<p>It sounds obvious, but <code>client_secret</code> strings end up in public GitHub repositories constantly. Remember: any secret included in Android/iOS apps, React single-page apps, or frontend code is public.</p>
<p><strong>Fix:</strong> Keep client secrets inside environment variables on server-side environments. Use PKCE for public clients where secrets can't be protected.</p>
<h3 id="heading-3-requesting-unnecessary-scopes-scope-creep">3. Requesting Unnecessary Scopes (Scope Creep)</h3>
<p>Asking for full account access when you only need read permission makes users suspicious and increases your liability if a token leaks.</p>
<p><strong>Fix:</strong> Follow the principle of least privilege. Request only the specific scopes your app needs immediately. If TravelBuddy later adds a feature to analyze emails, request the Gmail scope dynamically when the user activates that feature.</p>
<h3 id="heading-4-assuming-an-oauth-token-proves-identity">4. Assuming an OAuth Token Proves Identity</h3>
<p>Just because an app receives an <code>access_token</code> from an API doesn't mean it can treat that token as proof of who logged in.</p>
<p>If an attacker passes a valid access token obtained from a different application (a confused deputy attack), your system might accept it if it only checks token validity without checking who the token was issued to <code>aud</code> / audience claim).</p>
<p>Fix: Use OpenID Connect (and validate the <code>id_token</code> claims including <code>aud</code> and <code>iss</code>) when authenticating users.</p>
<h3 id="heading-5-skipping-state-or-pkce-validation">5. Skipping <code>state</code> or <code>PKCE</code> Validation</h3>
<p>If you manually build OAuth flows without checking the <code>state</code> parameter, your application is vulnerable to Login Cross-Site Request Forgery (CSRF). An attacker could trick a user's browser into completing an OAuth flow using the attacker's authorization code, linking the victim's session to the attacker's account data.</p>
<p>Fix: Always generate a cryptographically strong, non-guessable <code>state</code> parameter bound to the user's session, or rely on established security frameworks like Spring Security that enforce this automatically.</p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>OAuth 2.0 can feel overwhelming when viewed entirely as a web of specs, RFCs, and terminology.</p>
<p>When you strip away the jargon, OAuth solves one core problem: allowing a user to give an application permission to access their data without handing over their password.</p>
<p>Every moving part in the protocol exists to support that core mission safely:</p>
<ul>
<li><p><strong>Scopes</strong> restrict permissions.</p>
</li>
<li><p><strong>Access Tokens</strong> provide temporary, revocable access.</p>
</li>
<li><p><strong>Authorization Codes</strong> keep tokens out of vulnerable browser URLs.</p>
</li>
<li><p><strong>Refresh Tokens</strong> maintain long-term access without harassing the user.</p>
</li>
<li><p><strong>PKCE</strong> protects public applications that can't keep secrets.</p>
</li>
<li><p><strong>OpenID Connect</strong> adds a standardized identity layer on top.</p>
</li>
</ul>
<p>The next time you integrate an OAuth provider in Spring Boot or debug a token error in production, don't focus on memorizing the diagrams. Look at the HTTP request, ask which specific boundary it is crossing, and the design choices will make immediate sense.</p>
<details>
<summary>Summary Glossary</summary>
<ul><li><p><strong>Authorization Code:</strong> A short-lived, single-use ticket returned via browser redirect, exchanged server-side for access tokens.</p></li><li><p><strong>Access Token:</strong> A temporary keycard used in HTTP headers to access protected resources.</p></li><li><p><strong>Refresh Token:</strong> A long-lived credential used strictly at the token endpoint to obtain new access tokens.</p></li><li><p><strong>Scope:</strong> A string specifying granular permissions requested by the client.</p></li><li><p><strong>PKCE:</strong> Proof Key for Code Exchange. A cryptographic technique binding token exchange to the initiating client instance.</p></li><li><p><strong>OpenID Connect (OIDC):</strong> An identity layer built on top of OAuth 2.0 that issues an <code>id_token</code> containing user profile details.</p></li><li><p><strong>JWT:</strong> JSON Web Token, a compact, digitally signed format commonly used for ID tokens.</p></li></ul>
</details> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Authenticate Users in Kubernetes: x509 Certificates, OIDC, and Cloud Identity ]]>
                </title>
                <description>
                    <![CDATA[ Kubernetes doesn't know who you are. It has no user database, no built-in login system, no password file. When you run kubectl get pods, Kubernetes receives an HTTP request and asks one question: who  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-authenticate-users-in-kubernetes-x509-certificates-oidc-and-cloud-identity/</link>
                <guid isPermaLink="false">69d4182f40c9cabf4484dbdb</guid>
                
                    <category>
                        <![CDATA[ Kubernetes ]]>
                    </category>
                
                    <category>
                        <![CDATA[ authentication ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Cloud Computing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Destiny Erhabor ]]>
                </dc:creator>
                <pubDate>Mon, 06 Apr 2026 20:31:43 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/36356282-0cfb-43a8-8461-84f20e64b041.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Kubernetes doesn't know who you are.</p>
<p>It has no user database, no built-in login system, no password file. When you run <code>kubectl get pods</code>, Kubernetes receives an HTTP request and asks one question: who signed this, and do I trust that signature? Everything else — what you're allowed to do, which namespaces you can access, whether your request goes through at all — comes after that question is answered.</p>
<p>This surprises most engineers who are new to Kubernetes. They expect something like a database of users with passwords. Instead, they find a pluggable chain of authenticators, each one able to vouch for a request in a different way:</p>
<ul>
<li><p>Client certificates</p>
</li>
<li><p>OIDC tokens from an external identity provider</p>
</li>
<li><p>Cloud provider IAM tokens</p>
</li>
<li><p>Service account tokens projected into pods.</p>
</li>
</ul>
<p>Any of these can be active at the same time.</p>
<p>Understanding this model is what separates engineers who can debug authentication failures from engineers who copy kubeconfig files and hope for the best.</p>
<p>In this article, you'll work through how the Kubernetes authentication chain works from first principles. You'll see how x509 client certificates are used — and why they're a poor choice for human users in production. You'll configure OIDC authentication with Dex, giving your cluster a real browser-based login flow. And you'll see how AWS, GCP, and Azure each plug into the same underlying model.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p>A running kind cluster — a fresh one works fine, or reuse an existing one</p>
</li>
<li><p><code>kubectl</code> and <code>helm</code> installed</p>
</li>
<li><p><code>openssl</code> available on your machine (comes pre-installed on macOS and most Linux distros)</p>
</li>
<li><p>Basic familiarity with what a JWT is (a signed JSON object with claims) — you don't need to be able to write one, just recognise one</p>
</li>
</ul>
<p>All demo files are in the <a href="https://github.com/Caesarsage/DevOps-Cloud-Projects/tree/main/intermediate/k8/security">companion GitHub repository</a>.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-how-kubernetes-authentication-works">How Kubernetes Authentication Works</a></p>
<ul>
<li><p><a href="#heading-the-authenticator-chain">The Authenticator Chain</a></p>
</li>
<li><p><a href="#heading-users-vs-service-accounts">Users vs Service Accounts</a></p>
</li>
<li><p><a href="#heading-what-happens-after-authentication">What Happens After Authentication</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-how-to-use-x509-client-certificates">How to Use x509 Client Certificates</a></p>
<ul>
<li><p><a href="#heading-how-the-certificate-maps-to-an-identity">How the Certificate Maps to an Identity</a></p>
</li>
<li><p><a href="#the-cluster-ca">The Cluster CA</a></p>
</li>
<li><p><a href="#heading-the-limits-of-certificate-based-auth">The Limits of Certificate-Based Auth</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-demo-1--create-and-use-an-x509-client-certificate">Demo 1 — Create and Use an x509 Client Certificate</a></p>
</li>
<li><p><a href="#heading-how-to-set-up-oidc-authentication">How to Set Up OIDC Authentication</a></p>
<ul>
<li><p><a href="#heading-how-the-oidc-flow-works-in-kubernetes">How the OIDC Flow Works in Kubernetes</a></p>
</li>
<li><p><a href="#heading-the-api-server-configuration">The API Server Configuration</a></p>
</li>
<li><p><a href="#heading-jwt-claims-kubernetes-uses">JWT Claims Kubernetes Uses</a></p>
</li>
<li><p><a href="#heading-how-kubelogin-works">How kubelogin Works</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-demo-2--configure-oidc-login-with-dex-and-kubelogin">Demo 2 — Configure OIDC Login with Dex and kubelogin</a></p>
</li>
<li><p><a href="#heading-cloud-provider-authentication">Cloud Provider Authentication</a></p>
<ul>
<li><p><a href="#heading-aws-eks">AWS EKS</a></p>
</li>
<li><p><a href="#heading-google-gke">Google GKE</a></p>
</li>
<li><p><a href="#heading-azure-aks">Azure AKS</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-webhook-token-authentication">Webhook Token Authentication</a></p>
</li>
<li><p><a href="#heading-cleanup">Cleanup</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-how-kubernetes-authentication-works">How Kubernetes Authentication Works</h2>
<p>Every request that reaches the Kubernetes API server — whether from <code>kubectl</code>, a pod, a controller, or a CI pipeline — carries a credential of some kind.</p>
<p>The API server passes that credential through a chain of authenticators in sequence. The first authenticator that can verify the credential wins. If none can, the request is treated as anonymous.</p>
<h3 id="heading-the-authenticator-chain">The Authenticator Chain</h3>
<p>Kubernetes supports several authentication strategies simultaneously. You can have client certificate authentication and OIDC authentication active on the same cluster at the same time, which is common in production: cluster administrators use certificates, regular developers use OIDC. The strategies active on a cluster are determined by flags passed to the <code>kube-apiserver</code> process.</p>
<p>The strategies available are x509 client certificates, bearer tokens (static token files — rarely used in production), bootstrap tokens (used during node join operations), service account tokens, OIDC tokens, authenticating proxies, and webhook token authentication. A cluster doesn't have to use all of them, and most don't. But knowing they all exist helps when you're diagnosing an auth failure.</p>
<h3 id="heading-users-vs-service-accounts">Users vs Service Accounts</h3>
<p>There is an important distinction in how Kubernetes thinks about identity. Service accounts are Kubernetes objects — they live in a namespace, get created with <code>kubectl create serviceaccount</code>, and have tokens managed by the cluster itself. Every pod runs as a service account. These are machine identities for workloads.</p>
<p>Users, on the other hand, don't exist as Kubernetes objects at all. There is no <code>kubectl create user</code> command. Kubernetes doesn't manage user accounts. Instead, it trusts external systems to assert user identity — a certificate authority, an OIDC provider, or a cloud provider's IAM system. Kubernetes just verifies the assertion and extracts the username and group memberships from it.</p>
<table>
<thead>
<tr>
<th></th>
<th>Service Account</th>
<th>User</th>
</tr>
</thead>
<tbody><tr>
<td>Kubernetes object?</td>
<td>Yes — lives in a namespace</td>
<td>No — managed externally</td>
</tr>
<tr>
<td>Created with</td>
<td><code>kubectl create serviceaccount</code></td>
<td>External system (CA, IdP, cloud IAM)</td>
</tr>
<tr>
<td>Used by</td>
<td>Pods and workloads</td>
<td>Humans and CI systems</td>
</tr>
<tr>
<td>Token managed by</td>
<td>Kubernetes</td>
<td>External system</td>
</tr>
<tr>
<td>Namespaced?</td>
<td>Yes</td>
<td>No</td>
</tr>
</tbody></table>
<h3 id="heading-what-happens-after-authentication">What Happens After Authentication</h3>
<p>Authentication only answers one question: who is this? Once the API server has a verified identity — a username and zero or more group memberships — it passes the request to the authorisation layer. By default that is RBAC, which checks the identity against Role and ClusterRole bindings to determine what the request is allowed to do.</p>
<p>This is why authentication and authorisation are separate concerns in Kubernetes. A valid certificate gets you past the front door. What you can do inside is RBAC's job. An authenticated user with no RBAC bindings can authenticate successfully but will be denied every API call.</p>
<p>If you want a deep dive into how RBAC rules, roles, and bindings work, check out this handbook on <a href="https://www.freecodecamp.org/news/how-to-secure-a-kubernetes-cluster-handbook/">How to Secure a Kubernetes Cluster: RBAC, Pod Hardening, and Runtime Protection</a>.</p>
<h2 id="heading-how-to-use-x509-client-certificates">How to Use x509 Client Certificates</h2>
<p>x509 client certificate authentication is the oldest and simplest authentication method in Kubernetes. It's how <code>kubectl</code> works out of the box when you create a cluster — the kubeconfig file that <code>kind</code> or <code>kubeadm</code> generates contains an embedded client certificate signed by the cluster's Certificate Authority.</p>
<h3 id="heading-how-the-certificate-maps-to-an-identity">How the Certificate Maps to an Identity</h3>
<p>When the API server receives a request with a client certificate, it validates the certificate against its trusted CA, then reads two fields (The Common Name and Organization) from the certificate to construct an identity.</p>
<p>The <strong>Common Name (CN)</strong> field becomes the username. The <strong>Organization (O)</strong> field, which can contain multiple values, becomes the list of groups the user belongs to.</p>
<p>So a certificate with <code>CN=jane</code> and <code>O=engineering</code> authenticates as username <code>jane</code> in group <code>engineering</code>. If you want to give <code>jane</code> permissions, you create a RoleBinding that references either the username <code>jane</code> or the group <code>engineering</code> as a subject.</p>
<p>This is the same mechanism behind <code>system:masters</code>. When <code>kind</code> creates a cluster and writes a kubeconfig for you, it generates a certificate with <code>O=system:masters</code>. Kubernetes has a built-in ClusterRoleBinding that grants <code>cluster-admin</code> to anyone in the <code>system:masters</code> group. That's why your default kubeconfig has full admin access — it's not magic, it's a certificate with the right group.</p>
<h3 id="heading-the-cluster-ca">The Cluster CA</h3>
<p>Every Kubernetes cluster has a root Certificate Authority — a private key and a self-signed certificate that the API server trusts. Any client certificate signed by this CA is trusted by the cluster.</p>
<p>The CA certificate and key are typically stored in <code>/etc/kubernetes/pki/</code> on the control plane node, or in the <code>kube-system</code> namespace as a secret, depending on how the cluster was created.</p>
<p>On kind clusters, you can copy the CA cert and key directly from the control plane container:</p>
<pre><code class="language-bash">docker cp k8s-security-control-plane:/etc/kubernetes/pki/ca.crt ./ca.crt
docker cp k8s-security-control-plane:/etc/kubernetes/pki/ca.key ./ca.key
</code></pre>
<p>Whoever holds the CA key can issue certificates for any username and any group, including <code>system:masters</code>. This makes the CA key the most sensitive secret in a Kubernetes cluster. Guard it accordingly.</p>
<h3 id="heading-the-limits-of-certificate-based-auth">The Limits of Certificate-Based Auth</h3>
<p>Client certificates work, but they have two fundamental problems that make them a poor choice for human users in production.</p>
<p>The first is that <strong>Kubernetes doesn't check certificate revocation lists (CRLs)</strong>. If a developer's kubeconfig is stolen, the embedded certificate remains valid until it expires — which is typically one year in most Kubernetes setups. There's no way to immediately invalidate it. You can't "log out" a certificate. The only mitigation is to rotate the entire cluster CA, which invalidates every certificate including those belonging to other legitimate users.</p>
<p>The second is <strong>operational overhead</strong>. Certificates must be generated, distributed to users, and rotated before expiry. There's no self-service. In a team of ten engineers, managing certificates is annoying. In a team of a hundred, it's a full-time job.</p>
<p>For human access in production, OIDC is the right answer: short-lived tokens issued by a trusted identity provider, with a central revocation mechanism, and a standard browser-based login flow. Certificates are fine for service accounts and automation, where token management can be automated and rotation is handled programmatically.</p>
<p>That said, understanding certificates isn't optional. Your kubeconfig uses one. Your CI system probably does too. And cert-based auth is what you fall back to when everything else breaks.</p>
<h2 id="heading-demo-1-create-and-use-an-x509-client-certificate">Demo 1 — Create and Use an x509 Client Certificate</h2>
<p>In this section, you'll generate a user certificate signed by the cluster CA, bind it to an RBAC role, and use it to authenticate to the cluster as a different user.</p>
<p><strong>This guide is for local development and learning only.</strong> Manually signing certificates with the cluster CA and storing keys on disk is done here for simplicity.</p>
<p>In production, you should use the Kubernetes CertificateSigningRequest API or cert-manager for certificate issuance, enforce short-lived certificates with automatic rotation, and store private keys in a secrets manager (HashiCorp Vault, AWS Secrets Manager) or hardware security module (HSM) — never distribute the cluster CA key.</p>
<h3 id="heading-step-1-copy-the-ca-cert-and-key-from-the-kind-control-plane">Step 1: Copy the CA cert and key from the kind control plane</h3>
<pre><code class="language-bash">docker cp k8s-security-control-plane:/etc/kubernetes/pki/ca.crt ./ca.crt
docker cp k8s-security-control-plane:/etc/kubernetes/pki/ca.key ./ca.key
</code></pre>
<p>This will create two files in your current directory called <code>ca.crt</code> and <code>ca.key</code></p>
<h3 id="heading-step-2-generate-a-private-key-and-csr-for-a-new-user">Step 2: Generate a private key and CSR for a new user</h3>
<p>You're creating a certificate for a user named <code>jane</code> in the <code>engineering</code> group:</p>
<pre><code class="language-bash"># Generate the private key
openssl genrsa -out jane.key 2048

# Generate a Certificate Signing Request
# CN = username, O = group
openssl req -new \
  -key jane.key \
  -out jane.csr \
  -subj "/CN=jane/O=engineering"
</code></pre>
<h3 id="heading-step-3-sign-the-csr-with-the-cluster-ca">Step 3: Sign the CSR with the cluster CA</h3>
<pre><code class="language-bash">openssl x509 -req \
  -in jane.csr \
  -CA ca.crt \
  -CAkey ca.key \
  -CAcreateserial \
  -out jane.crt \
  -days 365
</code></pre>
<p>Expected output:</p>
<pre><code class="language-plaintext">Certificate request self-signature ok
subject=CN=jane, O=engineering
</code></pre>
<h3 id="heading-step-4-inspect-the-certificate">Step 4: Inspect the certificate</h3>
<p>Before using it, confirm the identity it carries:</p>
<pre><code class="language-bash">openssl x509 -in jane.crt -noout -subject -dates
</code></pre>
<pre><code class="language-plaintext">subject=CN=jane, O=engineering
notBefore=Mar 20 10:00:00 2024 GMT
notAfter=Mar 20 10:00:00 2025 GMT
</code></pre>
<p>One year from now, this certificate becomes invalid and must be replaced. There's no way to extend it — you have to issue a new one.</p>
<h3 id="heading-step-5-build-a-kubeconfig-entry-for-jane">Step 5: Build a kubeconfig entry for jane</h3>
<pre><code class="language-bash"># Get the cluster API server address from the current context
APISERVER=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')

# Create a kubeconfig for jane
kubectl config set-cluster k8s-security \
  --server=$APISERVER \
  --certificate-authority=ca.crt \
  --embed-certs=true \
  --kubeconfig=jane.kubeconfig

kubectl config set-credentials jane \
  --client-certificate=jane.crt \
  --client-key=jane.key \
  --embed-certs=true \
  --kubeconfig=jane.kubeconfig

kubectl config set-context jane@k8s-security \
  --cluster=k8s-security \
  --user=jane \
  --kubeconfig=jane.kubeconfig

kubectl config use-context jane@k8s-security \
  --kubeconfig=jane.kubeconfig
</code></pre>
<h3 id="heading-step-6-test-authentication-before-rbac">Step 6: Test authentication — before RBAC</h3>
<p>Try to list pods using jane's kubeconfig:</p>
<pre><code class="language-bash">kubectl get pods -n staging --kubeconfig=jane.kubeconfig
</code></pre>
<pre><code class="language-plaintext">Error from server (Forbidden): pods is forbidden: User "jane" cannot list
resource "pods" in API group "" in the namespace "staging"
</code></pre>
<p>This is correct. Jane authenticated successfully — Kubernetes knows who she is. But she has no RBAC bindings, so every API call is denied. Authentication passed, but authorisation failed.</p>
<h3 id="heading-step-7-grant-jane-access-with-rbac">Step 7: Grant jane access with RBAC</h3>
<p>RBAC bindings use the username exactly as it appears in the certificate's CN field. If you need a refresher on how Roles, ClusterRoles, and RoleBindings work, this handbook <a href="https://www.freecodecamp.org/news/how-to-secure-a-kubernetes-cluster-handbook/">How to Secure a Kubernetes Cluster: RBAC, Pod Hardening, and Runtime Protection</a> covers the full RBAC model. For now, a simple RoleBinding using the built-in <code>view</code> ClusterRole is enough:</p>
<pre><code class="language-yaml"># jane-rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: jane-reader
  namespace: staging
subjects:
  - kind: User
    name: jane          # matches the CN in the certificate
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: view
  apiGroup: rbac.authorization.k8s.io
</code></pre>
<pre><code class="language-bash">kubectl apply -f jane-rolebinding.yaml
kubectl get pods -n staging --kubeconfig=jane.kubeconfig
</code></pre>
<pre><code class="language-plaintext">No resources found in staging namespace.
</code></pre>
<p>No error — jane can now list pods in <code>staging</code>. She can't delete them, create them, or access other namespaces. The certificate got her in. RBAC determines what she can do.</p>
<h2 id="heading-how-to-set-up-oidc-authentication">How to Set Up OIDC Authentication</h2>
<p>OpenID Connect is an identity layer on top of OAuth 2.0. It's how Kubernetes integrates with enterprise identity providers — Active Directory, Okta, Google Workspace, Keycloak, and any other provider that speaks OIDC. Understanding how Kubernetes uses it requires following the token from the user's browser to the API server's decision.</p>
<h3 id="heading-how-the-oidc-flow-works-in-kubernetes">How the OIDC Flow Works in Kubernetes</h3>
<p>When a developer runs <code>kubectl get pods</code> with OIDC configured, the following happens:</p>
<ol>
<li><p><code>kubectl</code> checks whether the current credential in the kubeconfig is a valid, unexpired OIDC token</p>
</li>
<li><p>If not, it launches <code>kubelogin</code>, a kubectl plugin that opens a browser window</p>
</li>
<li><p>The browser redirects to the OIDC provider (Dex, Okta, your corporate IdP)</p>
</li>
<li><p>The user logs in with their corporate credentials</p>
</li>
<li><p>The OIDC provider issues a signed JWT and returns it to kubelogin</p>
</li>
<li><p>kubelogin caches the token locally (under <code>~/.kube/cache/oidc-login/</code>) and returns it to <code>kubectl</code></p>
</li>
<li><p><code>kubectl</code> sends the token to the API server as a <code>Bearer</code> header</p>
</li>
<li><p>The API server fetches the provider's public keys from its JWKS endpoint and verifies the token signature</p>
</li>
<li><p>If valid, the API server extracts the username and group claims from the token</p>
</li>
<li><p>RBAC takes over from there</p>
</li>
</ol>
<p>The Kubernetes API server never contacts the OIDC provider for each request. It only fetches the provider's public keys periodically to verify signatures locally. This makes OIDC authentication stateless and scalable.</p>
<h3 id="heading-the-api-server-configuration">The API Server Configuration</h3>
<p>For OIDC to work, the API server needs to know where to find the identity provider and how to interpret the tokens it issues.</p>
<p>In Kubernetes v1.30+, this is configured through an <code>AuthenticationConfiguration</code> file passed via the <code>--authentication-config</code> flag. (In older versions, individual <code>--oidc-*</code> flags were used instead, but these were removed in v1.35.)</p>
<p>The <code>AuthenticationConfiguration</code> defines OIDC providers under the <code>jwt</code> key:</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>What it does</th>
<th>Example</th>
</tr>
</thead>
<tbody><tr>
<td><code>issuer.url</code></td>
<td>The OIDC provider's base URL — must match the <code>iss</code> claim in the token</td>
<td><code>https://dex.example.com</code></td>
</tr>
<tr>
<td><code>issuer.audiences</code></td>
<td>The client IDs the token was issued for — must match the <code>aud</code> claim</td>
<td><code>["kubernetes"]</code></td>
</tr>
<tr>
<td><code>issuer.certificateAuthority</code></td>
<td>CA certificate to trust when contacting the OIDC provider (inlined PEM)</td>
<td><code>-----BEGIN CERTIFICATE-----...</code></td>
</tr>
<tr>
<td><code>claimMappings.username.claim</code></td>
<td>Which JWT claim to use as the Kubernetes username</td>
<td><code>email</code></td>
</tr>
<tr>
<td><code>claimMappings.groups.claim</code></td>
<td>Which JWT claim to use as the Kubernetes group list</td>
<td><code>groups</code></td>
</tr>
<tr>
<td><code>claimMappings.*.prefix</code></td>
<td>Prefix added to the claim value — set to <code>""</code> for no prefix</td>
<td><code>""</code></td>
</tr>
</tbody></table>
<p>On a kind cluster, the <code>--authentication-config</code> flag is set in the cluster configuration before creation, not after. You'll see this in the next demo.</p>
<h3 id="heading-jwt-claims-kubernetes-uses">JWT Claims Kubernetes Uses</h3>
<p>A JWT is a signed JSON object with three sections: a header, a payload, and a signature. The payload is a set of claims – key-value pairs that assert facts about the token. Kubernetes reads specific claims from the payload to build an identity.</p>
<p>The required claims are <code>iss</code> (the issuer URL, must match <code>issuer.url</code> in the <code>AuthenticationConfiguration</code>), <code>sub</code> (the subject, a unique identifier for the user), and <code>aud</code> (the audience, must match the <code>issuer.audiences</code> list). The <code>exp</code> claim (expiry time) is also required as the API server rejects expired tokens.</p>
<p>The most useful optional claim is <code>groups</code> (or whatever you configure via <code>claimMappings.groups.claim</code>). When this claim is present, Kubernetes can map OIDC group memberships directly to RBAC group bindings. A user in the <code>platform-engineers</code> group in your identity provider automatically gets the RBAC permissions you've bound to that group in Kubernetes — no manual user management required.</p>
<h3 id="heading-how-kubelogin-works">How kubelogin Works</h3>
<p>kubelogin (also distributed as <code>kubectl oidc-login</code>) is a kubectl credential plugin. Instead of embedding a static certificate or token in your kubeconfig, you configure a credential plugin that runs a helper binary when <code>kubectl</code> needs a token.</p>
<p>When kubelogin is invoked, it checks its local token cache. If the cached token is still valid, it returns it immediately. If the token has expired, it initiates the OIDC authorization code flow — opens a browser, redirects to the identity provider, receives the token after login, caches it locally, and returns it to <code>kubectl</code>. The whole flow takes about five seconds when it triggers.</p>
<p>This means tokens are short-lived (typically an hour) and rotate automatically. If a developer's machine is compromised, the token expires on its own. There is no long-lived credential sitting in a file somewhere.</p>
<h2 id="heading-demo-2-configure-oidc-login-with-dex-and-kubelogin">Demo 2 — Configure OIDC Login with Dex and kubelogin</h2>
<p>In this section, you'll deploy Dex as a self-hosted OIDC provider, configure a kind cluster to trust it, and log in with a browser. Dex is a good demo vehicle because it runs inside the cluster and doesn't require a cloud account or an external service.</p>
<p><strong>This guide is for local development and learning only.</strong> Self-signed certificates, static passwords, and certs stored on disk are used here for simplicity.</p>
<p>In production, use a managed identity provider (Azure Entra ID, Google Workspace, Okta), automate certificate lifecycle with cert-manager, and store secrets in a secrets manager (HashiCorp Vault, AWS Secrets Manager) or inject them via CSI driver — never commit or store certs as local files.</p>
<h3 id="heading-step-1-create-a-kind-cluster-with-oidc-authentication">Step 1: Create a kind cluster with OIDC authentication</h3>
<p>OIDC authentication for the API server must be configured at cluster creation time on Kind because the API server needs to know which identity provider to trust before it starts accepting requests.</p>
<p><strong>Note:</strong> Kubernetes v1.30+ deprecated the <code>--oidc-*</code> API server flags in favor of the structured <code>AuthenticationConfiguration</code> API (via <code>--authentication-config</code>). In v1.35+ the old flags are removed entirely. This guide uses the new approach.</p>
<p><strong>nip.io</strong> is a wildcard DNS service — <code>dex.127.0.0.1.nip.io</code> resolves to <code>127.0.0.1</code>. This lets us use a real hostname for TLS without editing <code>/etc/hosts</code>.</p>
<p>First, generate a self-signed CA and TLS certificate for Dex:</p>
<pre><code class="language-bash"># Generate a CA for Dex
openssl req -x509 -newkey rsa:4096 -keyout dex-ca.key \
  -out dex-ca.crt -days 365 -nodes \
  -subj "/CN=dex-ca"

# Generate a certificate for Dex signed by that CA
openssl req -newkey rsa:2048 -keyout dex.key \
  -out dex.csr -nodes \
  -subj "/CN=dex.127.0.0.1.nip.io"

openssl x509 -req -in dex.csr \
  -CA dex-ca.crt -CAkey dex-ca.key \
  -CAcreateserial -out dex.crt -days 365 \
  -extfile &lt;(printf "subjectAltName=DNS:dex.127.0.0.1.nip.io")
</code></pre>
<p>Next, generate the <code>AuthenticationConfiguration</code> file. This tells the API server how to validate JWTs — which issuer to trust (<code>url</code>), which audience to expect (<code>audiences</code>), and which JWT claims map to Kubernetes usernames and groups (<code>claimMappings</code>). The CA cert is inlined so the API server can verify Dex's TLS certificate when fetching signing keys:</p>
<pre><code class="language-bash">cat &gt; auth-config.yaml &lt;&lt;EOF
apiVersion: apiserver.config.k8s.io/v1beta1
kind: AuthenticationConfiguration
jwt:
  - issuer:
      url: https://dex.127.0.0.1.nip.io:32000
      audiences:
        - kubernetes
      certificateAuthority: |
$(sed 's/^/        /' dex-ca.crt)
    claimMappings:
      username:
        claim: email
        prefix: ""
      groups:
        claim: groups
        prefix: ""
EOF
</code></pre>
<p>The <code>kind-oidc.yaml</code> config uses <code>extraPortMappings</code> to expose Dex's port to your browser, <code>extraMounts</code> to copy files into the Kind node, and a <code>kubeadmConfigPatch</code> to pass <code>--authentication-config</code> to the API server:</p>
<pre><code class="language-yaml"># kind-oidc.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
    extraPortMappings:
      # Forward port 32000 from the Docker container to localhost,
      # so your browser can reach Dex's login page
      - containerPort: 32000
        hostPort: 32000
        protocol: TCP
    extraMounts:
      # Copy files from your machine into the Kind node's filesystem
      - hostPath: ./dex-ca.crt
        containerPath: /etc/ca-certificates/dex-ca.crt
        readOnly: true
      - hostPath: ./auth-config.yaml
        containerPath: /etc/kubernetes/auth-config.yaml
        readOnly: true
    kubeadmConfigPatches:
      # Patch the API server to enable OIDC authentication
      - |
        kind: ClusterConfiguration
        apiServer:
          extraArgs:
            # Tell the API server to load our AuthenticationConfiguration
            authentication-config: /etc/kubernetes/auth-config.yaml
          extraVolumes:
            # Mount files into the API server pod (it runs as a static pod,
            # so it needs explicit volume mounts even though files are on the node)
            - name: dex-ca
              hostPath: /etc/ca-certificates/dex-ca.crt
              mountPath: /etc/ca-certificates/dex-ca.crt
              readOnly: true
              pathType: File
            - name: auth-config
              hostPath: /etc/kubernetes/auth-config.yaml
              mountPath: /etc/kubernetes/auth-config.yaml
              readOnly: true
              pathType: File
</code></pre>
<p>Create the cluster:</p>
<pre><code class="language-bash">kind create cluster --name k8s-auth --config kind-oidc.yaml
</code></pre>
<h3 id="heading-step-2-deploy-dex">Step 2: Deploy Dex</h3>
<p>Dex is an OIDC-compliant identity provider that acts as a bridge between Kubernetes and upstream identity sources (LDAP, SAML, GitHub, and so on). In this demo it runs inside the cluster with a static password database — two hardcoded users you can log in as.</p>
<p>The API server doesn't talk to Dex directly on every request. It only needs Dex's CA certificate (which you inlined in the <code>AuthenticationConfiguration</code>) to verify the JWT signatures on tokens that Dex issues.</p>
<p>The deployment has four parts: a ConfigMap with Dex's configuration, a Deployment to run Dex, a NodePort Service to expose it on port 32000 (matching the issuer URL), and RBAC resources so Dex can store state using Kubernetes CRDs.</p>
<p>First, create the namespace and load the TLS certificate as a Kubernetes Secret. Dex needs this to serve HTTPS. Without it, your browser and the API server would refuse to connect:</p>
<pre><code class="language-bash">kubectl create namespace dex

kubectl create secret tls dex-tls \
  --cert=dex.crt \
  --key=dex.key \
  -n dex
</code></pre>
<p>Save the following as <code>dex-config.yaml</code>. This configures Dex with a static password connector — two hardcoded users for the demo:</p>
<pre><code class="language-yaml"># dex-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: dex-config
  namespace: dex
data:
  config.yaml: |
    # issuer must exactly match the URL in your AuthenticationConfiguration
    issuer: https://dex.127.0.0.1.nip.io:32000

    # Dex stores refresh tokens and auth codes — here it uses Kubernetes CRDs
    storage:
      type: kubernetes
      config:
        inCluster: true

    # Dex's HTTPS listener — serves the login page and token endpoints
    web:
      https: 0.0.0.0:5556
      tlsCert: /etc/dex/tls/tls.crt
      tlsKey: /etc/dex/tls/tls.key

    # staticClients defines which applications can request tokens.
    # "kubernetes" is the client ID that kubelogin uses when authenticating
    staticClients:
      - id: kubernetes
        redirectURIs:
          - http://localhost:8000     # kubelogin listens here to receive the callback
        name: Kubernetes
        secret: kubernetes-secret     # shared secret between kubelogin and Dex

    # Two demo users with the password "password" (bcrypt-hashed).
    # In production, you'd connect Dex to LDAP, SAML, or a social login instead
    enablePasswordDB: true
    staticPasswords:
      - email: "jane@example.com"
        # bcrypt hash of "password" — generate your own with: htpasswd -bnBC 10 "" password
        hash: "\(2a\)10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
        username: "jane"
        userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
      - email: "admin@example.com"
        hash: "\(2a\)10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
        username: "admin"
        userID: "a8b53e13-7e8c-4f7b-9a33-6c2f4d8c6a1b"
        groups:
          - platform-engineers
</code></pre>
<p>Save the following as <code>dex-deployment.yaml</code>. This creates the Deployment, Service, ServiceAccount, and RBAC that Dex needs to run:</p>
<pre><code class="language-yaml"># dex-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dex
  namespace: dex
spec:
  replicas: 1
  selector:
    matchLabels:
      app: dex
  template:
    metadata:
      labels:
        app: dex
    spec:
      serviceAccountName: dex
      containers:
        - name: dex
          # v2.45.0+ required — earlier versions don't include groups from staticPasswords in tokens
          image: ghcr.io/dexidp/dex:v2.45.0
          command: ["dex", "serve", "/etc/dex/cfg/config.yaml"]
          ports:
            - name: https
              containerPort: 5556
          volumeMounts:
            - name: config
              mountPath: /etc/dex/cfg
            - name: tls
              mountPath: /etc/dex/tls
      volumes:
        - name: config
          configMap:
            name: dex-config
        - name: tls
          secret:
            secretName: dex-tls
---
# NodePort Service — exposes Dex on port 32000 on the Kind node.
# Combined with extraPortMappings, this makes Dex reachable from your browser
apiVersion: v1
kind: Service
metadata:
  name: dex
  namespace: dex
spec:
  type: NodePort
  ports:
    - name: https
      port: 5556
      targetPort: 5556
      nodePort: 32000
  selector:
    app: dex
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: dex
  namespace: dex
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: dex
rules:
  - apiGroups: ["dex.coreos.com"]
    resources: ["*"]
    verbs: ["*"]
  - apiGroups: ["apiextensions.k8s.io"]
    resources: ["customresourcedefinitions"]
    verbs: ["create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: dex
subjects:
  - kind: ServiceAccount
    name: dex
    namespace: dex
roleRef:
  kind: ClusterRole
  name: dex
  apiGroup: rbac.authorization.k8s.io
</code></pre>
<pre><code class="language-bash">kubectl apply -f dex-config.yaml
kubectl apply -f dex-deployment.yaml
kubectl rollout status deployment/dex -n dex
</code></pre>
<h3 id="heading-step-3-install-kubelogin">Step 3: Install kubelogin</h3>
<pre><code class="language-bash"># macOS
brew install int128/kubelogin/kubelogin

# Linux
curl -LO https://github.com/int128/kubelogin/releases/latest/download/kubelogin_linux_amd64.zip
unzip -j kubelogin_linux_amd64.zip kubelogin -d /tmp
sudo mv /tmp/kubelogin /usr/local/bin/kubectl-oidc_login
rm kubelogin_linux_amd64.zip
</code></pre>
<p>Confirm it's installed:</p>
<pre><code class="language-bash">kubectl oidc-login --version
</code></pre>
<h3 id="heading-step-4-configure-a-kubeconfig-entry-for-oidc">Step 4: Configure a kubeconfig entry for OIDC</h3>
<p>This creates a new user and context in your kubeconfig. Instead of using a client certificate (like the default Kind admin), it tells kubectl to use kubelogin to get a token from Dex.</p>
<p>The <code>--oidc-extra-scope</code> flags are important: without <code>email</code> and <code>groups</code>, Dex won't include those claims in the JWT, and the API server won't know who you are or what groups you belong to.</p>
<pre><code class="language-bash">kubectl config set-credentials oidc-user \
  --exec-api-version=client.authentication.k8s.io/v1beta1 \
  --exec-command=kubectl \
  --exec-arg=oidc-login \
  --exec-arg=get-token \
  --exec-arg=--oidc-issuer-url=https://dex.127.0.0.1.nip.io:32000 \
  --exec-arg=--oidc-client-id=kubernetes \
  --exec-arg=--oidc-client-secret=kubernetes-secret \
  --exec-arg=--oidc-extra-scope=email \
  --exec-arg=--oidc-extra-scope=groups \
  --exec-arg=--certificate-authority=$(pwd)/dex-ca.crt

kubectl config set-context oidc@k8s-auth \
  --cluster=kind-k8s-auth \
  --user=oidc-user

kubectl config use-context oidc@k8s-auth
</code></pre>
<h3 id="heading-step-5-trigger-the-login-flow">Step 5: Trigger the login flow</h3>
<p>Jane has no RBAC permissions yet, so first grant her read access from the admin context:</p>
<pre><code class="language-bash">kubectl --context kind-k8s-auth create clusterrolebinding jane-view \
  --clusterrole=view --user=jane@example.com
</code></pre>
<p>Now switch to the OIDC context and trigger a login:</p>
<pre><code class="language-bash">kubectl get pods -n default
</code></pre>
<p>Your browser opens and redirects to the Dex login page. Log in as <code>jane@example.com</code> with password <code>password</code>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5f2a6b76d7d55f162b5da2ee/44fe0657-b383-4245-9e43-45daea7a3f4f.png" alt="dexidp login screen" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/5f2a6b76d7d55f162b5da2ee/4f77442a-3055-47fc-a141-8d881731a1f4.png" alt="dexidp grant access" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>After login, the terminal completes:</p>
<pre><code class="language-plaintext">No resources found in default namespace.
</code></pre>
<p>The browser-based authentication worked. <code>kubectl</code> received the token from Dex, sent it to the API server, the API server validated the JWT signature using the CA certificate from the <code>AuthenticationConfiguration</code>, extracted <code>jane@example.com</code> from the <code>email</code> claim, matched it against the RBAC binding, and authorized the request.</p>
<p>Without the <code>clusterrolebinding</code>, you would see <code>Error from server (Forbidden)</code> — authentication succeeds (the API server knows <em>who</em> you are) but authorization fails (jane has no permissions). This is the distinction between 401 Unauthorized and 403 Forbidden.</p>
<h3 id="heading-step-6-inspect-the-jwt">Step 6: Inspect the JWT</h3>
<p>A JWT (JSON Web Token) is a signed JSON payload that contains claims about the user. kubelogin caches the token locally under <code>~/.kube/cache/oidc-login/</code> so you don't have to log in on every kubectl command.</p>
<p>List the directory to find the cached file:</p>
<pre><code class="language-bash">ls ~/.kube/cache/oidc-login/
</code></pre>
<p>Decode the JWT payload directly from the cache:</p>
<pre><code class="language-bash">cat ~/.kube/cache/oidc-login/$(ls ~/.kube/cache/oidc-login/ | grep -v lock | head -1) | \
  python3 -c "
import json, sys, base64
token = json.load(sys.stdin)['id_token'].split('.')[1]
token += '=' * (4 - len(token) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(token)), indent=2))
"
</code></pre>
<p>You'll see something like:</p>
<pre><code class="language-json">{
  "iss": "https://dex.127.0.0.1.nip.io:32000",
  "sub": "CiQwOGE4Njg0Yi1kYjg4LTRiNzMtOTBhOS0zY2QxNjYxZjU0NjYSBWxvY2Fs",
  "aud": "kubernetes",
  "exp": 1775307910,
  "iat": 1775221510,
  "email": "jane@example.com",
  "email_verified": true
}
</code></pre>
<p>The <code>email</code> claim becomes jane's Kubernetes username because the <code>AuthenticationConfiguration</code> maps <code>username.claim: email</code>. The <code>aud</code> matches the configured <code>audiences</code>. The <code>iss</code> matches the issuer <code>url</code>. This is how the API server validates the token without contacting Dex on every request — it only needs the CA certificate to verify the JWT signature.</p>
<h3 id="heading-step-7-map-oidc-groups-to-rbac">Step 7: Map OIDC groups to RBAC</h3>
<p>The <code>admin@example.com</code> user has a <code>groups</code> claim in the Dex config containing <code>platform-engineers</code>. Instead of creating individual RBAC bindings per user, you can bind permissions to a group — anyone whose JWT contains that group gets the permissions automatically:</p>
<pre><code class="language-yaml"># platform-engineers-binding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: platform-engineers-admin
subjects:
  - kind: Group
    name: platform-engineers     # matches the groups claim in the JWT
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io
</code></pre>
<p>You're currently logged in as <code>jane@example.com</code> via the OIDC context, but jane only has <code>view</code> permissions — she can't create cluster-wide RBAC bindings. Switch back to the admin context to apply this:</p>
<pre><code class="language-bash">kubectl config use-context kind-k8s-auth
kubectl apply -f platform-engineers-binding.yaml
kubectl config use-context oidc@k8s-auth
</code></pre>
<p>Now clear the cached token to log out of jane's session, then trigger a new login as <code>admin@example.com</code>:</p>
<pre><code class="language-bash"># Clear the cached token — this is how you "log out" with kubelogin
rm -rf ~/.kube/cache/oidc-login/

# This will open the browser again for a fresh login
kubectl get pods -n default
</code></pre>
<p>Log in as <code>admin@example.com</code> with password <code>password</code>. This time the JWT will contain <code>"groups": ["platform-engineers"]</code>, which matches the <code>ClusterRoleBinding</code> you just created. The admin user gets full cluster access — without ever being added to a kubeconfig by name.</p>
<p>You can verify by decoding the new token (Step 6) — the <code>groups</code> claim will be present:</p>
<pre><code class="language-json">{
  "email": "admin@example.com",
  "groups": ["platform-engineers"]
}
</code></pre>
<p>This is the real power of OIDC group claims: you manage group membership in your identity provider, and Kubernetes permissions follow automatically. Add someone to the <code>platform-engineers</code> group in Dex (or any upstream IdP), and they get cluster-admin access on their next login — no kubeconfig or RBAC changes needed.</p>
<h2 id="heading-cloud-provider-authentication">Cloud Provider Authentication</h2>
<p>AWS, GCP, and Azure each give Kubernetes clusters a native authentication mechanism that ties into their IAM systems.</p>
<p>The implementations differ in API surface, but they all use the same underlying mechanism: OIDC token projection. Once you understand how Dex works above, these are all variations on the same theme.</p>
<h3 id="heading-aws-eks">AWS EKS</h3>
<p>EKS uses the <code>aws-iam-authenticator</code> to translate AWS IAM identities into Kubernetes identities. When you run <code>kubectl</code> against an EKS cluster, the AWS CLI generates a short-lived token signed with your IAM credentials. The API server passes this token to the aws-iam-authenticator webhook, which verifies it against AWS STS and returns the corresponding username and groups.</p>
<p>User access is controlled via the <code>aws-auth</code> ConfigMap in <code>kube-system</code>, which maps IAM role ARNs and IAM user ARNs to Kubernetes usernames and groups. A typical entry looks like this:</p>
<pre><code class="language-yaml"># In kube-system/aws-auth ConfigMap
mapRoles:
  - rolearn: arn:aws:iam::123456789:role/platform-engineers
    username: platform-engineer:{{SessionName}}
    groups:
      - platform-engineers
</code></pre>
<p>AWS is migrating from the <code>aws-auth</code> ConfigMap to a newer Access Entries API, which manages the same mapping through the EKS API rather than a ConfigMap. The underlying authentication mechanism is the same.</p>
<h3 id="heading-google-gke">Google GKE</h3>
<p>GKE integrates with Google Cloud IAM using two different mechanisms, depending on whether you're authenticating as a human user or as a workload.</p>
<p>For human users, GKE accepts standard Google OAuth2 tokens. Running <code>gcloud container clusters get-credentials</code> writes a kubeconfig that uses the <code>gcloud</code> CLI as a credential plugin, generating short-lived tokens from your Google account automatically.</p>
<p>For pod-level identity — letting a pod assume a Google Cloud IAM role — GKE uses Workload Identity. You annotate a Kubernetes service account to bind it to a Google Service Account, and pods running as that service account can call Google Cloud APIs using the GSA's permissions:</p>
<pre><code class="language-bash"># Bind a Kubernetes SA to a Google Service Account
kubectl annotate serviceaccount my-app \
  --namespace production \
  iam.gke.io/gcp-service-account=my-app@my-project.iam.gserviceaccount.com
</code></pre>
<h3 id="heading-azure-aks">Azure AKS</h3>
<p>AKS integrates with Azure Active Directory. When Azure AD integration is enabled, <code>kubectl</code> requests an Azure AD token on behalf of the user via the Azure CLI, and the AKS API server validates it against Azure AD.</p>
<p>For pod-level identity, AKS uses Azure Workload Identity, which follows the same OIDC federation pattern as GKE Workload Identity. A Kubernetes service account is annotated with an Azure Managed Identity client ID, and pods can request Azure AD tokens without storing any credentials:</p>
<pre><code class="language-bash"># Annotate a service account with the Azure Managed Identity client ID
kubectl annotate serviceaccount my-app \
  --namespace production \
  azure.workload.identity/client-id=&lt;MANAGED_IDENTITY_CLIENT_ID&gt;
</code></pre>
<p>The underlying pattern across all three providers is the same: a trusted OIDC token is issued by the cloud provider, verified by the Kubernetes API server, and mapped to an identity through a binding (the <code>aws-auth</code> ConfigMap, a GKE Workload Identity binding, or an AKS federated identity credential). The OIDC section in this article is the conceptual foundation for all of them.</p>
<h2 id="heading-webhook-token-authentication">Webhook Token Authentication</h2>
<p>Webhook token authentication is worth knowing about because it appears in several common Kubernetes setups, even if you never configure it yourself.</p>
<p>When a request arrives with a bearer token that no other authenticator recognises, Kubernetes can send that token to an external HTTP endpoint for validation. The endpoint returns a response indicating who the token belongs to.</p>
<p>This is how EKS authentication worked before the aws-iam-authenticator was built into the API server. It's also how bootstrap tokens work during node join operations: a token is generated, embedded in the <code>kubeadm join</code> command, and validated by the bootstrap webhook when the new node contacts the API server for the first time.</p>
<p>For most clusters, you'll encounter webhook auth as something already running rather than something you configure. The main thing to know is that it exists and what it looks like when it appears in logs or configuration.</p>
<h2 id="heading-cleanup">Cleanup</h2>
<p>To remove everything created in this article:</p>
<pre><code class="language-bash"># Delete the OIDC demo cluster
kind delete cluster --name k8s-auth

# Remove generated certificate files
rm -f ca.crt ca.key jane.key jane.csr jane.crt jane.kubeconfig
rm -f dex-ca.crt dex-ca.key dex.crt dex.key dex.csr dex-ca.srl auth-config.yaml

# Remove the kubelogin token cache
rm -rf ~/.kube/cache/oidc-login/
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Kubernetes authentication is not a single mechanism — it's a chain of pluggable strategies, each one suited to different use cases. In this article you worked through the most important ones.</p>
<p>x509 client certificates are how Kubernetes works out of the box. The CN field becomes the username, the O field becomes the group, and the cluster CA is the trust anchor. You created a certificate for a new user, bound it to RBAC, and saw exactly how authentication and authorisation interact — authentication gets you in, RBAC determines what you can do.</p>
<p>You also saw the fundamental limitation: Kubernetes doesn't check certificate revocation lists, so a compromised certificate remains valid until it expires. This makes certificates a poor fit for human users in production environments.</p>
<p>OIDC is the production-grade answer. Tokens are short-lived, issued by a trusted identity provider, and map directly to Kubernetes groups through JWT claims. You deployed Dex as a self-hosted OIDC provider, configured the API server to trust it, and set up kubelogin for browser-based authentication.</p>
<p>You then decoded a JWT to see exactly what the API server reads from it, and mapped an OIDC group claim to a Kubernetes ClusterRoleBinding.</p>
<p>Cloud provider authentication — EKS, GKE, AKS — uses the same OIDC foundation with provider-specific wrappers. Understanding how Dex works makes each of those systems immediately readable.</p>
<p>All YAML, certificates, and configuration files from this article are in the <a href="https://github.com/Caesarsage/DevOps-Cloud-Projects/tree/main/intermediate/k8/security">companion GitHub repository</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Prevent IDOR Vulnerabilities in Next.js API Routes ]]>
                </title>
                <description>
                    <![CDATA[ Imagine this situation: A user logs in successfully to your application, but upon loading their dashboard, they see someone else’s data. Why does this happen? The authentication worked, the session is ]]>
                </description>
                <link>https://www.freecodecamp.org/news/prevent-idor-in-nextjs/</link>
                <guid isPermaLink="false">69a1f073d4053a09f3430559</guid>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                    <category>
                        <![CDATA[ authentication ]]>
                    </category>
                
                    <category>
                        <![CDATA[ authorization ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Ayodele Aransiola ]]>
                </dc:creator>
                <pubDate>Fri, 27 Feb 2026 19:28:51 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5fc16e412cae9c5b190b6cdd/b14a67ea-e78b-4ebd-996f-98da3a0a8027.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Imagine this situation: A user logs in successfully to your application, but upon loading their dashboard, they see someone else’s data.</p>
<p>Why does this happen? The authentication worked, the session is valid, the user is authenticated, but the authorization failed.</p>
<p>This specific issue is called <strong>IDOR (Insecure Direct Object Reference)</strong>. It’s one of the most common security bugs and is categorized under <strong>Broken Object Level Authorization (BOLA)</strong> in the OWASP API Security Top 10.</p>
<p>In this tutorial, you’ll learn:</p>
<ul>
<li><p>Why IDOR happens</p>
</li>
<li><p>Why authentication alone is not enough</p>
</li>
<li><p>How object-level authorization works</p>
</li>
<li><p>How to fix IDOR properly in Next.js API routes</p>
</li>
<li><p>How to design safer APIs from the start</p>
</li>
</ul>
<h2 id="heading-table-of-content">Table of Content</h2>
<ul>
<li><p><a href="#heading-table-of-content">Table of Content</a></p>
</li>
<li><p><a href="#heading-authentication-vs-authorization">Authentication vs. Authorization</a></p>
</li>
<li><p><a href="#heading-what-is-an-idor-vulnerability">What is an IDOR Vulnerability?</a></p>
</li>
<li><p><a href="#heading-the-vulnerable-pattern-in-nextjs">The Vulnerable Pattern in Next.js</a></p>
</li>
<li><p><a href="#heading-how-to-handle-idor-in-nextjs">How to Handle IDOR in Next.js</a></p>
<ul>
<li><a href="#heading-object-level-authorization">Object-Level Authorization</a></li>
</ul>
</li>
<li><p><a href="#heading-how-to-design-safer-endpoints-apime">How to Design Safer Endpoints (/api/me)</a></p>
</li>
<li><p><a href="#heading-mental-model-for-api-design">Mental Model for API Design</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-authentication-vs-authorization">Authentication vs. Authorization</h2>
<p>Before writing further, let’s clarify something critical.</p>
<ul>
<li><p><strong>Authentication answers:</strong> Who are you?</p>
</li>
<li><p><strong>Authorization answers:</strong> What are you allowed to access?</p>
</li>
</ul>
<p>In IDOR scenarios, authentication works (the user is logged in), while authorization is missing or incomplete. That distinction is the core lesson of this article.</p>
<h2 id="heading-what-is-an-idor-vulnerability">What is an IDOR Vulnerability?</h2>
<p>An IDOR vulnerability happens when your API fetches a resource by an identifier (like a user ID), and then you do not verify that the requester owns or is allowed to access that resource.</p>
<p>Example of such a request:</p>
<pre><code class="language-plaintext">GET /api/users/123
</code></pre>
<p>The code above is an HTTP <strong>GET</strong> request to the <code>/api/users/123</code> route. The <code>GET</code> method is used to request data from the server. This indicates that the client is requesting a specific user with the ID <code>123</code> and this request returns the user data in a response (often in JSON format).</p>
<p>If your backend makes the request using a similar structure to the code snippet below without checking who is making the request, you have an IDOR vulnerability, even if the user is logged in.</p>
<pre><code class="language-tsx">db.user.findUnique({ where: { id: "123" } })
</code></pre>
<p>What the code does is to query the database for a single user record. The <code>db.user</code> part refers to the <code>user</code> model/table and <code>findUnique()</code> is a method that returns only one record based on a unique field. Inside the method, the <code>where</code> clause specifies the filter condition and <code>{ id: "123" }</code> tells the database to find the user whose unique <code>id</code> equals <code>"123"</code>. If a matching record exists, it returns that user object; otherwise, it returns <code>null</code>.</p>
<h2 id="heading-the-vulnerable-pattern-in-nextjs">The Vulnerable Pattern in Next.js</h2>
<p>Looking at this Next.js App Router API route:</p>
<pre><code class="language-tsx">// app/api/users/[id]/route.ts
import { NextResponse } from "next/server";
import { db } from "@/lib/db";

export async function GET(
  req: Request,
  { params }: { params: { id: string } }
) {
  const user = await db.user.findUnique({
    where: { id: params.id },
    select: { id: true, email: true, name: true },
  });

  return NextResponse.json({ user });
}
</code></pre>
<p>Before going to the implication of this code snippet, let's understand what the code does. It defines a dynamic API route for <code>/api/users/[id]</code>. The exported <code>GET</code> function is an async route handler that runs when a GET request is made to this endpoint. It receives the request object and a <code>params</code> object, where <code>params.id</code> contains the dynamic <code>[id]</code> in the URL segment. The <code>db.user.findUnique()</code> method queries the database for a user whose <code>id</code> matches <code>params.id</code>, and the <code>select</code> option limits the returned fields to <code>id</code>, <code>email</code>, and <code>name</code>. Finally, <code>NextResponse.json()</code> sends the retrieved user data back to the client as a JSON response.</p>
<p>Now, to the implication, the code is a bad approach because the route accepts a user ID from the URL, fetches that user directly from the database, and returns the result. There is no session validation, no ownership check, and no role check.</p>
<p>If a logged-in user changes the <code>id</code> in the URL, they may access other users’ data. This is simply IDOR.</p>
<h2 id="heading-how-to-handle-idor-in-nextjs">How to Handle IDOR in Next.js</h2>
<p>The first element of defense is verifying identity. We’ll use <code>getServerSession</code> from NextAuth (adjust if using another auth provider). This change ensures that you read the session from the cookies, verify it on the server side, and ensure the user has a valid ID. This prevents unauthenticated access.</p>
<pre><code class="language-tsx">// lib/auth.ts
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/authOptions";

export async function requireSession() {
  const session = await getServerSession(authOptions);

  if (!session?.user?.id) {
    return null;
  }

  return session;
}
</code></pre>
<p>The code above defines an authentication helper function called <code>requireSession</code>. The <code>getServerSession(authOptions)</code> function retrieves the current user session on the server using the provided authentication configuration. The optional chaining (<code>session?.user?.id</code>) in the <code>if</code> block that follows safely checks whether a logged-in user and their <code>id</code> exist. If no valid session or user ID is found, the function returns <code>null</code>, indicating the request is unauthenticated. Otherwise, it returns the full <code>session</code> object so it can be used in protected routes or server logic.</p>
<p>You have successfully confirmed that the user and session exist; now, update the route:</p>
<pre><code class="language-tsx">export async function GET(
  req: Request,
  { params }: { params: { id: string } }
) {
  const session = await requireSession();

  if (!session) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const user = await db.user.findUnique({
    where: { id: params.id },
    select: { id: true, email: true, name: true },
  });

  return NextResponse.json({ user });
}
</code></pre>
<p>The fix is incomplete yet, but in the above code, you’ve prevented anonymous access. The <code>GET</code> handler calls the <code>requireSession()</code> that was created earlier to verify that the request is authenticated. If no valid session is returned, it immediately responds with a JSON error message and a <code>401 Unauthorized</code> HTTP status. If the user is authenticated, it proceeds to call <code>db.user.findUnique()</code> to fetch the user whose <code>id</code> matches <code>params.id</code>, selecting only the <code>id</code>, <code>email</code>, and <code>name</code> fields. Finally, it returns the retrieved user data as a JSON response using <code>NextResponse.json()</code>.</p>
<p>Something is still missing. Can you guess? Any authenticated user can still request any resource by changing the URL path to the request they want. How? This leads us to object-level authorization.</p>
<h3 id="heading-object-level-authorization">Object-Level Authorization</h3>
<p>An object-level authorization ensures that a user can only access their own data (unless explicitly permitted).</p>
<p>The improvement to the code would be to add an ownership check. The adjustment ensures the API request checks if the requester is authenticated and owns the requested object. If either fails, access is denied.</p>
<pre><code class="language-tsx">export async function GET(
  req: Request,
  { params }: { params: { id: string } }
) {
  const session = await requireSession();

  if (!session) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  if (session.user.id !== params.id) {
    return NextResponse.json({ error: "Forbidden" }, { status: 403 });
  }

  const user = await db.user.findUnique({
    where: { id: params.id },
    select: { id: true, email: true, name: true },
  });

  return NextResponse.json({ user });
}
</code></pre>
<p>Let's take a look at what happened in the code, the <code>GET</code> handler first authenticates the request using <code>requireSession()</code>, returning a <code>401</code> response if no valid session exists. It then performs an authorization check by comparing <code>session.user.id</code> with <code>params.id</code>. If they do not match, it returns a <code>403 Forbidden</code> response, preventing users from accessing other users’ data. If both checks pass, it queries the database using <code>db.user.findUnique()</code> to retrieve the specified user and limits the result to selected fields. Finally, it sends the user data back as a JSON response. With this, you’ve enforced an <strong>object-level authorization</strong>.</p>
<h2 id="heading-how-to-design-safer-endpoints-apime">How to Design Safer Endpoints (<code>/api/me</code>)</h2>
<p>The safest approach in designing your endpoint is to eliminate the risk entirely. Instead of allowing users to specify IDs (<code>/api/users/:id</code>), use <code>/api/me</code>, because the server already knows the user’s identity from the session.</p>
<pre><code class="language-tsx">// app/api/me/route.ts
export async function GET() {
  const session = await requireSession();

  if (!session) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const user = await db.user.findUnique({
    where: { id: session.user.id },
    select: { id: true, email: true, name: true },
  });

  return NextResponse.json({ user });
}
</code></pre>
<p>This approach makes sure that your API only returns data for the currently authenticated user. It first calls <code>requireSession()</code> to ensure the request is authenticated, returning a <code>401</code> response if no session exists. Instead of using a URL parameter, it reads the user’s ID directly from <code>session.user.id</code>, ensuring the user can only access their own data. It then calls <code>db.user.findUnique()</code> to retrieve that user from the database, selecting only specific fields, and returns the result as a JSON response.</p>
<p>You can be confident with this approach because the client cannot manipulate user IDs. The server gets the user identity from a trusted source, and the attack surface is reduced. This is called <code>secure-by-design</code> <strong>API model</strong>.</p>
<p>Now, you should clearly understand that authentication does not imply authorization. Hence,</p>
<ul>
<li><p>IDOR occurs when object ownership is not verified</p>
</li>
<li><p>Every API route that accepts an ID must validate access</p>
</li>
<li><p>Safer API design reduces vulnerability surface</p>
</li>
<li><p>Authorization must always run on the server</p>
</li>
</ul>
<h2 id="heading-mental-model-for-api-design">Mental Model for API Design</h2>
<p>When writing any API route, answer these questions:</p>
<ol>
<li><p>Who is making this request?</p>
</li>
<li><p>What object are they requesting?</p>
</li>
<li><p>Does policy allow them to access it?</p>
</li>
</ol>
<p>If you cannot clearly answer all three, your route may be vulnerable.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>IDOR vulnerabilities happen when APIs trust user-supplied identifiers without verifying ownership or permission.</p>
<p>To prevent them in Next.js, authenticate every private route, enforce object-level authorization, centralize authorization logic, and write tests for forbidden access.</p>
<p>Security is not about adding logins, it’s about enforcing security policy on every object access.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Secure Authentication System with JWT and Refresh Tokens ]]>
                </title>
                <description>
                    <![CDATA[ Every app that handles user accounts needs a way to confirm who’s who. That’s what authentication is for, making sure the person using an app is the person they claim to be. But doing this securely is harder than it sounds. Traditional methods often ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-secure-authentication-system-with-jwt-and-refresh-tokens/</link>
                <guid isPermaLink="false">6925f655569c4dde127d2f88</guid>
                
                    <category>
                        <![CDATA[ authentication ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JWT ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Joan Ayebola ]]>
                </dc:creator>
                <pubDate>Tue, 25 Nov 2025 18:32:53 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1764095460886/51b9c653-fa95-42f0-8c51-37f6d6805da4.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Every app that handles user accounts needs a way to confirm who’s who. That’s what authentication is for, making sure the person using an app is the person they claim to be. But doing this securely is harder than it sounds.</p>
<p>Traditional methods often rely on server sessions and cookies. Those work, but they don’t always scale well, especially when you’re building APIs or mobile apps that talk to multiple services. This is why JWTs, or JSON Web Tokens, are useful. They’re small, self-contained tokens that can carry user data safely between a client and a server.</p>
<p>JWTs make it easy to verify users without constantly checking a database – but they also expire fast to reduce risk. To keep users logged in without forcing them to sign in again every few minutes, we use something called a refresh token. It’s a separate, long-lived token that can request new access tokens when the old ones expire.</p>
<p>In this guide, we’ll walk through how to build a secure authentication system using JWTs and refresh tokens. You’ll learn how to generate tokens, validate them, handle expiry, and keep everything safe from common security threats.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-understanding-jwts-json-web-tokens">Understanding JWTs (JSON Web Tokens)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-setting-up-the-project">Setting Up the Project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-implement-jwt-authentication">How to Implement JWT Authentication</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-verify-jwts-and-protect-routes">How to Verify JWTs and Protect Routes</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-refresh-tokens-and-rotation">Refresh Tokens and Rotation</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ol>
<h2 id="heading-understanding-jwts-json-web-tokens">Understanding JWTs (JSON Web Tokens)</h2>
<p>A JWT, short for JSON Web Token, is a compact way to share information between a client and a server. It’s often used to prove that a user is who they say they are. The token is created on the server after a user logs in and is then sent back to the client. The client then includes this token with each request, so the server knows who is making the call.</p>
<p>A JWT has three parts: a header, a payload, and a signature.</p>
<ul>
<li><p>The <strong>header</strong> usually tells the system which algorithm was used to sign the token.</p>
</li>
<li><p>The <strong>payload</strong> contains the data, such as the user’s ID or role.</p>
</li>
<li><p>The <strong>signature</strong> is the part that keeps everything secure. It’s created by hashing the header and payload with a secret key.</p>
</li>
</ul>
<p>Once created, a JWT looks like a long string of random characters separated by dots. When the client sends it back to the server, the server verifies the signature using the same secret key. If it matches, the request is trusted.</p>
<p>One of the main benefits of JWTs is that they are stateless. The server doesn’t need to store session data. Everything needed to verify the user is already inside the token. This makes them fast and easy to use in modern APIs and microservices.</p>
<p>JWTs do have a downside: they cannot be revoked easily once issued. If a token is stolen, the attacker can use it until it expires. This is why short token lifetimes matter. It’s also why refresh tokens exist.</p>
<p>In the next section, we’ll finish the basic JWT setup. After that, we’ll add refresh tokens in <strong>“Refresh Tokens and Rotation.”</strong> That part shows how to handle expiry without making users log in again.</p>
<h2 id="heading-setting-up-the-project">Setting Up the Project</h2>
<p>Before writing any code, let’s set up a simple backend where we can build and test our authentication system. For this guide, we’ll use Node.js with Express, since it’s lightweight and easy to follow. You can use any stack later once you understand the flow.</p>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>Make sure you have:</p>
<ul>
<li><p>Node.js and npm installed</p>
</li>
<li><p>A text editor (VS Code works great)</p>
</li>
<li><p>Basic knowledge of JavaScript and APIs</p>
</li>
</ul>
<h3 id="heading-1-initialize-the-project">1. Initialize the Project</h3>
<p>Create a new folder and open it in your terminal.</p>
<pre><code class="lang-bash">mkdir jwt-auth-demo
<span class="hljs-built_in">cd</span> jwt-auth-demo
npm init -y
</code></pre>
<p>This creates a <code>package.json</code> file that will track your dependencies.</p>
<h3 id="heading-2-install-dependencies">2. Install Dependencies</h3>
<p>You’ll need a few packages to get started:</p>
<ul>
<li><p><code>express</code>: the web framework</p>
</li>
<li><p><code>jsonwebtoken</code>: to create and verify tokens</p>
</li>
<li><p><code>bcryptjs</code>: to hash passwords</p>
</li>
<li><p><code>dotenv</code>: to manage environment variables</p>
</li>
</ul>
<p>Install them all at once like this:</p>
<pre><code class="lang-bash">npm install express jsonwebtoken bcryptjs dotenv
</code></pre>
<p>If you want auto-reloading while developing, install nodemon as a dev dependency:</p>
<pre><code class="lang-bash">npm install --save-dev nodemon
</code></pre>
<h3 id="heading-3-project-structure">3. Project Structure</h3>
<p>Here’s a clean structure to keep things organized:</p>
<pre><code class="lang-plaintext">jwt-auth-demo/
│
├── server.js
├── .env
├── package.json
│
├── config/
│   └── db.js
│
├── middleware/
│   └── auth.js
│
├── routes/
│   └── auth.js
│
└── models/
    └── user.js
</code></pre>
<h3 id="heading-4-basic-express-setup">4. Basic Express Setup</h3>
<p>In <code>server.js</code>, start with a minimal Express server.</p>
<pre><code class="lang-js"><span class="hljs-built_in">require</span>(<span class="hljs-string">'dotenv'</span>).config();
<span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">'express'</span>);
<span class="hljs-keyword">const</span> app = express();

app.use(express.json());

app.get(<span class="hljs-string">'/'</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  res.send(<span class="hljs-string">'JWT Auth API running'</span>);
});

<span class="hljs-keyword">const</span> PORT = process.env.PORT || <span class="hljs-number">5000</span>;
app.listen(PORT, <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Server running on port <span class="hljs-subst">${PORT}</span>`</span>));
</code></pre>
<p>You can now run it using:</p>
<pre><code class="lang-bash">node server.js
</code></pre>
<p>or, if you’re using nodemon:</p>
<pre><code class="lang-bash">npx nodemon server.js
</code></pre>
<p>If everything is set up correctly, visiting <code>http://localhost:5000</code> should display <strong>“JWT Auth API running”:</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760559643076/8fb7dcbf-50ca-44bc-b2a3-32273d82957f.png" alt="Screenshot of a terminal running nodemon server.js next to a browser window showing the text “JWT Auth API running” at http://localhost:5000, confirming the server started correctly." class="image--center mx-auto" width="482" height="178" loading="lazy"></p>
<h2 id="heading-how-to-implement-jwt-authentication"><strong>How to Implement JWT Authentication</strong></h2>
<p>Now that your server is up, let’s add real authentication. We’ll start with user registration, password hashing, and login. Each user will get a token after logging in, which they can use to access protected routes.</p>
<h3 id="heading-1-set-up-the-user-model">1. Set Up the User Model</h3>
<p>We’ll store users in a simple database. For this demo, let’s use MongoDB with Mongoose, since it’s quick to set up and easy to scale later.</p>
<p>Install the required packages:</p>
<pre><code class="lang-bash">npm install mongoose
</code></pre>
<p>Then create <code>models/user.js</code>:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> mongoose = <span class="hljs-built_in">require</span>(<span class="hljs-string">'mongoose'</span>);

<span class="hljs-keyword">const</span> userSchema = <span class="hljs-keyword">new</span> mongoose.Schema({
  <span class="hljs-attr">username</span>: { <span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>, <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">unique</span>: <span class="hljs-literal">true</span> },
  <span class="hljs-attr">email</span>: { <span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>, <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">unique</span>: <span class="hljs-literal">true</span> },
  <span class="hljs-attr">password</span>: { <span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>, <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span> }
});

<span class="hljs-built_in">module</span>.exports = mongoose.model(<span class="hljs-string">'User'</span>, userSchema);
</code></pre>
<p>We store users with a unique email and a hashed password. The database never sees the raw password. Hashing makes stolen data harder to use.</p>
<h3 id="heading-2-connect-to-mongodb">2. Connect to MongoDB</h3>
<p>Inside <code>config/db.js</code>:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> mongoose = <span class="hljs-built_in">require</span>(<span class="hljs-string">'mongoose'</span>);

<span class="hljs-keyword">const</span> connectDB = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">await</span> mongoose.connect(process.env.MONGO_URI);
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'MongoDB connected'</span>);
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-built_in">console</span>.error(err.message);
    process.exit(<span class="hljs-number">1</span>);
  }
};

<span class="hljs-built_in">module</span>.exports = connectDB;
</code></pre>
<p><code>mongoose.connect</code> reads the connection string from <code>.env</code>. If the connection fails, we exit the process so we don’t continue in a broken state.</p>
<p>Update your <code>server.js</code> to include the connection:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> connectDB = <span class="hljs-built_in">require</span>(<span class="hljs-string">'./config/db'</span>);
connectDB();
</code></pre>
<p>And don’t forget to add your MongoDB URI in the <code>.env</code> file:</p>
<pre><code class="lang-plaintext">MONGO_URI=mongodb+srv://yourusername:yourpassword@cluster.mongodb.net/auth
JWT_SECRET=your_jwt_secret_key
</code></pre>
<h3 id="heading-3-create-registration-and-login-routes">3. Create Registration and Login Routes</h3>
<p>In <code>routes/auth.js</code>:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">'express'</span>);
<span class="hljs-keyword">const</span> bcrypt = <span class="hljs-built_in">require</span>(<span class="hljs-string">'bcryptjs'</span>);
<span class="hljs-keyword">const</span> jwt = <span class="hljs-built_in">require</span>(<span class="hljs-string">'jsonwebtoken'</span>);
<span class="hljs-keyword">const</span> User = <span class="hljs-built_in">require</span>(<span class="hljs-string">'../models/user'</span>);

<span class="hljs-keyword">const</span> router = express.Router();

<span class="hljs-comment">// Register a new user</span>
router.post(<span class="hljs-string">'/register'</span>, <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> { username, email, password } = req.body;

    <span class="hljs-keyword">const</span> existingUser = <span class="hljs-keyword">await</span> User.findOne({ email });
    <span class="hljs-keyword">if</span> (existingUser) <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">'User already exists'</span> });

    <span class="hljs-keyword">const</span> hashedPassword = <span class="hljs-keyword">await</span> bcrypt.hash(password, <span class="hljs-number">10</span>);

    <span class="hljs-keyword">const</span> newUser = <span class="hljs-keyword">new</span> User({ username, email, <span class="hljs-attr">password</span>: hashedPassword });
    <span class="hljs-keyword">await</span> newUser.save();

    res.status(<span class="hljs-number">201</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">'User created successfully'</span> });
  } <span class="hljs-keyword">catch</span> (err) {
    res.status(<span class="hljs-number">500</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">'Server error'</span> });
  }
});

<span class="hljs-comment">// Login and issue JWT</span>
router.post(<span class="hljs-string">'/login'</span>, <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> { email, password } = req.body;

    <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> User.findOne({ email });
    <span class="hljs-keyword">if</span> (!user) <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">'Invalid credentials'</span> });

    <span class="hljs-keyword">const</span> isMatch = <span class="hljs-keyword">await</span> bcrypt.compare(password, user.password);
    <span class="hljs-keyword">if</span> (!isMatch) <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">'Invalid credentials'</span> });

    <span class="hljs-keyword">const</span> payload = { <span class="hljs-attr">id</span>: user._id, <span class="hljs-attr">email</span>: user.email };

    <span class="hljs-keyword">const</span> token = jwt.sign(payload, process.env.JWT_SECRET, { <span class="hljs-attr">expiresIn</span>: <span class="hljs-string">'15m'</span> });

    res.json({ token });
  } <span class="hljs-keyword">catch</span> (err) {
    res.status(<span class="hljs-number">500</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">'Server error'</span> });
  }
});

<span class="hljs-built_in">module</span>.exports = router;
</code></pre>
<p>Add it to your server in <code>server.js</code>:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> authRoutes = <span class="hljs-built_in">require</span>(<span class="hljs-string">'./routes/auth'</span>);
app.use(<span class="hljs-string">'/api/auth'</span>, authRoutes);
</code></pre>
<h3 id="heading-4-test-it-out">4. Test It Out</h3>
<p>You can now test these routes using Postman or Insomnia.</p>
<p>Send a <code>POST</code> request to <code>/api/auth/register</code> with a JSON body:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"username"</span>: <span class="hljs-string">"demoUser"</span>,
  <span class="hljs-attr">"email"</span>: <span class="hljs-string">"demo@email.com"</span>,
  <span class="hljs-attr">"password"</span>: <span class="hljs-string">"mypassword"</span>
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760713863394/c13ddbd5-ebb1-47d1-9b6d-06bc0f33eb7d.png" alt="Screenshot of a Postman request sending a POST call to http://localhost:3000/api/auth/register with a JSON body containing a username, email, and password. The response area shows a 201 Created status and the message “User created successfully.&quot;" class="image--center mx-auto" width="1646" height="1356" loading="lazy"></p>
<p>The register route checks for an existing user by email. It hashes the password with a cost factor of 10 and then returns a 201 on success. We don’t log the password or include it in the response.</p>
<p>Then log in at <code>/api/auth/login</code> to receive a JWT.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760713960135/58eeaa4e-d652-4509-ad6e-756baf19ff8c.png" alt="Screenshot of a Postman request sending a POST call to http://localhost:3000/api/auth/login with a JSON body containing a username, email, and password. The response panel shows a 200 OK status and a JSON object with a generated JWT token." class="image--center mx-auto" width="1636" height="1352" loading="lazy"></p>
<p>The login route finds the user by email and compares the password with bcrypt.compare. If it matches, we sign a token with a small payload: the user ID and email. The JWT_SECRET signs the token so the server can verify it later. The expiresIn: '15m' setting keeps the token short-lived to limit risk. The response only includes the token. User data can be fetched from a protected route.</p>
<p>Once you get the token, copy it, you’ll use it to access protected routes later.</p>
<h2 id="heading-how-to-verify-jwts-and-protect-routes">How to Verify JWTs and Protect Routes</h2>
<p>Now that login returns a token, we should verify it on each request that needs auth. We will write a small middleware that checks the <code>Authorization</code> header, validates the token, and adds the user info to the request.</p>
<h3 id="heading-1-create-the-auth-middleware">1. Create the Auth Middleware</h3>
<p>Create <code>middleware/auth.js</code>:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> jwt = <span class="hljs-built_in">require</span>(<span class="hljs-string">'jsonwebtoken'</span>);

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">auth</span>(<span class="hljs-params">req, res, next</span>) </span>{
  <span class="hljs-keyword">const</span> authHeader = req.headers.authorization || <span class="hljs-string">''</span>;
  <span class="hljs-keyword">const</span> [scheme, token] = authHeader.split(<span class="hljs-string">' '</span>);

  <span class="hljs-keyword">if</span> (scheme !== <span class="hljs-string">'Bearer'</span> || !token) {
    <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">401</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">'Missing or invalid Authorization header'</span> });
  }

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = { <span class="hljs-attr">id</span>: decoded.id, <span class="hljs-attr">email</span>: decoded.email };
    next();
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-keyword">if</span> (err.name === <span class="hljs-string">'TokenExpiredError'</span>) {
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">401</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">'Access token expired'</span> });
    }
    <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">401</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">'Invalid token'</span> });
  }
}

<span class="hljs-built_in">module</span>.exports = auth;
</code></pre>
<p>What it does:</p>
<ul>
<li><p>Reads the <code>Authorization</code> header.</p>
</li>
<li><p>Checks for the <code>Bearer &lt;token&gt;</code> format.</p>
</li>
<li><p>Verifies the token with the secret.</p>
</li>
<li><p>Attaches a simple <code>user</code> object to <code>req</code> for later use.</p>
</li>
</ul>
<h3 id="heading-2-create-the-protected-route">2. Create the Protected Route</h3>
<p>Create a small profile route that returns the current user. Add <code>routes/profile.js</code>:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">'express'</span>);
<span class="hljs-keyword">const</span> auth = <span class="hljs-built_in">require</span>(<span class="hljs-string">'../middleware/auth'</span>);
<span class="hljs-keyword">const</span> User = <span class="hljs-built_in">require</span>(<span class="hljs-string">'../models/user'</span>);

<span class="hljs-keyword">const</span> router = express.Router();

router.get(<span class="hljs-string">'/me'</span>, auth, <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> User.findById(req.user.id).select(<span class="hljs-string">'-password'</span>);
    <span class="hljs-keyword">if</span> (!user) {
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">404</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">'User not found'</span> });
    }
    res.json({ user });
  } <span class="hljs-keyword">catch</span> (err) {
    res.status(<span class="hljs-number">500</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">'Server error'</span> });
  }
});

<span class="hljs-built_in">module</span>.exports = router;
</code></pre>
<p>Wire it in <code>server.js</code>:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> profileRoutes = <span class="hljs-built_in">require</span>(<span class="hljs-string">'./routes/profile'</span>);
app.use(<span class="hljs-string">'/api/profile'</span>, profileRoutes);
</code></pre>
<p>Now a <code>GET /api/profile/me</code> call will only work with a valid token.</p>
<h3 id="heading-3-handle-token-expiry-clearly">3. Handle Token Expiry Clearly</h3>
<p>Short access tokens reduce damage if they leak. We set <code>expiresIn: '15m'</code> during login. When a token expires, the middleware returns a 401 with <code>Access token expired</code>.</p>
<p>We won’t refresh the token here because refresh requires its own endpoint, storage, and rotation rules. You’ll add that in <strong>“Refresh Tokens and Rotation.”</strong> For now, the 401 proves that the expiry is enforced.</p>
<h3 id="heading-4-testing-the-flow">4. Testing the Flow</h3>
<p>In this section, we’ll test that the server blocks requests without a valid token and allows requests with a valid token.</p>
<p>Log in at <code>/api/auth/login</code> and copy the token. Then call <code>/api/profile/me</code> with:</p>
<pre><code class="lang-typescript">Authorization: Bearer &lt;paste_token_here&gt;
</code></pre>
<p>You should see the current user without the password field.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760715340324/13779fe0-304c-460b-87ac-c86133eea2a4.png" alt="Screenshot of a Postman GET request to http://localhost:3000/api/profile/me using a valid JWT. The response shows a 200 OK status and returns the user’s _id, username, and email, confirming that the protected route works when a proper token is included." width="1702" height="1174" loading="lazy"></p>
<p>Then remove the header or change the token and call again. You should get a 401.</p>
<p>Next, wait for the token to expire or change <code>expiresIn</code> to a very short value for a quick test. Call again and confirm you get <code>Access token expired</code>.</p>
<h4 id="heading-tips-for-debugging">Tips for debugging</h4>
<ul>
<li><p>401 with “Missing or invalid Authorization header” means the header format is wrong. Use <code>Authorization: Bearer &lt;token&gt;</code>.</p>
</li>
<li><p>401 with “Invalid token” means the token string is wrong, signed with the wrong secret, or corrupted.</p>
</li>
<li><p>401 with “Access token expired” means the expiry check works. You will fix the client experience with the refresh endpoint later.</p>
</li>
<li><p>If all calls fail, confirm your <code>JWT_SECRET</code> is set in <code>.env</code> and that the server was restarted after changes.</p>
</li>
</ul>
<h3 id="heading-5-optional-cookie-support">5. Optional Cookie Support</h3>
<p>You can store tokens in HTTP-only cookies. The browser sends them automatically. Scripts cannot read HTTP-only cookies, which reduces the risk from XSS.</p>
<p>Install and enable cookies:</p>
<pre><code class="lang-plaintext">npm install cookie-parser
</code></pre>
<pre><code class="lang-javascript"><span class="hljs-comment">// server.js</span>
<span class="hljs-keyword">const</span> cookieParser = <span class="hljs-built_in">require</span>(<span class="hljs-string">'cookie-parser'</span>);
app.use(cookieParser());
</code></pre>
<p>Read the access token from a cookie as a fallback:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// middleware/auth.js</span>
<span class="hljs-keyword">const</span> jwt = <span class="hljs-built_in">require</span>(<span class="hljs-string">'jsonwebtoken'</span>);

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">auth</span>(<span class="hljs-params">req, res, next</span>) </span>{
  <span class="hljs-keyword">const</span> header = req.headers.authorization || <span class="hljs-string">''</span>;
  <span class="hljs-keyword">const</span> [scheme, tokenFromHeader] = header.split(<span class="hljs-string">' '</span>);
  <span class="hljs-keyword">const</span> tokenFromCookie = req.cookies?.access_token;

  <span class="hljs-keyword">const</span> token = scheme === <span class="hljs-string">'Bearer'</span> &amp;&amp; tokenFromHeader ? tokenFromHeader : tokenFromCookie;

  <span class="hljs-keyword">if</span> (!token) <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">401</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">'No token provided'</span> });

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = { <span class="hljs-attr">id</span>: decoded.id, <span class="hljs-attr">email</span>: decoded.email };
    next();
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-keyword">const</span> msg = err.name === <span class="hljs-string">'TokenExpiredError'</span> ? <span class="hljs-string">'Access token expired'</span> : <span class="hljs-string">'Invalid token'</span>;
    <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">401</span>).json({ <span class="hljs-attr">message</span>: msg });
  }
}

<span class="hljs-built_in">module</span>.exports = auth;
</code></pre>
<p>How this works:</p>
<ul>
<li><p>The access token can live in a cookie named <code>access_token</code>.</p>
</li>
<li><p>Mark the cookie as <code>httpOnly</code> and <code>secure</code> in production.</p>
</li>
<li><p>Set <code>sameSite: 'strict'</code> to reduce CSRF risk.</p>
</li>
<li><p>For APIs used by browsers, cookies simplify sending tokens. For SPAs that call many domains, an <code>Authorization</code> header may be simpler.</p>
</li>
</ul>
<p>In the next section, we’ll use the same cookie approach for the refresh token. That section explains why refresh belongs in a cookie and how rotation blocks replay.</p>
<h2 id="heading-refresh-tokens-and-rotation">Refresh Tokens and Rotation</h2>
<p>Access tokens are short-lived and used on every request. They prove the user identity quickly. Refresh tokens live longer and are used only to get new access tokens when the old ones expire. This split keeps day-to-day requests fast and limits the damage if a token leaks.</p>
<p>We will store the refresh token in an HTTP-only cookie. This reduces exposure to scripts and keeps the flow smooth.</p>
<h3 id="heading-1-install-and-setup">1. Install and Setup</h3>
<p>We already have <code>cookie-parser</code>. We won’t add anything new for now, but we will use <a target="_blank" href="https://nodejs.org/api/crypto.html">Node’s built-in <code>crypto</code> module</a> to hash the refresh token before storing it. As a reminder, hashing means the raw token is never saved. If the database leaks, attackers cannot use the hashes to log in.</p>
<p>Create <code>models/refreshToken.js</code>:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> mongoose = <span class="hljs-built_in">require</span>(<span class="hljs-string">'mongoose'</span>);

<span class="hljs-keyword">const</span> refreshTokenSchema = <span class="hljs-keyword">new</span> mongoose.Schema({
  <span class="hljs-attr">user</span>: { <span class="hljs-attr">type</span>: mongoose.Schema.Types.ObjectId, <span class="hljs-attr">ref</span>: <span class="hljs-string">'User'</span>, <span class="hljs-attr">index</span>: <span class="hljs-literal">true</span> },
  <span class="hljs-attr">tokenHash</span>: { <span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>, <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">unique</span>: <span class="hljs-literal">true</span> },
  <span class="hljs-attr">jti</span>: { <span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>, <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">index</span>: <span class="hljs-literal">true</span> },
  <span class="hljs-attr">expiresAt</span>: { <span class="hljs-attr">type</span>: <span class="hljs-built_in">Date</span>, <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">index</span>: <span class="hljs-literal">true</span> },
  <span class="hljs-attr">revokedAt</span>: { <span class="hljs-attr">type</span>: <span class="hljs-built_in">Date</span>, <span class="hljs-attr">default</span>: <span class="hljs-literal">null</span> },
  <span class="hljs-attr">replacedBy</span>: { <span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>, <span class="hljs-attr">default</span>: <span class="hljs-literal">null</span> }, <span class="hljs-comment">// new jti when rotated</span>
  <span class="hljs-attr">createdAt</span>: { <span class="hljs-attr">type</span>: <span class="hljs-built_in">Date</span>, <span class="hljs-attr">default</span>: <span class="hljs-built_in">Date</span>.now },
  <span class="hljs-attr">ip</span>: <span class="hljs-built_in">String</span>,
  <span class="hljs-attr">userAgent</span>: <span class="hljs-built_in">String</span>
});

<span class="hljs-built_in">module</span>.exports = mongoose.model(<span class="hljs-string">'RefreshToken'</span>, refreshTokenSchema);
</code></pre>
<h3 id="heading-2-token-helpers">2. Token Helpers</h3>
<p>Create <code>utils/tokens.js</code> for clean, reusable logic.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> jwt = <span class="hljs-built_in">require</span>(<span class="hljs-string">'jsonwebtoken'</span>);
<span class="hljs-keyword">const</span> crypto = <span class="hljs-built_in">require</span>(<span class="hljs-string">'crypto'</span>);
<span class="hljs-keyword">const</span> RefreshToken = <span class="hljs-built_in">require</span>(<span class="hljs-string">'../models/refreshToken'</span>);

<span class="hljs-keyword">const</span> ACCESS_TTL = <span class="hljs-string">'15m'</span>;
<span class="hljs-keyword">const</span> REFRESH_TTL_SEC = <span class="hljs-number">60</span> * <span class="hljs-number">60</span> * <span class="hljs-number">24</span> * <span class="hljs-number">7</span>; <span class="hljs-comment">// 7 days</span>

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">hashToken</span>(<span class="hljs-params">token</span>) </span>{
  <span class="hljs-keyword">return</span> crypto.createHash(<span class="hljs-string">'sha256'</span>).update(token).digest(<span class="hljs-string">'hex'</span>);
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createJti</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> crypto.randomBytes(<span class="hljs-number">16</span>).toString(<span class="hljs-string">'hex'</span>);
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">signAccessToken</span>(<span class="hljs-params">user</span>) </span>{
  <span class="hljs-keyword">const</span> payload = { <span class="hljs-attr">id</span>: user._id.toString(), <span class="hljs-attr">email</span>: user.email };
  <span class="hljs-keyword">return</span> jwt.sign(payload, process.env.JWT_SECRET, { <span class="hljs-attr">expiresIn</span>: ACCESS_TTL });
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">signRefreshToken</span>(<span class="hljs-params">user, jti</span>) </span>{
  <span class="hljs-keyword">const</span> payload = { <span class="hljs-attr">id</span>: user._id.toString(), jti };
  <span class="hljs-keyword">const</span> token = jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET, { <span class="hljs-attr">expiresIn</span>: REFRESH_TTL_SEC });
  <span class="hljs-keyword">return</span> token;
}

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">persistRefreshToken</span>(<span class="hljs-params">{ user, refreshToken, jti, ip, userAgent }</span>) </span>{
  <span class="hljs-keyword">const</span> tokenHash = hashToken(refreshToken);
  <span class="hljs-keyword">const</span> expiresAt = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(<span class="hljs-built_in">Date</span>.now() + REFRESH_TTL_SEC * <span class="hljs-number">1000</span>);
  <span class="hljs-keyword">await</span> RefreshToken.create({ <span class="hljs-attr">user</span>: user._id, tokenHash, jti, expiresAt, ip, userAgent });
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setRefreshCookie</span>(<span class="hljs-params">res, refreshToken</span>) </span>{
  <span class="hljs-keyword">const</span> isProd = process.env.NODE_ENV === <span class="hljs-string">'production'</span>;
  res.cookie(<span class="hljs-string">'refresh_token'</span>, refreshToken, {
    <span class="hljs-attr">httpOnly</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">secure</span>: isProd,
    <span class="hljs-attr">sameSite</span>: <span class="hljs-string">'strict'</span>,
    <span class="hljs-attr">path</span>: <span class="hljs-string">'/api/auth/refresh'</span>,
    <span class="hljs-attr">maxAge</span>: REFRESH_TTL_SEC * <span class="hljs-number">1000</span>
  });
}

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">rotateRefreshToken</span>(<span class="hljs-params">oldDoc, user, req, res</span>) </span>{
  <span class="hljs-comment">// revoke old</span>
  oldDoc.revokedAt = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>();
  <span class="hljs-keyword">const</span> newJti = createJti();
  oldDoc.replacedBy = newJti;
  <span class="hljs-keyword">await</span> oldDoc.save();

  <span class="hljs-comment">// issue new</span>
  <span class="hljs-keyword">const</span> newAccess = signAccessToken(user);
  <span class="hljs-keyword">const</span> newRefresh = signRefreshToken(user, newJti);
  <span class="hljs-keyword">await</span> persistRefreshToken({
    user,
    <span class="hljs-attr">refreshToken</span>: newRefresh,
    <span class="hljs-attr">jti</span>: newJti,
    <span class="hljs-attr">ip</span>: req.ip,
    <span class="hljs-attr">userAgent</span>: req.headers[<span class="hljs-string">'user-agent'</span>] || <span class="hljs-string">''</span>
  });
  setRefreshCookie(res, newRefresh);
  <span class="hljs-keyword">return</span> { <span class="hljs-attr">accessToken</span>: newAccess };
}

<span class="hljs-built_in">module</span>.exports = {
  hashToken,
  createJti,
  signAccessToken,
  signRefreshToken,
  persistRefreshToken,
  setRefreshCookie,
  rotateRefreshToken
};
</code></pre>
<p>In this code,</p>
<ul>
<li><p>signAccessToken creates a short token with the user ID and email.</p>
</li>
<li><p>signRefreshToken creates a long-lived token with a jti value. The jti lets us rotate and track tokens.</p>
</li>
<li><p>persistRefreshToken hashes the refresh token and stores metadata like expiry and device info.</p>
</li>
<li><p>setRefreshCookie writes the HTTP-only cookie so the browser sends it to the refresh endpoint automatically.</p>
</li>
<li><p>rotateRefreshToken revokes the old token, issues a new pair, and saves the new record. Rotation blocks replay if an old refresh token is stolen.</p>
</li>
</ul>
<h3 id="heading-3-issue-refresh-token-on-login">3. Issue Refresh Token on Login</h3>
<p>Update your <code>routes/auth.js</code> login handler to create and store a refresh token, then set the cookie.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">'express'</span>);
<span class="hljs-keyword">const</span> bcrypt = <span class="hljs-built_in">require</span>(<span class="hljs-string">'bcryptjs'</span>);
<span class="hljs-keyword">const</span> jwt = <span class="hljs-built_in">require</span>(<span class="hljs-string">'jsonwebtoken'</span>);
<span class="hljs-keyword">const</span> User = <span class="hljs-built_in">require</span>(<span class="hljs-string">'../models/user'</span>);
<span class="hljs-keyword">const</span> RefreshToken = <span class="hljs-built_in">require</span>(<span class="hljs-string">'../models/refreshToken'</span>);
<span class="hljs-keyword">const</span> {
  createJti,
  signAccessToken,
  signRefreshToken,
  persistRefreshToken,
  setRefreshCookie
} = <span class="hljs-built_in">require</span>(<span class="hljs-string">'../utils/tokens'</span>);

<span class="hljs-keyword">const</span> router = express.Router();

router.post(<span class="hljs-string">'/login'</span>, <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> { email, password } = req.body;

    <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> User.findOne({ email });
    <span class="hljs-keyword">if</span> (!user) <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">'Invalid credentials'</span> });

    <span class="hljs-keyword">const</span> isMatch = <span class="hljs-keyword">await</span> bcrypt.compare(password, user.password);
    <span class="hljs-keyword">if</span> (!isMatch) <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">'Invalid credentials'</span> });

    <span class="hljs-keyword">const</span> accessToken = signAccessToken(user);

    <span class="hljs-keyword">const</span> jti = createJti();
    <span class="hljs-keyword">const</span> refreshToken = signRefreshToken(user, jti);

    <span class="hljs-keyword">await</span> persistRefreshToken({
      user,
      refreshToken,
      jti,
      <span class="hljs-attr">ip</span>: req.ip,
      <span class="hljs-attr">userAgent</span>: req.headers[<span class="hljs-string">'user-agent'</span>] || <span class="hljs-string">''</span>
    });

    setRefreshCookie(res, refreshToken);

    res.json({ accessToken });
  } <span class="hljs-keyword">catch</span> (err) {
    res.status(<span class="hljs-number">500</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">'Server error'</span> });
  }
});

<span class="hljs-built_in">module</span>.exports = router;
</code></pre>
<p>On login, we issue both tokens. The access token goes to the JSON response. The refresh token goes to an HTTP-only cookie scoped to <code>/api/auth/refresh</code>. This keeps the refresh token away from frontend code while still letting the browser send it to the refresh endpoint.</p>
<h3 id="heading-4-the-refresh-endpoint">4. The Refresh Endpoint</h3>
<p>Create an endpoint that reads the refresh cookie, verifies it, checks the database entry, and rotates it. If all checks pass, it returns a new access token and sets a new refresh cookie.</p>
<p>Add to <code>routes/auth.js</code>:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> { hashToken, rotateRefreshToken } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'../utils/tokens'</span>);

router.post(<span class="hljs-string">'/refresh'</span>, <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> token = req.cookies?.refresh_token;
    <span class="hljs-keyword">if</span> (!token) <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">401</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">'No refresh token'</span> });

    <span class="hljs-keyword">let</span> decoded;
    <span class="hljs-keyword">try</span> {
      decoded = jwt.verify(token, process.env.REFRESH_TOKEN_SECRET);
    } <span class="hljs-keyword">catch</span> (err) {
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">401</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">'Invalid or expired refresh token'</span> });
    }

    <span class="hljs-keyword">const</span> tokenHash = hashToken(token);
    <span class="hljs-keyword">const</span> doc = <span class="hljs-keyword">await</span> RefreshToken.findOne({ tokenHash, <span class="hljs-attr">jti</span>: decoded.jti }).populate(<span class="hljs-string">'user'</span>);

    <span class="hljs-keyword">if</span> (!doc) {
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">401</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">'Refresh token not recognized'</span> });
    }
    <span class="hljs-keyword">if</span> (doc.revokedAt) {
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">401</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">'Refresh token revoked'</span> });
    }
    <span class="hljs-keyword">if</span> (doc.expiresAt &lt; <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>()) {
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">401</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">'Refresh token expired'</span> });
    }

    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> rotateRefreshToken(doc, doc.user, req, res);
    <span class="hljs-keyword">return</span> res.json({ <span class="hljs-attr">accessToken</span>: result.accessToken });
  } <span class="hljs-keyword">catch</span> (err) {
    res.status(<span class="hljs-number">500</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">'Server error'</span> });
  }
});
</code></pre>
<p>The refresh endpoint verifies the cookie, checks the database record, confirms it is not expired or revoked, then rotates it. Rotation sets <code>revokedAt</code> on the old record and creates a new one with a fresh <code>jti</code>. The response returns a new access token and sets a new refresh cookie.</p>
<h3 id="heading-5-logout-and-revoke">5. Logout and Revoke</h3>
<p>On logout, revoke the current refresh token and clear the cookie.</p>
<pre><code class="lang-js">router.post(<span class="hljs-string">'/logout'</span>, <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> token = req.cookies?.refresh_token;
    <span class="hljs-keyword">if</span> (token) {
      <span class="hljs-keyword">const</span> tokenHash = hashToken(token);
      <span class="hljs-keyword">const</span> doc = <span class="hljs-keyword">await</span> RefreshToken.findOne({ tokenHash });
      <span class="hljs-keyword">if</span> (doc &amp;&amp; !doc.revokedAt) {
        doc.revokedAt = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>();
        <span class="hljs-keyword">await</span> doc.save();
      }
    }
    res.clearCookie(<span class="hljs-string">'refresh_token'</span>, { <span class="hljs-attr">path</span>: <span class="hljs-string">'/api/auth/refresh'</span> });
    res.json({ <span class="hljs-attr">message</span>: <span class="hljs-string">'Logged out'</span> });
  } <span class="hljs-keyword">catch</span> (err) {
    res.status(<span class="hljs-number">500</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">'Server error'</span> });
  }
});
</code></pre>
<p>Logout revokes the matching refresh token if present and clears the cookie. This ends the session cleanly on the server side and the client side.</p>
<h3 id="heading-6-client-flow">6. Client Flow</h3>
<p>Here is how the browser app should behave:</p>
<ul>
<li><p>Keep the access token in memory. Do not put it in localStorage.</p>
</li>
<li><p>Call protected APIs with the <code>Authorization</code> header or let cookies handle it if you chose the cookie approach for access.</p>
</li>
<li><p>If a call fails with <code>Access token expired</code>, call <code>/api/auth/refresh</code>. The browser sends the refresh cookie automatically.</p>
</li>
<li><p>Replace the in-memory access token with the new one.</p>
</li>
<li><p>Retry the original request.</p>
</li>
<li><p>On logout, call <code>/api/auth/logout</code> and clear any local state.</p>
</li>
</ul>
<h3 id="heading-7-security-notes">7. Security Notes</h3>
<p>There are some key steps you can take to make sure everything is secure:</p>
<h4 id="heading-separate-secrets">Separate secrets</h4>
<p>Use a different secret for access and refresh tokens. If the access secret leaks, refresh tokens still use a different key. Set <code>JWT_SECRET</code> and <code>REFRESH_TOKEN_SECRET</code> in <code>.env</code>.</p>
<h4 id="heading-https-only">HTTPS only</h4>
<p>Serve production traffic over HTTPS. Cookies marked <code>secure: true</code> only travel over HTTPS. This protects tokens in transit.</p>
<h4 id="heading-rotate-on-every-refresh">Rotate on every refresh</h4>
<p>Issue a new refresh token and revoke the old one each time you refresh. Rotation makes a stolen old token useless after the next refresh.</p>
<h4 id="heading-hash-refresh-tokens-in-the-database">Hash refresh tokens in the database</h4>
<p>Store a SHA-256 hash, not the raw token. This way a database leak does not give attackers the actual token string.</p>
<h4 id="heading-scope-and-flags-for-cookies">Scope and flags for cookies</h4>
<p>Use <code>httpOnly: true</code>, <code>secure: true</code> in production, <code>sameSite: 'strict'</code>, and a narrow <code>path</code> such as <code>/api/auth/refresh</code>. These flags reduce XSS and CSRF risk and limit where the cookie is sent.</p>
<h4 id="heading-short-access-ttl-and-moderate-refresh-ttl">Short access TTL and moderate refresh TTL</h4>
<p>Keep access tokens short, such as 15 minutes. Use a refresh lifetime like 7 days. This keeps risk low without annoying users.</p>
<h4 id="heading-device-awareness">Device awareness</h4>
<p>Store <code>ip</code> and <code>userAgent</code>. If patterns change in a suspicious way, you can revoke or challenge the session.</p>
<h4 id="heading-auditing-and-limits">Auditing and limits</h4>
<p>Log refresh events and consider rate limits on the refresh endpoint. This helps detect abuse.’</p>
<p>Add to <code>.env</code>:</p>
<pre><code class="lang-plaintext">REFRESH_TOKEN_SECRET=your_refresh_secret_key
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You now have a working authentication system that uses JWTs and refresh tokens to keep users logged in safely. The access token handles quick verification. The refresh token quietly renews access when it expires. Together, they strike a balance between security and convenience.</p>
<p>You built user registration, login, protected routes, and a full refresh flow. You also learned how to rotate refresh tokens, store them securely, and handle logout cleanly. Each step adds another layer of safety that keeps your app and users protected.</p>
<p>From here, you can expand this setup to match your real project. You can add role-based permissions, track user sessions by device, or move the logic into a dedicated authentication service. What matters most is understanding the flow and keeping tokens short-lived and well-guarded.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ The JSON Web Token Handbook: Learn to Use JWTs for Web Authentication ]]>
                </title>
                <description>
                    <![CDATA[ JWT stands for JSON Web Token, and it’s one of those terms you’ll constantly come across in modern web development. At its core, a JWT is a JSON-based open standard format that allows you to represent specific claims securely between two parties. The... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/the-json-web-token-handbook-learn-to-use-jwts-for-web-authentication/</link>
                <guid isPermaLink="false">68e6ab03a5598a61a63ce39d</guid>
                
                    <category>
                        <![CDATA[ JSON Web Tokens (JWT) ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JWT ]]>
                    </category>
                
                    <category>
                        <![CDATA[ authentication ]]>
                    </category>
                
                    <category>
                        <![CDATA[ token ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Sumit Saha ]]>
                </dc:creator>
                <pubDate>Wed, 08 Oct 2025 18:18:43 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1759947512495/9c8aee78-1a83-4958-8c01-110e2247286d.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>JWT stands for JSON Web Token, and it’s one of those terms you’ll constantly come across in modern web development.</p>
<p>At its core, a JWT is a JSON-based open standard format that allows you to represent specific claims securely between two parties. The exciting part is how widely JWT is used, especially in microservice architectures and modern authentication systems.</p>
<p>In this article, we’ll break down what JWTs really are, explore their structure, and see exactly how they help secure web applications. By the end, you’ll understand why developers rely on JWTs every single day.</p>
<h2 id="heading-heres-what-well-cover">Here’s What We’ll Cover</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-a-jwt">What is a JWT?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-do-we-need-tokens">Why Do We Need Tokens?</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-session-tokens-the-classic-approach">Session Tokens: The Classic Approach</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-jwt-the-modern-solution">JWT: The Modern Solution</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-jwt-structure-header-payload-amp-signature">JWT Structure: Header, Payload &amp; Signature</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-example-decoding-a-jwt">Example: Decoding a JWT</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-jwts-ensure-security-the-signature">How JWTs Ensure Security: The Signature</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-security-considerations-and-token-management">Security Considerations and Token Management</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-create-jwts-in-different-languages">How to Create JWTs in Different Languages</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-practical-implementation-jwt-authentication-with-express-mongodb">Practical Implementation: JWT Authentication with Express + MongoDB</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-1-project-setup-amp-dependencies">1. Project Setup &amp; Dependencies</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-project-folder-structure">2. Project Folder Structure</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-step-by-step-implementation">3. Step-by-Step Implementation</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-4-how-to-test-your-api">4. How to Test Your API</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-summary">Summary</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-final-words">Final Words</a></p>
</li>
</ol>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<p>To follow along and get the most out of this guide, you should have:</p>
<ol>
<li><p>Basic familiarity with JavaScript / Node.js</p>
</li>
<li><p>Node.js and npm installed on your local machine</p>
</li>
<li><p>Basic understanding of HTTP and REST APIs</p>
</li>
<li><p>Understanding of JSON and how to parse/serialize it</p>
</li>
<li><p>Basic knowledge of Express (or ability to follow along)</p>
</li>
<li><p>A running instance of MongoDB (local or remote)</p>
</li>
<li><p>Experience with asynchronous code / Promises / async-await</p>
</li>
<li><p>Familiarity with environment variables / .env setup</p>
</li>
</ol>
<p>I’ve also created a video to go along with this article. If you’re the type who likes to learn from video as well as text, you can check it out here:</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/6drpx_QcMdg" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
<p> </p>
<h2 id="heading-what-is-a-jwt">What is a JWT?</h2>
<p>JWTs are most commonly used for authentication today, but that wasn’t actually their original purpose. They were created to provide a standard way for two parties to securely exchange information. In fact, there’s even an industry standard specification (<a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc7519">RFC 7519</a>) that lays out exactly how JWTs should be structured and how they’re meant to be used for data exchange. Think of it like <a target="_blank" href="https://en.wikipedia.org/wiki/ECMAScript#:~:text=ECMAScript%20\(%2F%CB%88%C9%9Bkm,pages%20across%20different%20web%20browsers.">ECMAScript</a>, or ES, which defines the standard for JavaScript.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759525281325/62565bc2-dc09-4565-8e5b-12b6333e6ff6.jpeg" alt="Client Server Secure Communication" class="image--center mx-auto" width="1919" height="1080" loading="lazy"></p>
<p>In real-world applications, JWTs are primarily used for authentication, and that’s the angle we’ll focus on in this article.</p>
<p>But remember that JWTs weren’t designed only for authentication. There are other ways to handle authentication too, and one of the most popular alternatives is session tokens.</p>
<h2 id="heading-why-do-we-need-tokens">Why Do We Need Tokens?</h2>
<p>Whatever authentication strategy we use, whether it’s a session token or a JWT, the underlying reason is the same: the stateless nature of the HTTP protocol.</p>
<p>When we exchange requests and responses from a browser to a server or between servers using HTTP, the protocol itself does not retain any information.</p>
<p><em>Stateless</em> means that during interactions between the client and the server, HTTP doesn’t remember any previous requests or data. In other words, every request must carry all the necessary information separately. HTTP doesn’t store any data on its own. Once it receives information, it forgets it. That’s why we say HTTP is stateless, as it has no inherent state or persistent information.</p>
<p>Think of it this way: when we access a webpage from a server, what information do we actually send to the server? If it’s a simple static website, we don’t need to send much. We just send the URL of the page to the server, and the server responds by delivering the corresponding HTML page. This means the server doesn’t need to remember any information or maintain any state, which is exactly how HTTP is designed to work, because HTTP itself is stateless.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759525352836/7e6081f5-7d34-462a-9a7d-bcffd0242e00.jpeg" alt="Simple HTML Response from a Static Website" class="image--center mx-auto" width="1919" height="1080" loading="lazy"></p>
<p>But if the web application provides different responses for each user – in other words, if the website is dynamic – then sending only the URL is not sufficient. The user must also send their identity along with the URL to the server.</p>
<p>For example, if a user wants to access <code>page-1</code>, they must tell the server: “<em>I am User A, please give me page-1.</em>” The server will then respond with <code>page-1</code> accordingly. But next time, if the user requests, “<em>Now give me page-2</em>”, what will the server do? Since HTTP is stateless, if the request doesn’t include the user’s identity, the server won’t know which response to provide. This means that with every request, the user must provide their identity, right?</p>
<p>But if we look at the websites around us, do we really have to provide our identity every single time? Take Facebook as an example. Once we authenticate and log in, the server shows us the homepage when we request it, or our profile page when we request that, without requiring us to authenticate with every single request.</p>
<p>So the question is, if HTTP is stateless, how is this possible? How does the web application remember our browsing session? The answer is that, web applications can maintain sessions in different ways, and one of the most common methods is by using <strong>tokens</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759525399836/7b7cdeab-4baa-4cda-bbeb-aaf4e4d4170c.jpeg" alt="How Server Remember our Browsing Session?" class="image--center mx-auto" width="1918" height="1077" loading="lazy"></p>
<h3 id="heading-session-tokens-the-classic-approach">Session Tokens: The Classic Approach</h3>
<p>There are two popular options for this. One is a <strong>Session Token</strong>, and the other is a <strong>JSON Web Token (JWT)</strong>. Let’s understand both so that it becomes clear what JWTs are and why they’re used.</p>
<p>Imagine a scenario in a company’s customer care department. A customer calls in with a complaint. The customer support representative listens to the issue and tries various troubleshooting steps but is unable to resolve the problem.</p>
<p>At this point, they forward the case to their higher management team and create a case file for the customer. This file contains all conversations with the customer and details of the troubleshooting attempts. The customer is then given a case ID or ticket ID, so that the next time they call, they don’t have to go through the same steps all over again.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759525453002/c56bb7da-f6dd-4afe-b16b-966149bc7f91.jpeg" alt="Customer Care Scenerio 1 - Session Token Analogy" class="image--center mx-auto" width="1920" height="1080" loading="lazy"></p>
<p>The next day, when the customer calls again, they give their ticket ID to the customer care representative. The representative searches the system using that ticket ID, retrieves the details, and is able to respond accurately to the customer.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759525515798/426af5fa-ff38-4ce2-ae1b-48ca1a8f1e6c.jpeg" alt="Customer Care Scenerio 2 - Session Token Analogy" class="image--center mx-auto" width="1920" height="1080" loading="lazy"></p>
<p>This scenario illustrates how authentication works in a web application using a session token. When a user authenticates, the server creates a session and keeps track of it. A session ID is generated for that session and sent back to the user, similar to the support ticket in the earlier example. From then on, whenever the user sends a request to the server, they include this session ID or token. The server looks up the session using that ID and identifies the client. Since the server has to handle multiple clients, this session token method has become an effective and widely used strategy for authentication.</p>
<p>And how the client sends the session ID to the server can vary depending on the implementation. The most common method is to store the session ID in the browser’s cookies. The advantage of this approach is that whenever the browser sends a request to the same server, it automatically adds the cookie information to the request header. This is a built-in behaviour of browsers, so no extra steps are needed.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759525561275/5881de41-571d-40ca-a0f7-4022d8c41754.jpeg" alt="Session Token Example" class="image--center mx-auto" width="1920" height="1080" loading="lazy"></p>
<p>When the user authenticates, the server saves data in the browser’s cookie, and from then on, that cookie information is sent automatically with every request, allowing the server to recognize the user. This was a very popular method, although in modern applications it has become a bit outdated.</p>
<p>But this mechanism has some issues. The biggest problem is that it assumes there is only a single server. In modern web applications, there are usually multiple servers. In such cases, a load balancer sits in front and decides which server will handle the user’s request.</p>
<p>Let’s say the session token method is being used. When the user sends the first request, the load balancer forwards it to <code>Server-1</code>. <code>Server-1</code> creates a session ID and sends it back to the client. Later, when the user sends another request, the load balancer routes it to <code>Server-2</code>. But <code>Server-2</code> doesn’t have that session ID stored, so how will it know which user the request belongs to?</p>
<p>The common solution to this is to store session IDs not on a specific server but in a shared <a target="_blank" href="https://redis.io/">Redis</a> database, so that any server can verify the session ID from there. This is what’s called a <strong>Redis cache</strong>. But in a microservice architecture, this approach has a weakness. If for some reason the Redis cache goes down, the servers may still be running, but the authentication mechanism will fail. This is exactly where JSON Web Tokens come in, offering a slightly different approach.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759525611999/a970e2d9-6663-4a4e-9c63-37ea13470b90.jpeg" alt="Session Token Handling Multiple Servers with Redis Cache" class="image--center mx-auto" width="1920" height="1080" loading="lazy"></p>
<h3 id="heading-jwt-the-modern-solution">JWT: The Modern Solution</h3>
<p>Let’s revisit the customer care department example. This time, imagine there’s no phone or system. The customer comes directly to the office and meets the support agent in person. Since the agent doesn’t have any system this time, they can’t store all the information like before. Instead, they write everything down on a piece of paper and tell the customer, “<em>Next time you come, bring this with you.</em>”</p>
<p>This means the method is a bit different from the previous concept, right? But there’s still a problem: “<strong>validity</strong>”. If the customer isn’t legitimate and acts maliciously, how can the support representative trust them? The next day, if the customer comes in with the same information written on a blank sheet of paper, how can the agent verify the validity of their identity?</p>
<p>In this case, a possible solution is for the customer care executive to sign the paper when giving it to the customer. Then, when the customer brings the paper back, the support representative can verify the signature and confidently provide the service.</p>
<p>JSON Web Tokens work in a similar way. Here, when the client authenticates, instead of the server saving all the information, it sends all the user’s information as a JSON token along with a signature. Later, with each subsequent request, the client sends the entire token along with the request, which contains information like which user it is, their name, and other necessary details.</p>
<p>In this case, the server doesn’t save anything, and all the information stays with the client. Each time the client sends a request with this token, the server can read it, identify which user made the request, and provide the necessary data.</p>
<p>This token is not just a simple ID. It’s a JSON object containing all the information, and this is what we call a JSON Web Token. How the client stores this JWT is entirely up to the client. The most common methods are storing it in the browser’s cookies or local storage.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759525648690/691848c9-e4c2-4b3f-b3f5-06623627e38f.jpeg" alt="JSON Web Token Analogy" class="image--center mx-auto" width="1920" height="1080" loading="lazy"></p>
<h3 id="heading-jwt-structure-header-payload-amp-signature">JWT Structure: Header, Payload, &amp; Signature</h3>
<p>As mentioned, the server receives a JSON object, but a JWT doesn’t look like a regular JSON.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759525702339/f74219b8-4a01-4ac4-920b-449faf103520.png" alt="JWT Structure" class="image--center mx-auto" width="1920" height="1078" loading="lazy"></p>
<p>In the image above, it may seem a bit unusual. In fact, it’s an encoded version of the JSON object, a kind of scrambled or compact representation. If you look closely, you’ll see that a JWT is divided into three parts, separated by dots. The first part is the <strong>header</strong>, the second part is the <strong>JSON payload,</strong> which essentially holds our data, and the third part is the <strong>signature</strong>.</p>
<p>If we examine each part individually:</p>
<ul>
<li><p>The <strong>header</strong> is a separate JSON object.</p>
</li>
<li><p>The <strong>payload</strong> is also a separate JSON object containing our data.</p>
</li>
<li><p>The third part is the <strong>signature</strong>.</p>
</li>
</ul>
<p>But what does the signature mean here? Simply put, the signature is a hash value. Our data is hashed using a secret key to create the signature. This secret key is kept on the server. So, when this JSON Web Token is sent to the server, the server can use that secret key to verify the signature. This ensures that the token is valid and has not been tampered with.</p>
<h2 id="heading-example-decoding-a-jwt">Example: Decoding a JWT</h2>
<p>Let’s look at an example. The best website for working with JWTs and understanding their structure is <a target="_blank" href="http://jwt.io">jwt.io</a><a target="_blank" href="https://jwt.io/">.</a> If you paste a JWT into the site, three sections appear: the header, payload, and signature. The payload is shown in the “Decoded Payload” section, which contains content and data. You’ll see there’s an ID, a JSON object with a name, and an expiration time.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759525738886/84c2532f-dc09-4a96-83de-ecd4a24d958f.jpeg" alt="Decoding a JWT" class="image--center mx-auto" width="636" height="367" loading="lazy"></p>
<p>The header is also a completely valid JSON object, which specifies an algorithm and shows the type –essentially indicating which algorithm will be used to create or verify this JWT.</p>
<p>So, the main data is in the “Decoded Payload” section, and the third part is the signature. Now there’s an important point to note: you might wonder where this scrambled-looking token comes from. It’s actually very simple. The data in the “Decoded Payload” is <strong>Base64 encoded</strong>, and that’s what forms the appearance of this scrambled token.</p>
<p>If you copy this part of the JWT and paste it into any online Base64 decoder, you’ll immediately see the data.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759525794705/4ee950a2-2ad0-40b4-8287-fdfea9543a6f.png" alt="Base64 Encode Decode" class="image--center mx-auto" width="1919" height="1080" loading="lazy"></p>
<p>What does this mean? It means that if this data is encoded again using Base64, the same token will be generated. The header works the same way as well.</p>
<p>And the final point: the scrambled or encoded part. Is it done for security? No, it’s not for security. It’s done purely for convenience. JSON objects can be quite large, and not all programming languages handle them in the same way. In JavaScript it’s easy, but in other languages, it can sometimes cause issues. So to make it easier to handle, the data is Base64 encoded. This is not for security, as encoding it like this doesn’t make the data secure, because the information can still be viewed publicly.</p>
<p>As you can see in the diagram above, the moment you enter it on this site, your data is immediately visible. This means that no sensitive information should be stored here, only user identification details, like a user ID or other public information. <strong>Passwords or any secret keys should never be stored in the token, because they can be easily read.</strong> Even though it looks scrambled or encoded, it is actually public.</p>
<h2 id="heading-how-jwts-ensure-security-the-signature">How JWTs Ensure Security: The Signature</h2>
<p>Now let’s move to the security part, which is ensured by the signature. In our earlier paper example, a person could simply add a signature by hand.</p>
<p>But for data, the process of creating a signature is different. For data, the signature is created cryptographically using a secret key, which is the actual signature. The process of creating the signature is as follows:</p>
<ol>
<li><p>The data is Base64 encoded.</p>
</li>
<li><p>It is concatenated with the secret key.</p>
</li>
<li><p>It is encoded again in Base64.</p>
</li>
</ol>
<p>The configuration specifies an algorithm. This algorithm can be changed, but the same algorithm used to create the token must be used to verify it. In other words, the algorithm for generating and verifying the token must always be the same.</p>
<p>Finally, the data is hashed using a secret key. This secret key is not available to the public. Instead, it’s kept only on the server, usually stored securely in a server vault. When this JWT reaches the server, the server uses the secret key to verify whether the token is valid. If it doesn’t match correctly, it will display “invalid signature.” This ensures that the server can confirm whether the token has been tampered with and that its integrity is intact.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759525829955/bf017016-d9fd-43cb-836a-eafe4f35540b.jpeg" alt="The Big Formula" class="image--center mx-auto" width="1224" height="1078" loading="lazy"></p>
<p>For example, if you use <code>love-you-all-from-logicbaselabs</code> as the signature, and the server verifies it, it will show “<em>signature verified</em>”. This demonstrates that the secret key exists only on the server. This ensures that even though public information is displayed, the token’s validity can be confirmed.</p>
<p>JSON Web Tokens aren’t like a password, though. They primarily serve to identify the user. The server can check the JWT to determine whether it belongs to a valid user. In other words, the JWT represents the user’s identity. It’s a very important token, containing secure content along with the signature.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759525873387/a434b453-0a38-41a5-93f3-bd12b46806f3.jpeg" alt="Signature Verification" class="image--center mx-auto" width="1920" height="1078" loading="lazy"></p>
<h2 id="heading-security-considerations-and-token-management">Security Considerations and Token Management</h2>
<p>One important thing to remember: if someone gets hold of your JWT, meaning they have the exact same token, they can easily log in as that user. They just need to send requests with that token to gain the necessary access.</p>
<p>You could think of it like this: if someone gets hold of your Facebook password, they can log in to your Facebook account. Similarly, if someone obtains your PayPal account PIN, they can easily access your account. In other words, if someone gets hold of your most secure information, there’s no way to protect it.</p>
<p>The same applies to JWTs: keeping the token safely on the client side is absolutely crucial. In this regard, we are somewhat vulnerable.</p>
<p>There is, though, one key difference. In the case of session tokens, if we assume an account has been compromised, the server can invalidate that session. In other words, no one can log in using that session ID anymore.</p>
<p>But with a JWT, the token remains valid until its expiration time. So there’s no direct way to invalidate it. Since the token is cryptographically self-contained and signed with the server’s secret key, once it’s created, it cannot be directly revoked by the server.</p>
<p>The only way to handle this is what’s done on the web: denylisting the token. In other words, the server maintains a separate database listing all JWT tokens that are denylisted. Whenever a request comes in, the server first verifies whether the token is valid. Then, through middleware, it checks whether the token is on the denylist. Only if it’s not on that list is the user allowed access.</p>
<p>So, these are the rules for using JSON Web Tokens. JWTs can be used in any programming language, especially in the context of REST APIs. They are extremely popular and widely used in microservice architectures.</p>
<h2 id="heading-how-to-create-jwts-in-different-languages">How to Create JWTs in Different Languages</h2>
<p>How you create a JWT depends on the programming language you’re using. For example, in Node.js, there are specialized libraries available, like <a target="_blank" href="https://www.npmjs.com/package/jsonwebtoken">jsonwebtoken</a>, so it’s straightforward. And in PHP, there are easy-to-use options for creating JWTs as well. So, JWTs are a universal tool, not limited to any specific programming language. Many people think they’re only for JavaScript, but that’s not true.</p>
<p>And remember that JWTs aren’t just used for authentication purposes. You can use them to represent any kind of identity. For example, if you’re going to a concert, access could be granted using a JWT instead of a regular ticket. When your client uses that JWT, the gateway or server can read the token, provide access to the information, and verify it using the signature.</p>
<h2 id="heading-practical-implementation-jwt-authentication-with-express-mongodb">Practical Implementation: JWT Authentication with Express + MongoDB</h2>
<p>In this section, we will put into practice all the concepts we have learned so far. Using <a target="_blank" href="https://www.freecodecamp.org/news/the-express-handbook/"><strong>Express.js</strong></a> and <a target="_blank" href="https://www.freecodecamp.org/news/how-to-start-using-mongodb/"><strong>MongoDB</strong></a>, we will build a complete JWT authentication system step by step.</p>
<p>Don’t worry if it feels overwhelming at first. We will go carefully, one step at a time, and by the end, you will have a fully working project. Think of it as entering a building floor by floor: we’ll explore each section thoroughly and come out with a solid understanding.</p>
<h3 id="heading-1-project-setup-amp-dependencies">1. Project Setup &amp; Dependencies</h3>
<p>Before writing any code, we need to set up our Node.js project and install the required dependencies.</p>
<h4 id="heading-initialize-the-nodejs-project">Initialize the Node.js Project</h4>
<p>Open your terminal and run:</p>
<pre><code class="lang-javascript">mkdir jwt-auth-demo
cd jwt-auth-demo
npm init -y
</code></pre>
<p>This will create a <code>package.json</code> file with default settings.</p>
<h4 id="heading-install-dependencies">Install Dependencies</h4>
<p>We need some packages to build our JWT authentication system:</p>
<pre><code class="lang-javascript">npm install express mongoose bcryptjs jsonwebtoken dotenv
</code></pre>
<ul>
<li><p><code>express</code>: Fast and minimal Node.js web framework to create API routes.</p>
</li>
<li><p><code>mongoose</code>: ODM (Object Data Modeling) library to interact with MongoDB easily.</p>
</li>
<li><p><code>bcryptjs</code>: Library to hash and compare passwords securely.</p>
</li>
<li><p><code>jsonwebtoken</code>: Library to generate and verify JWT tokens.</p>
</li>
<li><p><code>dotenv</code>: Loads environment variables from a <code>.env</code> file to keep secrets secure.</p>
</li>
</ul>
<h4 id="heading-install-dev-dependencies-optional">Install Dev Dependencies (Optional)</h4>
<p>For development convenience, install <strong>nodemon</strong> to auto-restart the server on file changes:</p>
<pre><code class="lang-javascript">npm install --save-dev nodemon
</code></pre>
<p>Update <code>package.json</code> scripts:</p>
<pre><code class="lang-javascript"><span class="hljs-string">"scripts"</span>: {
  <span class="hljs-string">"start"</span>: <span class="hljs-string">"node server.js"</span>,
  <span class="hljs-string">"dev"</span>: <span class="hljs-string">"nodemon server.js"</span>
}
</code></pre>
<ul>
<li><p><code>npm start</code> runs the server normally.</p>
</li>
<li><p><code>npm run dev</code> runs the server with auto-restart using <strong>nodemon</strong>.</p>
</li>
</ul>
<h3 id="heading-2-project-folder-structure">2. Project Folder Structure</h3>
<pre><code class="lang-javascript">jwt-auth-demo/
│
├── config/
│   └── db.js
│
├── controllers/
│   └── authController.js
│
├── middlewares/
│   └── authMiddleware.js
│
├── models/
│   └── User.js
│
├── routes/
│   └── auth.js
│
├── services/
│   ├── hashService.js
│   └── jwtService.js
│
├── .env
├── server.js
├── package.json
</code></pre>
<p><strong>What goes where?</strong></p>
<ul>
<li><p><code>config/</code>: Database connection and environment config.</p>
</li>
<li><p><code>controllers/</code>: Main logic for each endpoint.</p>
</li>
<li><p><code>middlewares/</code>: Functions that run before controllers (for example, auth checks).</p>
</li>
<li><p><code>models/</code>: Mongoose schemas.</p>
</li>
<li><p><code>routes/</code>: API endpoint definitions.</p>
</li>
<li><p><code>services/</code>: Reusable logic (hashing, JWT).</p>
</li>
<li><p><code>.env</code>: Secrets and config variables.</p>
</li>
<li><p><code>server.js</code>: Entry point of the app.</p>
</li>
</ul>
<h3 id="heading-3-step-by-step-implementation">3. Step-by-Step Implementation</h3>
<h4 id="heading-initialize-the-express-server">Initialize the Express Server</h4>
<p>Before doing anything complex, we need to set up a simple server using Express. Think of this as the heart of our application. This server will be responsible for listening to incoming requests (like user login or register) and sending back responses.</p>
<p><strong>File: server.js</strong></p>
<pre><code class="lang-javascript"><span class="hljs-comment">// server.js</span>

<span class="hljs-comment">// Import the express library to build our server</span>
<span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">"express"</span>);

<span class="hljs-comment">// Create an instance of express</span>
<span class="hljs-keyword">const</span> app = express();

<span class="hljs-comment">// Middleware to parse JSON request bodies (important for APIs)</span>
app.use(express.json());

<span class="hljs-comment">// Default route to test server</span>
app.get(<span class="hljs-string">"/"</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  res.send(<span class="hljs-string">"Hello World! Your server is working 🚀"</span>);
});

<span class="hljs-comment">// Start the server on port 5000</span>
<span class="hljs-keyword">const</span> PORT = process.env.PORT || <span class="hljs-number">5000</span>;
app.listen(PORT, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Server running on http://localhost:<span class="hljs-subst">${PORT}</span>`</span>);
});
</code></pre>
<ul>
<li><p>We import Express and create an app instance.</p>
</li>
<li><p>We use middleware to parse JSON requests (important for APIs).</p>
</li>
<li><p>We define a simple route <code>/</code> to test if our server works.</p>
</li>
<li><p>We start the server on port 5000 and log a message when it's running.</p>
</li>
</ul>
<p>Now, let’s test it:</p>
<ul>
<li><p>Run <code>node server.js</code> or <code>npm run dev</code>.</p>
</li>
<li><p>Open your browser at <code>http://localhost:5000</code>.</p>
</li>
<li><p>You should see: <code>Hello World! Your server is working 🚀</code></p>
</li>
</ul>
<h4 id="heading-connect-mongodb-with-mongoose">Connect MongoDB with Mongoose</h4>
<p>In this step, we want to store users in a database. For that, we will use MongoDB. To interact with MongoDB in Node.js easily, we use Mongoose, which is an ODM library.</p>
<p><strong>File: config/db.js</strong></p>
<pre><code class="lang-javascript"><span class="hljs-comment">// config/db.js</span>

<span class="hljs-comment">// Import mongoose</span>
<span class="hljs-keyword">const</span> mongoose = <span class="hljs-built_in">require</span>(<span class="hljs-string">"mongoose"</span>);

<span class="hljs-comment">// Connect to MongoDB using environment variable</span>
<span class="hljs-keyword">const</span> connectDB = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">await</span> mongoose.connect(process.env.MONGO_URI, {
      <span class="hljs-attr">useNewUrlParser</span>: <span class="hljs-literal">true</span>,
      <span class="hljs-attr">useUnifiedTopology</span>: <span class="hljs-literal">true</span>,
    });
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"✅ MongoDB Connected"</span>);
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"❌ MongoDB Connection Error:"</span>, err.message);
    process.exit(<span class="hljs-number">1</span>); <span class="hljs-comment">// Stop server if DB fails</span>
  }
};

<span class="hljs-built_in">module</span>.exports = connectDB;
</code></pre>
<p>Now our server is connected to MongoDB. Whenever we insert, update, or query data, it will go into this database.</p>
<p><strong>File: .env</strong></p>
<pre><code class="lang-javascript">PORT=<span class="hljs-number">5000</span>
MONGO_URI=mongodb:<span class="hljs-comment">//127.0.0.1:27017/jwt-auth-demo</span>
JWT_SECRET=your_super_secret_key
</code></pre>
<p>The .env file stores sensitive information like your database URI, JWT secret, and server port. By using environment variables, you can keep secrets out of your code and easily change configuration without modifying your source files. Never commit .env to public repositories to protect your credentials.</p>
<h4 id="heading-create-user-model">Create User Model</h4>
<p>In this step, we need to define how a User looks in our database. Each user will have a <strong>name, email, and password</strong>.</p>
<p><strong>File: models/User.js</strong></p>
<pre><code class="lang-javascript"><span class="hljs-comment">// models/User.js</span>
<span class="hljs-keyword">const</span> mongoose = <span class="hljs-built_in">require</span>(<span class="hljs-string">"mongoose"</span>);

<span class="hljs-comment">// Define a schema (blueprint of user data)</span>
<span class="hljs-keyword">const</span> userSchema = <span class="hljs-keyword">new</span> mongoose.Schema({
  <span class="hljs-attr">name</span>: { <span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>, <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span> },
  <span class="hljs-attr">email</span>: { <span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>, <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">unique</span>: <span class="hljs-literal">true</span> },
  <span class="hljs-attr">password</span>: { <span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>, <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span> },
});

<span class="hljs-comment">// Create and export the model</span>
<span class="hljs-built_in">module</span>.exports = mongoose.model(<span class="hljs-string">"User"</span>, userSchema);
</code></pre>
<p>As you can see, each user now has a name, email, and hashed password. This ensures that every user we save has these three fields.</p>
<h4 id="heading-hashing-amp-jwt-services">Hashing &amp; JWT Services</h4>
<p>In this step, we will handle password hashing and JWT management using separate services. This keeps our code organized and reusable.</p>
<p><strong>File: services/hashService.js</strong></p>
<pre><code class="lang-javascript"><span class="hljs-comment">//services/hashService.js</span>

<span class="hljs-keyword">const</span> bcrypt = <span class="hljs-built_in">require</span>(<span class="hljs-string">"bcryptjs"</span>);

<span class="hljs-comment">// Function to hash a plain password</span>
<span class="hljs-built_in">exports</span>.hashPassword = <span class="hljs-keyword">async</span> (plainPassword) =&gt; {
  <span class="hljs-comment">// bcrypt.hash generates a hashed version of the password</span>
  <span class="hljs-comment">// The number 10 is the salt rounds, which affects the hashing complexity</span>
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> bcrypt.hash(plainPassword, <span class="hljs-number">10</span>);
};

<span class="hljs-comment">// Function to compare a plain password with a hashed password</span>
<span class="hljs-built_in">exports</span>.comparePassword = <span class="hljs-keyword">async</span> (plainPassword, hashedPassword) =&gt; {
  <span class="hljs-comment">// bcrypt.compare checks if the plain password matches the hashed one</span>
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> bcrypt.compare(plainPassword, hashedPassword);
};
</code></pre>
<ul>
<li><p><code>hashPassword(plainPassword)</code>: Takes a plain text password and returns a hashed version using bcrypt. Never store plain passwords directly.</p>
</li>
<li><p><code>comparePassword(plainPassword, hashedPassword)</code>: Compares a user-entered password with the hashed password stored in the database. Returns <code>true</code> if they match.</p>
</li>
</ul>
<p><strong>File: services/jwtService.js</strong></p>
<pre><code class="lang-javascript"><span class="hljs-comment">// services/jwtService.js</span>

<span class="hljs-keyword">const</span> jwt = <span class="hljs-built_in">require</span>(<span class="hljs-string">"jsonwebtoken"</span>);

<span class="hljs-comment">// Function to generate a JWT</span>
<span class="hljs-built_in">exports</span>.generateToken = <span class="hljs-function">(<span class="hljs-params">payload</span>) =&gt;</span> {
  <span class="hljs-comment">// jwt.sign creates a signed token using our secret key from environment variables</span>
  <span class="hljs-comment">// expiresIn defines how long the token is valid (1 hour here)</span>
  <span class="hljs-keyword">return</span> jwt.sign(payload, process.env.JWT_SECRET, { <span class="hljs-attr">expiresIn</span>: <span class="hljs-string">"1h"</span> });
};

<span class="hljs-comment">// Function to verify a JWT</span>
<span class="hljs-built_in">exports</span>.verifyToken = <span class="hljs-function">(<span class="hljs-params">token</span>) =&gt;</span> {
  <span class="hljs-comment">// jwt.verify checks if the token is valid and not expired</span>
  <span class="hljs-keyword">return</span> jwt.verify(token, process.env.JWT_SECRET);
};
</code></pre>
<ul>
<li><p><code>generateToken(payload)</code>: Generates a JWT for a user. The <code>payload</code> typically contains user ID and email.</p>
</li>
<li><p><code>verifyToken(token)</code>: Verifies that the JWT is valid and returns the decoded payload if successful.</p>
</li>
<li><p>Using a separate JWT service keeps token logic centralized and easy to manage.</p>
</li>
</ul>
<h4 id="heading-auth-controller">Auth Controller</h4>
<p>In this step, we will handle all authentication-related logic in a separate controller. This keeps routes clean and separates business logic from endpoint definitions.</p>
<p><strong>File: controllers/authController.js</strong></p>
<pre><code class="lang-javascript"><span class="hljs-comment">// controllers/authController.js</span>

<span class="hljs-keyword">const</span> User = <span class="hljs-built_in">require</span>(<span class="hljs-string">"../models/User"</span>);
<span class="hljs-keyword">const</span> { hashPassword, comparePassword } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"../services/hashService"</span>);
<span class="hljs-keyword">const</span> { generateToken } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"../services/jwtService"</span>);

<span class="hljs-comment">// Register new user</span>
<span class="hljs-built_in">exports</span>.register = <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> { name, email, password } = req.body; <span class="hljs-comment">// Get user input</span>

    <span class="hljs-comment">// Step 1: Check if user already exists</span>
    <span class="hljs-keyword">const</span> existingUser = <span class="hljs-keyword">await</span> User.findOne({ email });
    <span class="hljs-keyword">if</span> (existingUser)
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">"User already exists!"</span> });

    <span class="hljs-comment">// Step 2: Hash password using hashService</span>
    <span class="hljs-keyword">const</span> hashedPassword = <span class="hljs-keyword">await</span> hashPassword(password);

    <span class="hljs-comment">// Step 3: Save user to database</span>
    <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">new</span> User({ name, email, <span class="hljs-attr">password</span>: hashedPassword });
    <span class="hljs-keyword">await</span> user.save();

    <span class="hljs-comment">// Step 4: Send success response</span>
    res.status(<span class="hljs-number">201</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">"User registered successfully!"</span> });
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-comment">// Handle errors gracefully</span>
    res.status(<span class="hljs-number">500</span>).json({ <span class="hljs-attr">error</span>: err.message });
  }
};

<span class="hljs-comment">// Login user</span>
<span class="hljs-built_in">exports</span>.login = <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> { email, password } = req.body; <span class="hljs-comment">// Get user input</span>

    <span class="hljs-comment">// Step 1: Find user by email</span>
    <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> User.findOne({ email });
    <span class="hljs-keyword">if</span> (!user)
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">"Invalid email or password"</span> });

    <span class="hljs-comment">// Step 2: Compare provided password with hashed password</span>
    <span class="hljs-keyword">const</span> isMatch = <span class="hljs-keyword">await</span> comparePassword(password, user.password);
    <span class="hljs-keyword">if</span> (!isMatch)
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">"Invalid email or password"</span> });

    <span class="hljs-comment">// Step 3: Generate JWT using jwtService</span>
    <span class="hljs-keyword">const</span> token = generateToken({ <span class="hljs-attr">id</span>: user._id, <span class="hljs-attr">email</span>: user.email });

    <span class="hljs-comment">// Step 4: Send success response with token</span>
    res.json({ <span class="hljs-attr">message</span>: <span class="hljs-string">"Login successful!"</span>, token });
  } <span class="hljs-keyword">catch</span> (err) {
    res.status(<span class="hljs-number">500</span>).json({ <span class="hljs-attr">error</span>: err.message });
  }
};

<span class="hljs-comment">// Protected profile route</span>
<span class="hljs-built_in">exports</span>.profile = <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  <span class="hljs-comment">// req.user is set by auth middleware after token verification</span>
  res.json({
    <span class="hljs-attr">message</span>: <span class="hljs-string">"Welcome to your profile!"</span>,
    <span class="hljs-attr">user</span>: req.user,
  });
};
</code></pre>
<ul>
<li><p><strong>File:</strong> <code>controllers/authController.js</code> – Contains all logic related to authentication.</p>
</li>
<li><p><code>exports.register</code> handles user registration:</p>
<ul>
<li><p>Checks if the user exists.</p>
</li>
<li><p>Hashes the password using <code>hashService</code>.</p>
</li>
<li><p>Saves the new user to MongoDB.</p>
</li>
<li><p>Returns a success message.</p>
</li>
</ul>
</li>
<li><p><code>exports.login</code> handles user login:</p>
<ul>
<li><p>Finds the user by email.</p>
</li>
<li><p>Compares passwords using <code>hashService.comparePassword</code>.</p>
</li>
<li><p>Generates a JWT token if valid.</p>
</li>
<li><p>Returns the token in the response.</p>
</li>
</ul>
</li>
<li><p><code>exports.profile</code> handles protected profile route:</p>
<ul>
<li>Returns user information from <code>req.user</code>, which is set by the auth middleware.</li>
</ul>
</li>
<li><p>Using a controller keeps route definitions clean and separates business logic from endpoint handling.</p>
</li>
</ul>
<h4 id="heading-auth-middleware">Auth Middleware</h4>
<p>In this step, we create a middleware to protect routes by verifying JWTs. Only authenticated users can access protected endpoints.</p>
<p><strong>File: middlewares/authMiddleware.js</strong></p>
<pre><code class="lang-javascript"><span class="hljs-comment">// middlewares/authMiddleware.js</span>

<span class="hljs-keyword">const</span> { verifyToken } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"../services/jwtService"</span>);

<span class="hljs-comment">// Middleware to protect routes</span>
<span class="hljs-built_in">module</span>.exports = <span class="hljs-function">(<span class="hljs-params">req, res, next</span>) =&gt;</span> {
  <span class="hljs-comment">// Step 1: Get Authorization header</span>
  <span class="hljs-keyword">const</span> authHeader = req.headers[<span class="hljs-string">"authorization"</span>];
  <span class="hljs-keyword">if</span> (!authHeader)
    <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">401</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">"No token provided"</span> });

  <span class="hljs-comment">// Step 2: Extract token from format 'Bearer &lt;token&gt;'</span>
  <span class="hljs-keyword">const</span> token = authHeader.split(<span class="hljs-string">" "</span>)[<span class="hljs-number">1</span>];
  <span class="hljs-keyword">if</span> (!token) <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">401</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">"Malformed token"</span> });

  <span class="hljs-keyword">try</span> {
    <span class="hljs-comment">// Step 3: Verify token using jwtService</span>
    <span class="hljs-keyword">const</span> decoded = verifyToken(token);

    <span class="hljs-comment">// Step 4: Attach decoded user info to request object</span>
    req.user = decoded;

    <span class="hljs-comment">// Proceed to next middleware or route handler</span>
    next();
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-comment">// If token is invalid or expired</span>
    res.status(<span class="hljs-number">401</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">"Invalid or expired token"</span> });
  }
};
</code></pre>
<ul>
<li><p><strong>File:</strong> <code>middlewares/authMiddleware.js</code> – Middleware for protecting routes.</p>
</li>
<li><p>Step 1: Checks if the <code>Authorization</code> header is present.</p>
</li>
<li><p>Step 2: Extracts the token from the <code>Bearer &lt;token&gt;</code> format.</p>
</li>
<li><p>Step 3: Verifies the token using <code>jwtService.verifyToken</code>.</p>
</li>
<li><p>Step 4: Attaches the decoded user info to <code>req.user</code> for use in subsequent route handlers.</p>
</li>
<li><p>If the token is missing, malformed, invalid, or expired, the middleware responds with <strong>401 Unauthorized</strong>. This ensures only authenticated users can access protected routes.</p>
</li>
</ul>
<h4 id="heading-auth-routes">Auth Routes</h4>
<p>In this step, we will define authentication-related routes and connect them with the controller and middleware.</p>
<p><strong>File: routes/auth.js</strong></p>
<pre><code class="lang-javascript"><span class="hljs-comment">// routes/auth.js</span>

<span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">"express"</span>);
<span class="hljs-keyword">const</span> router = express.Router();
<span class="hljs-keyword">const</span> authController = <span class="hljs-built_in">require</span>(<span class="hljs-string">"../controllers/authController"</span>);
<span class="hljs-keyword">const</span> authMiddleware = <span class="hljs-built_in">require</span>(<span class="hljs-string">"../middlewares/authMiddleware"</span>);

<span class="hljs-comment">// Step 1: Register route</span>
<span class="hljs-comment">// Users send their name, email, and password to this endpoint</span>
router.post(<span class="hljs-string">"/register"</span>, authController.register);

<span class="hljs-comment">// Step 2: Login route</span>
<span class="hljs-comment">// Users send email and password to receive JWT</span>
router.post(<span class="hljs-string">"/login"</span>, authController.login);

<span class="hljs-comment">// Step 3: Protected profile route</span>
<span class="hljs-comment">// Only accessible to authenticated users with a valid JWT</span>
router.get(<span class="hljs-string">"/profile"</span>, authMiddleware, authController.profile);

<span class="hljs-built_in">module</span>.exports = router;
</code></pre>
<ul>
<li><p><strong>File:</strong> <code>routes/auth.js</code> – Central file to define authentication endpoints.</p>
</li>
<li><p><code>router.post("/register", authController.register)</code>: Handles user registration.</p>
</li>
<li><p><code>router.post("/login", authController.login)</code>: Handles user login and token generation.</p>
</li>
<li><p><code>router.get("/profile", authMiddleware, authController.profile)</code>: Protected route, requires JWT. The <code>authMiddleware</code> ensures only authenticated users can access it.</p>
</li>
<li><p>Using routes with controllers and middleware keeps the application organized and professional.</p>
</li>
</ul>
<h4 id="heading-main-server-file">Main Server File</h4>
<p>This is the main entry point of our application. It sets up the server, connects to the database, and mounts all routes.</p>
<p><strong>File: server.js</strong></p>
<pre><code class="lang-javascript"><span class="hljs-comment">// server.js</span>

<span class="hljs-built_in">require</span>(<span class="hljs-string">"dotenv"</span>).config(); <span class="hljs-comment">// Step 1: Load environment variables from .env</span>
<span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">"express"</span>);
<span class="hljs-keyword">const</span> connectDB = <span class="hljs-built_in">require</span>(<span class="hljs-string">"./config/db"</span>);

<span class="hljs-keyword">const</span> app = express();

<span class="hljs-comment">// Step 2: Connect to MongoDB</span>
connectDB();

<span class="hljs-comment">// Step 3: Middleware to parse JSON request bodies</span>
app.use(express.json());

<span class="hljs-comment">// Step 4: Mount auth routes</span>
<span class="hljs-comment">// All auth-related routes will start with /api/auth</span>
app.use(<span class="hljs-string">"/api/auth"</span>, <span class="hljs-built_in">require</span>(<span class="hljs-string">"./routes/auth"</span>));

<span class="hljs-comment">// Step 5: Default route to test server</span>
app.get(<span class="hljs-string">"/"</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  res.send(<span class="hljs-string">"Hello World! Your server is working 🚀"</span>);
});

<span class="hljs-comment">// Step 6: Start server on PORT from .env or default 5000</span>
<span class="hljs-keyword">const</span> PORT = process.env.PORT || <span class="hljs-number">5000</span>;
app.listen(PORT, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Server running on http://localhost:<span class="hljs-subst">${PORT}</span>`</span>);
});
</code></pre>
<ul>
<li><p><strong>Load environment variables:</strong> Using <code>dotenv</code> to keep secrets and configuration separate from code.</p>
</li>
<li><p><strong>Connect to MongoDB:</strong> Calls <code>connectDB()</code> from <code>config/db.js</code>.</p>
</li>
<li><p><strong>Middleware:</strong> <code>express.json()</code> allows Express to parse JSON request bodies.</p>
</li>
<li><p><strong>Mount routes:</strong> <code>app.use("/api/auth", ...)</code> registers all authentication routes.</p>
</li>
<li><p><strong>Default route:</strong> A simple GET endpoint to verify server is running.</p>
</li>
<li><p><strong>Start server:</strong> <code>app.listen</code> starts listening on the configured port.</p>
</li>
</ul>
<h3 id="heading-4-how-to-test-your-api">4. How to Test Your API</h3>
<p>In this section, you’ll learn how to test your JWT authentication API using tools like Postman or any HTTP client.</p>
<p>Before testing, make sure your server is running. If it’s not running, open a terminal and run:</p>
<pre><code class="lang-javascript">npm run dev
</code></pre>
<p>or</p>
<pre><code class="lang-javascript">node server.js
</code></pre>
<p>This will start your server on the port defined in <code>.env</code> (default <code>5000</code>).</p>
<p>Make sure your MongoDB is running. If using local MongoDB, start it with:</p>
<pre><code class="lang-javascript">mongod
</code></pre>
<p>or ensure your MongoDB service is active.</p>
<p>Always check the terminal for any errors. If the server or database fails to start, your API requests will not work.</p>
<h4 id="heading-register-a-user">Register a User</h4>
<p>Request:</p>
<pre><code class="lang-javascript">POST http:<span class="hljs-comment">//localhost:5000/api/auth/register</span>
Content-Type: application/json

{
  <span class="hljs-string">"name"</span>: <span class="hljs-string">"sumit"</span>,
  <span class="hljs-string">"email"</span>: <span class="hljs-string">"sumit@example.com"</span>,
  <span class="hljs-string">"password"</span>: <span class="hljs-string">"mypassword"</span>
}
</code></pre>
<p>Response:</p>
<pre><code class="lang-javascript">{
  <span class="hljs-string">"message"</span>: <span class="hljs-string">"User registered successfully!"</span>
}
</code></pre>
<p>This sends a POST request to <code>http://localhost:5000/api/auth/register</code> with user details. If successful, you get a confirmation message.</p>
<h4 id="heading-login">Login</h4>
<p>Request:</p>
<pre><code class="lang-javascript">POST http:<span class="hljs-comment">//localhost:5000/api/auth/login</span>
Content-Type: application/json

{
  <span class="hljs-string">"email"</span>: <span class="hljs-string">"sumit@example.com"</span>,
  <span class="hljs-string">"password"</span>: <span class="hljs-string">"mypassword"</span>
}
</code></pre>
<p>Response:</p>
<pre><code class="lang-javascript">{
  <span class="hljs-string">"message"</span>: <span class="hljs-string">"Login successful!"</span>,
  <span class="hljs-string">"token"</span>: <span class="hljs-string">"&lt;JWT_TOKEN&gt;"</span>
}
</code></pre>
<p>This sends a POST request to <code>http://localhost:5000/api/auth/login</code> with email and password. If the credentials are correct, you receive a JWT to access protected routes.</p>
<h4 id="heading-access-protected-route">Access Protected Route</h4>
<p>Request:</p>
<pre><code class="lang-javascript">GET http:<span class="hljs-comment">//localhost:5000/api/auth/profile</span>
Authorization: Bearer &lt;JWT_TOKEN&gt;
</code></pre>
<p>Response:</p>
<pre><code class="lang-javascript">{
  <span class="hljs-string">"message"</span>: <span class="hljs-string">"Welcome to your profile!"</span>,
  <span class="hljs-string">"user"</span>: {
    <span class="hljs-string">"id"</span>: <span class="hljs-string">"..."</span>,
    <span class="hljs-string">"email"</span>: <span class="hljs-string">"sumit@example.com"</span>,
    <span class="hljs-string">"iat"</span>: ...,
    <span class="hljs-string">"exp"</span>: ...
  }
}
</code></pre>
<p>This sends the JWT in the <code>Authorization</code> header using the <code>Bearer</code> scheme.</p>
<ul>
<li><p>Only valid tokens will allow access to this protected route.</p>
</li>
<li><p><code>iat</code> and <code>exp</code> indicate issued-at and expiry time of the token.</p>
</li>
</ul>
<p><strong>Note:</strong> Always include <code>Authorization: Bearer &lt;token&gt;</code> for protected routes.</p>
<h2 id="heading-summary">Summary</h2>
<p>This article gave you a comprehensive overview of JSON Web Tokens (JWTs) and their role in web authentication. It explained the stateless nature of HTTP, the need for tokens, and compares classic session tokens with JWTs.</p>
<p>We covered JWT structure, security mechanisms, and practical implementation using Node.js, Express, and MongoDB. We also discussed security considerations, token management, and how to test a JWT authentication API.</p>
<h3 id="heading-heres-a-summary-of-the-key-points">Here’s a Summary of the Key Points:</h3>
<ol>
<li><p><strong>What is JWT?</strong></p>
<ul>
<li><p>JWT is a JSON-based open standard for securely representing claims between two parties, defined by RFC 7519.</p>
</li>
<li><p>Widely used for authorization in modern web applications and microservice architectures.</p>
</li>
<li><p>Alternative to session tokens for maintaining user state.</p>
</li>
</ul>
</li>
<li><p><strong>Stateless Nature of HTTP</strong></p>
<ul>
<li><p>HTTP does not retain information between requests, requiring each request to carry necessary data.</p>
</li>
<li><p>Tokens (session or JWT) are used to maintain user sessions in dynamic web applications.</p>
</li>
</ul>
</li>
<li><p><strong>Session Tokens</strong></p>
<ul>
<li><p>Classic approach where the server creates and stores a session ID, typically in cookies.</p>
</li>
<li><p>Works well for single-server setups but requires shared storage (for example, Redis) in multi-server environments.</p>
</li>
<li><p>Vulnerable if the shared cache goes down.</p>
</li>
</ul>
</li>
<li><p><strong>JWT: The Modern Solution</strong></p>
<ul>
<li><p>Server sends a signed JSON token to the client, which stores and sends it with each request.</p>
</li>
<li><p>No server-side storage required – all user info is in the token.</p>
</li>
<li><p>Signature ensures validity and integrity.</p>
</li>
</ul>
</li>
<li><p><strong>JWT Structure</strong></p>
<ul>
<li><p>Three parts: Header, Payload, Signature (separated by dots).</p>
</li>
<li><p>Header and payload are Base64 encoded JSON objects. Signature is a hash using a secret key.</p>
</li>
<li><p>Base64 encoding is for convenience, not security.</p>
</li>
</ul>
</li>
<li><p><strong>Decoding JWTs</strong></p>
<ul>
<li><p>Tools like <a target="_blank" href="https://jwt.io/">jwt.io</a> can decode JWTs to show header, payload, and signature.</p>
</li>
<li><p>Sensitive data should not be stored in JWTs, as payload is publicly readable.</p>
</li>
</ul>
</li>
<li><p><strong>JWT Security</strong></p>
<ul>
<li><p>Signature is created using a secret key and cryptographic algorithm.</p>
</li>
<li><p>Server verifies token integrity using the secret key.</p>
</li>
<li><p>JWTs identify users but do not act as passwords.</p>
</li>
</ul>
</li>
<li><p><strong>Security Considerations &amp; Token Management</strong></p>
<ul>
<li><p>If a JWT is compromised, the attacker can impersonate the user until the token expires.</p>
</li>
<li><p>JWTs cannot be directly revoked; blacklisting is used to invalidate compromised tokens.</p>
</li>
<li><p>Session tokens can be invalidated by the server.</p>
</li>
</ul>
</li>
<li><p><strong>JWTs in Different Languages</strong></p>
<ul>
<li><p>JWTs are language-agnostic and can be implemented in Node.js, PHP, and other languages.</p>
</li>
<li><p>Useful for authentication and representing any kind of identity.</p>
</li>
</ul>
</li>
<li><p><strong>Practical Implementation: JWT Authentication with Express + MongoDB</strong></p>
<ul>
<li><p>Step-by-step guide to building a JWT authentication system:</p>
<ul>
<li><p>Project setup and dependencies</p>
</li>
<li><p>Folder structure</p>
</li>
<li><p>Express server initialization</p>
</li>
<li><p>MongoDB connection</p>
</li>
<li><p>User model creation</p>
</li>
<li><p>Password hashing and JWT services</p>
</li>
<li><p>Auth controller and middleware</p>
</li>
<li><p>Auth routes</p>
</li>
<li><p>Main server file</p>
</li>
<li><p>API testing instructions</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>Testing the API</strong></p>
<ul>
<li><p>Instructions for registering users, logging in, and accessing protected routes using tools like Postman.</p>
</li>
<li><p>Example requests and responses provided.</p>
</li>
</ul>
</li>
<li><p><strong>Summary &amp; Final Words</strong></p>
<ul>
<li><p>JWTs are secure, stateless, and widely used for authorization.</p>
</li>
<li><p>Security depends on safe token storage and proper management.</p>
</li>
</ul>
</li>
</ol>
<h2 id="heading-final-words">Final Words</h2>
<p>You can find all the source code from this tutorial in <a target="_blank" href="https://github.com/logicbaselabs/jwt-auth-demo">this GitHub repository</a>. If it helped you in any way, consider giving it a star to show your support!</p>
<p>Also, if you found the information here valuable, feel free to share it with others who might benefit from it. I’d really appreciate your thoughts – mention me on X <a target="_blank" href="https://x.com/sumit_analyzen">@sumit_analyzen</a> or on Facebook <a target="_blank" href="https://facebook.com/sumit.analyzen">@sumit.analyzen</a>, <a target="_blank" href="https://youtube.com/@logicBaseLabs">watch my coding tutorials</a>, or simply <a target="_blank" href="https://www.linkedin.com/in/sumitanalyzen/">connect with me</a> on LinkedIn.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Integrate Facial Recognition Authentication in a Social App with Face API ]]>
                </title>
                <description>
                    <![CDATA[ Social applications have evolved over the years, and there is a major need for secure methods to authenticate users' identities. Integrating multifactor authentication capabilities into applications is crucial for strengthening their integrity. In so... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/integrate-facial-recognition-authentication-in-a-social-application/</link>
                <guid isPermaLink="false">68d20d7a6bd072175081e6b2</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ authentication ]]>
                    </category>
                
                    <category>
                        <![CDATA[ facial recognition ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Oluwatobi ]]>
                </dc:creator>
                <pubDate>Tue, 23 Sep 2025 03:01:14 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1758208687476/3ca6b95d-55c8-4bb6-a4aa-580409e1608f.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Social applications have evolved over the years, and there is a major need for secure methods to authenticate users' identities.</p>
<p>Integrating multifactor authentication capabilities into applications is crucial for strengthening their integrity. In social apps, authentication mechanisms eliminate unwanted access to personal information between two parties. Facial authentication is not entirely new, as most devices have it built-in as security measure. It offers stronger protection compared to many traditional methods, especially against risks like phishing, brute-force attacks, and account hacking.</p>
<h2 id="heading-outline">Outline</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-to-expect">What to expect</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-a-brief-intro-to-the-face-api-tool">A Brief Intro to the Face API tool</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-demo-project-integrating-facial-recognition-and-authentication">Demo Project: Integrating Facial Recognition and Authentication</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-additional-information-and-tips">Additional Information and Tips</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-to-expect">What to Expect</h2>
<p>In this article, I’ll walk you through creating a multi-factor authentication system for a chat application powered by <a target="_blank" href="https://getstream.io">Stream.io</a>, and ensuring efficient user face ID authentication to allow only authorized access to your app. I will illustrate all these with relevant code examples.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Here are the necessary prerequisites to follow along with this tutorial:</p>
<ul>
<li><p>Intermediate knowledge of Node.js/Express for the backend aspect</p>
</li>
<li><p>Knowledge of React for the frontend aspect</p>
</li>
<li><p><a target="_blank" href="https://getstream.io">Stream.io</a> API key</p>
</li>
</ul>
<p>Before we get started, we’ll briefly highlight the facial authentication tool of choice: <a target="_blank" href="https://justadudewhohacks.github.io/face-api.js/docs/index.html">Face-Api.js</a>.</p>
<h2 id="heading-a-brief-intro-to-the-face-api-tool">A Brief Intro to the Face API tool</h2>
<p>Face-Api.js is a facial recognition package designed for integration with JavaScript-powered applications. It was built on top of the Tensor flow library and provides extensive facial detection based on machine learning models and abstract calculations.</p>
<p>In addition to all these features, it's friendly to use and can also be used locally with its predefined models. Here is a link to its <a target="_blank" href="https://justadudewhohacks.github.io/face-api.js/docs/index.html">documentation page</a>, which provides relevant code examples.</p>
<p>It provides features such as face detection, face capture, and face match, which use the <a target="_blank" href="https://en.wikipedia.org/wiki/Euclidean_algorithm">Euclidean algorithm</a> to make precise distinctions. We'll now set it up alongside our chat application project in the next section.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>As mentioned earlier, this is a full-stack project containing both the frontend and the backend aspects. In this section, we’ll set up both code bases before proceeding to the demo project section.</p>
<h3 id="heading-frontend">Frontend</h3>
<p>We will power the application using the Vite framework for the frontend.</p>
<pre><code class="lang-javascript">npm create vite@latest
</code></pre>
<p>After creating the React application, install face-api.js with this command:</p>
<pre><code class="lang-javascript">npm i face-api.js
</code></pre>
<p>This will install the <code>face</code> package and the required dependencies. You can then install Stream’s powered chat SDK, which will form the main crux of the project.</p>
<pre><code class="lang-javascript">npm i stream-chat stream-chat-react
</code></pre>
<p>After successful completion, we are finally done with the project structure scaffold. To aid ease of local testing of our frontend application, we will have to host the face models needed by the Face package locally. Here is a <a target="_blank" href="https://github.com/justadudewhohacks/face-api.js-models">link</a> to the models. Kindly copy the model's folder and paste it into the public folder in the code project. Next, we’ll set up our backend project.</p>
<h3 id="heading-backend">Backend</h3>
<p>The backend is built to store user details and ensure user authentication before accessing the chat application. MongoDB will be the database of choice, and we will use the Express.js library as the backend API development environment of choice. For the ease of setup, kindly clone this <a target="_blank" href="https://github.com/oluwatobi2001/stream-backend.git">code-base</a> and install it on the local PC. It comes preloaded with the necessary installation files. To further enjoy a seamless backend experience, you can utilize the MongoDB <a target="_blank" href="https://www.mongodb.com/products/platform/atlas-database">Atlas</a> option as the database for storing user details. With that, we will now begin the code project in the next section.</p>
<h2 id="heading-demo-project-integrating-facial-recognition-and-authentication">Demo Project: Integrating Facial Recognition and Authentication</h2>
<p>In this section, we will walk through setting up an authentication page on the frontend where a user can register their details, username, email, and password on the registration page. They are also obliged to take a snapshot of their face, and the face API will be called to detect a face in the image. They won't be allowed to proceed beyond this until it is successful.</p>
<p>Thereafter, the image <code>faceDescriptor</code> function is called, which generates a unique face description vector value of the user’s face based on the machine learning models provided. These values are securely stored in the MongoDB database via the Express.js backend after successfully registering. The application is coupled to a multifactor authentication system, which has both the password based authentication and the facial authentication mechanisms.</p>
<p>When the first hurdle (password authentication) is completed, the user is then required to take a face match, comparing it with the user's face descriptor stored from the registration page. The comparison is achieved using the Euclidean algorithmic comparison based on the threshold we provide. If it meets the threshold, the face is said to be matched, and the user gets access to the chat application; else, the user is denied access to the Stream.io-powered chat application. Relevant source code snippets highlighting these steps will be provided concurrently with images.</p>
<p>We’ll begin by building a defunct registration page for our chat application using React, of course. We will begin by importing and initializing the necessary packages.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> React, {useState, useRef, useEffect} <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> faceapi <span class="hljs-keyword">from</span> <span class="hljs-string">'face-api.js'</span>
<span class="hljs-keyword">import</span> {useNavigate} <span class="hljs-keyword">from</span> <span class="hljs-string">'react-router-dom'</span>
<span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">'axios'</span>;

<span class="hljs-keyword">const</span> Register =<span class="hljs-function">()=&gt;</span> {

    <span class="hljs-keyword">const</span> navigate= useNavigate(<span class="hljs-string">"/"</span>)
    <span class="hljs-keyword">const</span> userRef = useRef();
    <span class="hljs-keyword">const</span> passwordRef= useRef();
    <span class="hljs-keyword">const</span> emailRef = useRef();
    <span class="hljs-keyword">const</span> FullRef = useRef()
</code></pre>
<p>In the code snippet above, we imported useful React hooks and initialized our installed <code>Face-api.js</code> tool. <a target="_blank" href="https://www.npmjs.com/package/axios">Axios</a> will serve as our API request tool of choice for this project. The <code>useRef</code> hook will be used to track the user inputs. We then defined the register function and initialized the various <code>useRef</code> hooks for the various input fields to be inputted.</p>
<pre><code class="lang-javascript">

    useEffect(<span class="hljs-function">()=&gt;</span> {
<span class="hljs-keyword">const</span> loadModels =<span class="hljs-keyword">async</span>() =&gt; {
<span class="hljs-keyword">await</span> faceapi.nets.tinyFaceDetector.loadFromUri(<span class="hljs-string">'/models'</span>);
<span class="hljs-keyword">await</span> faceapi.nets.faceLandmark68Net.loadFromUri(<span class="hljs-string">'/models'</span>);
<span class="hljs-keyword">await</span> faceapi.nets.faceRecognitionNet.loadFromUri(<span class="hljs-string">'/models'</span>);
<span class="hljs-keyword">await</span> faceapi.nets.faceExpressionNet.loadFromUri(<span class="hljs-string">'/models'</span>);
<span class="hljs-keyword">await</span> faceapi.nets.tinyFaceDetector.loadFromUri(<span class="hljs-string">'/models'</span>);
setModelIsLoaded(<span class="hljs-literal">true</span>);
                startVideo();
}
  loadModels()  }, [])
</code></pre>
<p>In the code above, the <code>useEffect</code> hook is called to ensure that the various locally stored <code>face-api</code> models are initialized and active in our application. The models are stored in the <code>models</code> sub-folder within the <code>public</code> folder. Going forward, after initializing our models, we will now set up our camcorder feature on our webpage.</p>
<pre><code class="lang-javascript">  <span class="hljs-keyword">const</span> [faceDetected, setFaceDetected] = useState(<span class="hljs-literal">false</span>);


        <span class="hljs-comment">// Start video feed</span>
        <span class="hljs-keyword">const</span> startVideo = <span class="hljs-function">() =&gt;</span> {
            navigator.mediaDevices
                .getUserMedia({ <span class="hljs-attr">video</span>: <span class="hljs-literal">true</span> })
                .then(<span class="hljs-function">(<span class="hljs-params">stream</span>) =&gt;</span> {
                    videoRef.current.srcObject = stream;
                })
                .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error accessing webcam: "</span>, err));
        };
        <span class="hljs-keyword">const</span> captureSnapshot = <span class="hljs-keyword">async</span> () =&gt; {
            <span class="hljs-keyword">const</span> canvas = snapshotRef.current;
            <span class="hljs-keyword">const</span> context = canvas.getContext(<span class="hljs-string">'2d'</span>);
            context.drawImage(videoRef.current, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, canvas.width, canvas.height);
            <span class="hljs-keyword">const</span> dataUrl = canvas.toDataURL(<span class="hljs-string">'image/jpeg'</span>);
            setSnapshot(dataUrl);

            <span class="hljs-comment">// Generate the face descriptor (128-dimensional vector)</span>
            <span class="hljs-keyword">const</span> detection = <span class="hljs-keyword">await</span> faceapi
                .detectSingleFace(canvas, <span class="hljs-keyword">new</span> faceapi.TinyFaceDetectorOptions())
                .withFaceLandmarks()
                .withFaceDescriptor();

            <span class="hljs-keyword">if</span> (detection) {
                <span class="hljs-keyword">const</span> newDescriptor = detection.descriptor;
                setDescriptionValue(newDescriptor)
                <span class="hljs-built_in">console</span>.log( newDescriptor);
               setSubmitDisabled(<span class="hljs-literal">false</span>)
                stopVid()
            } <span class="hljs-keyword">else</span> {
                <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"No face detected in snapshot"</span>);
            }
        };
    <span class="hljs-keyword">const</span> stopVid =<span class="hljs-function">() =&gt;</span> {

        navigator.mediaDevices
                .getUserMedia({ <span class="hljs-attr">video</span>: <span class="hljs-literal">false</span> })
                <span class="hljs-keyword">const</span> stream = videoRef?.current?.srcObject;
        <span class="hljs-keyword">if</span> (stream) {
            stream.getTracks().forEach(<span class="hljs-function"><span class="hljs-params">track</span> =&gt;</span> {track.stop()})
            videoRef.current.srcObject = <span class="hljs-literal">null</span>;
            setCameraActive(<span class="hljs-literal">false</span>)
        }
    }
        <span class="hljs-comment">// Detect face in the video stream</span>
        <span class="hljs-keyword">const</span> handleVideoPlay = <span class="hljs-keyword">async</span> () =&gt; {
            <span class="hljs-keyword">const</span> video = videoRef.current;
            <span class="hljs-keyword">const</span> canvas = canvasRef.current;

            <span class="hljs-keyword">const</span> displaySize = { <span class="hljs-attr">width</span>: video.width, <span class="hljs-attr">height</span>: video.height };
            faceapi.matchDimensions(canvas, displaySize);

            <span class="hljs-built_in">setInterval</span>(<span class="hljs-keyword">async</span> () =&gt; {
                <span class="hljs-keyword">if</span> (!cameraActive) <span class="hljs-keyword">return</span> ;
                <span class="hljs-keyword">const</span> detections = <span class="hljs-keyword">await</span> faceapi.detectAllFaces(
                    video,
                    <span class="hljs-keyword">new</span> faceapi.TinyFaceDetectorOptions()
                );

                <span class="hljs-keyword">const</span> resizedDetections = faceapi.resizeResults(detections, displaySize);

                canvas.getContext(<span class="hljs-string">'2d'</span>).clearRect(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, canvas.width, canvas.height);
                faceapi.draw.drawDetections(canvas, resizedDetections);
                    <span class="hljs-keyword">const</span> detected = detections.length &gt; <span class="hljs-number">0</span>;
                 <span class="hljs-keyword">if</span> (detected &amp;&amp; !faceDetected) {
                captureSnapshot();  <span class="hljs-comment">// Capture the snapshot as soon as a face is detected</span>
            }

                setFaceDetected(detections.length &gt; <span class="hljs-number">0</span>);
            }, <span class="hljs-number">100</span>);
        };
</code></pre>
<p>In the code above, we begin by defining a <code>useState</code> array when the user’s face is detected during the sign-up process. Thereafter, the function to trigger the browser camcorder is then activated. With this on, we can then trigger the <code>handlePlayFunction</code> in the code. This function monitors facial detection as highlighted by the face models already initialized. The <code>stopVid</code> function is also triggered when the user’s facial detection has been successfully completed.</p>
<p>In this section, we also activated the browser camcorder tool in our application to provide us with real time video. The <code>CaptureSnapshot</code> function helps to obtain a snapshot from the current video being showcased.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> RegSubmit = <span class="hljs-keyword">async</span> (e) =&gt; {
  e.preventDefault();
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"hello"</span>);

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> axios.post(BACKEND_URL, {
      <span class="hljs-attr">username</span>: userRef.current.value,
      <span class="hljs-attr">email</span>: emailRef.current.value,
      <span class="hljs-attr">FullName</span>: FullRef.current.value,
      <span class="hljs-attr">password</span>: passwordRef.current.value,
      <span class="hljs-attr">faceDescriptor</span>: descriptionValue,
    });

    <span class="hljs-built_in">console</span>.log(res.data);
    setError(<span class="hljs-literal">false</span>);
    navigate(<span class="hljs-string">"/login"</span>);
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"help"</span>);
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-built_in">console</span>.error(err);
    setError(<span class="hljs-literal">true</span>);
  }
};
</code></pre>
<p>With all the values obtained, the <code>regSubmit</code> function is then defined. When executed, it stores the provided user details with the face description object on our backend server which can then be accessed in the next section for authentication.</p>
<p>Below is the full registration code.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> React, { useState, useRef, useEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> faceapi <span class="hljs-keyword">from</span> <span class="hljs-string">'face-api.js'</span>;
<span class="hljs-keyword">import</span> { useNavigate } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-router-dom'</span>;
<span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">'axios'</span>;

<span class="hljs-keyword">const</span> Register = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> navigate = useNavigate(<span class="hljs-string">"/"</span>);

  <span class="hljs-keyword">const</span> userRef = useRef();
  <span class="hljs-keyword">const</span> passwordRef = useRef();
  <span class="hljs-keyword">const</span> emailRef = useRef();
  <span class="hljs-keyword">const</span> FullRef = useRef();
  <span class="hljs-keyword">const</span> snapshotRef = useRef(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> videoRef = useRef(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> canvasRef = useRef(<span class="hljs-literal">null</span>);

  <span class="hljs-keyword">const</span> [modelIsLoaded, setModelIsLoaded] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> [detections, setDetections] = useState([]);
  <span class="hljs-keyword">const</span> [error, setError] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> [snapshot, setSnapshot] = useState(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> [cameraActive, setCameraActive] = useState(<span class="hljs-literal">true</span>);
  <span class="hljs-keyword">const</span> [submitDisabled, setSubmitDisabled] = useState(<span class="hljs-literal">true</span>);
  <span class="hljs-keyword">const</span> [descriptionValue, setDescriptionValue] = useState(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> [faceDetected, setFaceDetected] = useState(<span class="hljs-literal">false</span>);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> loadModels = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">await</span> faceapi.nets.tinyFaceDetector.loadFromUri(<span class="hljs-string">'/models'</span>);
      <span class="hljs-keyword">await</span> faceapi.nets.faceLandmark68Net.loadFromUri(<span class="hljs-string">'/models'</span>);
      <span class="hljs-keyword">await</span> faceapi.nets.faceRecognitionNet.loadFromUri(<span class="hljs-string">'/models'</span>);
      <span class="hljs-keyword">await</span> faceapi.nets.faceExpressionNet.loadFromUri(<span class="hljs-string">'/models'</span>);
      <span class="hljs-keyword">await</span> faceapi.nets.tinyFaceDetector.loadFromUri(<span class="hljs-string">'/models'</span>);
      setModelIsLoaded(<span class="hljs-literal">true</span>);
      startVideo();
    };

    loadModels();
  }, []);

  <span class="hljs-keyword">const</span> RegSubmit = <span class="hljs-keyword">async</span> (e) =&gt; {
    e.preventDefault();
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"hello"</span>);

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> axios.post(<span class="hljs-string">'http://localhost:5000/v1/users'</span>, {
        <span class="hljs-attr">username</span>: userRef.current.value,
        <span class="hljs-attr">email</span>: emailRef.current.value,
        <span class="hljs-attr">FullName</span>: FullRef.current.value,
        <span class="hljs-attr">password</span>: passwordRef.current.value,
        <span class="hljs-attr">faceDescriptor</span>: descriptionValue
      });

      <span class="hljs-built_in">console</span>.log(res.data);
      setError(<span class="hljs-literal">false</span>);
      navigate(<span class="hljs-string">"/login"</span>);
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"help"</span>);
    } <span class="hljs-keyword">catch</span> (err) {
      <span class="hljs-built_in">console</span>.log(err);
      setError(<span class="hljs-literal">true</span>);
    }
  };

  <span class="hljs-keyword">const</span> startVideo = <span class="hljs-function">() =&gt;</span> {
    navigator.mediaDevices
      .getUserMedia({ <span class="hljs-attr">video</span>: <span class="hljs-literal">true</span> })
      .then(<span class="hljs-function">(<span class="hljs-params">stream</span>) =&gt;</span> {
        videoRef.current.srcObject = stream;
      })
      .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error accessing webcam: "</span>, err));
  };

  <span class="hljs-keyword">const</span> stopVid = <span class="hljs-function">() =&gt;</span> {
    navigator.mediaDevices.getUserMedia({ <span class="hljs-attr">video</span>: <span class="hljs-literal">false</span> });
    <span class="hljs-keyword">const</span> stream = videoRef?.current?.srcObject;
    <span class="hljs-keyword">if</span> (stream) {
      stream.getTracks().forEach(<span class="hljs-function">(<span class="hljs-params">track</span>) =&gt;</span> track.stop());
      videoRef.current.srcObject = <span class="hljs-literal">null</span>;
      setCameraActive(<span class="hljs-literal">false</span>);
    }
  };

  <span class="hljs-keyword">const</span> captureSnapshot = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> canvas = snapshotRef.current;
    <span class="hljs-keyword">const</span> context = canvas.getContext(<span class="hljs-string">'2d'</span>);
    context.drawImage(videoRef.current, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, canvas.width, canvas.height);
    <span class="hljs-keyword">const</span> dataUrl = canvas.toDataURL(<span class="hljs-string">'image/jpeg'</span>);
    setSnapshot(dataUrl);

    <span class="hljs-keyword">const</span> detection = <span class="hljs-keyword">await</span> faceapi
      .detectSingleFace(canvas, <span class="hljs-keyword">new</span> faceapi.TinyFaceDetectorOptions())
      .withFaceLandmarks()
      .withFaceDescriptor();

    <span class="hljs-keyword">if</span> (detection) {
      <span class="hljs-keyword">const</span> newDescriptor = detection.descriptor;
      setDescriptionValue(newDescriptor);
      <span class="hljs-built_in">console</span>.log(newDescriptor);
      setSubmitDisabled(<span class="hljs-literal">false</span>);
      stopVid();

      <span class="hljs-keyword">if</span> (storedDescriptor &amp;&amp; isMatchingFace(storedDescriptor, newDescriptor)) {
        <span class="hljs-built_in">setInterval</span>(alert(<span class="hljs-string">"face matched"</span>), <span class="hljs-number">100</span>);
      } <span class="hljs-keyword">else</span> {
        alert(<span class="hljs-string">"No Match Found!"</span>);
      }
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"No face detected in snapshot"</span>);
    }
  };

  <span class="hljs-keyword">const</span> handleVideoPlay = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> video = videoRef.current;
    <span class="hljs-keyword">const</span> canvas = canvasRef.current;
    <span class="hljs-keyword">const</span> displaySize = { <span class="hljs-attr">width</span>: video.width, <span class="hljs-attr">height</span>: video.height };
    faceapi.matchDimensions(canvas, displaySize);

    <span class="hljs-built_in">setInterval</span>(<span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">if</span> (!cameraActive) <span class="hljs-keyword">return</span>;

      <span class="hljs-keyword">const</span> detections = <span class="hljs-keyword">await</span> faceapi.detectAllFaces(
        video,
        <span class="hljs-keyword">new</span> faceapi.TinyFaceDetectorOptions()
      );

      <span class="hljs-keyword">const</span> resizedDetections = faceapi.resizeResults(detections, displaySize);
      canvas.getContext(<span class="hljs-string">'2d'</span>).clearRect(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, canvas.width, canvas.height);
      faceapi.draw.drawDetections(canvas, resizedDetections);

      <span class="hljs-keyword">const</span> detected = detections.length &gt; <span class="hljs-number">0</span>;
      <span class="hljs-keyword">if</span> (detected &amp;&amp; !faceDetected) {
        captureSnapshot();
      }

      setFaceDetected(detected);
    }, <span class="hljs-number">100</span>);
  };

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-col w-full h-screen justify-center"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-col"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">form</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-col mb-2 w-full"</span> <span class="hljs-attr">onSubmit</span>=<span class="hljs-string">{RegSubmit}</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">h3</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-col mx-auto mb-5"</span>&gt;</span>Registration Page<span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>

          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-col mb-2 w-[50%] mx-auto items-center"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
              <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span>
              <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"Email"</span>
              <span class="hljs-attr">className</span>=<span class="hljs-string">"w-full rounded-2xl h-[50px] border-2 p-2 mb-2 border-gray-900"</span>
              <span class="hljs-attr">required</span>
              <span class="hljs-attr">ref</span>=<span class="hljs-string">{emailRef}</span>
            /&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
              <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span>
              <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"Username"</span>
              <span class="hljs-attr">className</span>=<span class="hljs-string">"w-full rounded-2xl h-[50px] border-2 p-2 mb-2 border-gray-900"</span>
              <span class="hljs-attr">required</span>
              <span class="hljs-attr">ref</span>=<span class="hljs-string">{userRef}</span>
            /&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
              <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span>
              <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"Full Name"</span>
              <span class="hljs-attr">className</span>=<span class="hljs-string">"w-full rounded-2xl h-[50px] border-2 p-2 mb-2 border-gray-900"</span>
              <span class="hljs-attr">required</span>
              <span class="hljs-attr">ref</span>=<span class="hljs-string">{FullRef}</span>
            /&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
              <span class="hljs-attr">type</span>=<span class="hljs-string">"password"</span>
              <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"Password"</span>
              <span class="hljs-attr">className</span>=<span class="hljs-string">"w-full rounded-2xl h-[50px] border-2 p-2 mb-2 border-gray-900"</span>
              <span class="hljs-attr">required</span>
              <span class="hljs-attr">ref</span>=<span class="hljs-string">{passwordRef}</span>
            /&gt;</span>

            <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
              {!modelIsLoaded &amp;&amp; cameraActive &amp;&amp; !descriptionValue ? (
                <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Loading<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
              ) : (
                <span class="hljs-tag">&lt;&gt;</span>
                  {!descriptionValue &amp;&amp; (
                    <span class="hljs-tag">&lt;&gt;</span>
                      <span class="hljs-tag">&lt;<span class="hljs-name">video</span>
                        <span class="hljs-attr">ref</span>=<span class="hljs-string">{videoRef}</span>
                        <span class="hljs-attr">width</span>=<span class="hljs-string">"200"</span>
                        <span class="hljs-attr">height</span>=<span class="hljs-string">"160"</span>
                        <span class="hljs-attr">onPlay</span>=<span class="hljs-string">{handleVideoPlay}</span>
                        <span class="hljs-attr">autoPlay</span>
                        <span class="hljs-attr">muted</span>
                      /&gt;</span>
                      <span class="hljs-tag">&lt;<span class="hljs-name">canvas</span>
                        <span class="hljs-attr">ref</span>=<span class="hljs-string">{canvasRef}</span>
                        <span class="hljs-attr">width</span>=<span class="hljs-string">"200"</span>
                        <span class="hljs-attr">height</span>=<span class="hljs-string">"160"</span>
                        <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">position:</span> '<span class="hljs-attr">absolute</span>', <span class="hljs-attr">top:</span> <span class="hljs-attr">0</span>, <span class="hljs-attr">left:</span> <span class="hljs-attr">0</span> }}
                      /&gt;</span>
                      <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>
                        {faceDetected ? (
                          <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">color:</span> '<span class="hljs-attr">green</span>' }}&gt;</span>Face Detected<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
                        ) : (
                          <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">color:</span> '<span class="hljs-attr">red</span>' }}&gt;</span>No Face Detected<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
                        )}
                      <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
                      <span class="hljs-tag">&lt;<span class="hljs-name">canvas</span>
                        <span class="hljs-attr">ref</span>=<span class="hljs-string">{snapshotRef}</span>
                        <span class="hljs-attr">width</span>=<span class="hljs-string">"480"</span>
                        <span class="hljs-attr">height</span>=<span class="hljs-string">"360"</span>
                        <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">display:</span> '<span class="hljs-attr">none</span>' }}
                      /&gt;</span>
                    <span class="hljs-tag">&lt;/&gt;</span>
                  )}
                <span class="hljs-tag">&lt;/&gt;</span>
              )}

              {snapshot &amp;&amp; (
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">marginTop:</span> '<span class="hljs-attr">20px</span>' }}&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">h4</span>&gt;</span>Face Snapshot:<span class="hljs-tag">&lt;/<span class="hljs-name">h4</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">img</span>
                    <span class="hljs-attr">src</span>=<span class="hljs-string">{snapshot}</span>
                    <span class="hljs-attr">alt</span>=<span class="hljs-string">"Face Snapshot"</span>
                    <span class="hljs-attr">width</span>=<span class="hljs-string">"200"</span>
                    <span class="hljs-attr">height</span>=<span class="hljs-string">"160"</span>
                  /&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
              )}
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"mt-2"</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"button"</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{stopVid}</span>&gt;</span>
                Stop Video
              <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

            <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
              <span class="hljs-attr">disabled</span>=<span class="hljs-string">{submitDisabled}</span>
              <span class="hljs-attr">className</span>=<span class="hljs-string">"mx-auto mt-4 rounded-2xl cursor-pointer text-white bg-primary w-[80%] lg:w-[50%] h-[40px] text-center items-center justify-center"</span>
              <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span>
            &gt;</span>
              Register
            <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-col mt-1 w-full"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex justify-center"</span>&gt;</span>
              Registered previously?<span class="hljs-symbol">&amp;nbsp;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/login"</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-blue-600 underline"</span>&gt;</span>
                Login
              <span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

          {error &amp;&amp; (
            <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-red-600 text-center mt-2"</span>&gt;</span>
              Error while registering, try again
            <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
          )}
        <span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span></span>
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> Register;
</code></pre>
<p>Going forward, we will be working on our multifactor authentication system. In the code below, we will be highlighting the <code>loginSubmit</code> function which will be triggered when the user email and password credentials are provided for logging in to our chat application. The <code>useRef</code> hook is initialized which ensures that the values passed in the input boxes are parsed to the backend via the <code>Axios</code> request tool.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> React, { useState, useRef, useEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { Link, useNavigate } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-router-dom'</span>;
<span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">'axios'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Login</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> navigate = useNavigate();
  <span class="hljs-keyword">const</span> userRef = useRef();
  <span class="hljs-keyword">const</span> passwordRef = useRef();

  <span class="hljs-keyword">const</span> [error, setError] = useState(<span class="hljs-literal">false</span>);

  <span class="hljs-keyword">const</span> LoginSubmit = <span class="hljs-keyword">async</span> (e) =&gt; {
    e.preventDefault();
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> axios.post(
        <span class="hljs-string">'http://localhost:5000/v1/auth/login'</span>,
        {
          <span class="hljs-attr">email</span>: userRef.current.value,
          <span class="hljs-attr">password</span>: passwordRef.current.value,
        },
        { <span class="hljs-attr">withCredentials</span>: <span class="hljs-literal">true</span> }
      );

      <span class="hljs-built_in">console</span>.log(res?.data);
      setError(<span class="hljs-literal">false</span>);
      navigate(<span class="hljs-string">'/confirm-auth'</span>);
      <span class="hljs-built_in">console</span>.log(res);
    } <span class="hljs-keyword">catch</span> (err) {
      setError(<span class="hljs-literal">true</span>);
      <span class="hljs-built_in">console</span>.log(err);
    }
  };
}
</code></pre>
<p>The full login page code example will be provided <a target="_blank" href="http://github.com/oluwatobi2001/Stream-frontend.git">here</a>. After successfully confirming their identity via the use of the password authentication feature, we can then go on to confirm the user’s identity via the use of the face recognition system.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">'axios'</span>;
<span class="hljs-keyword">import</span> React, { useRef, useEffect, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> faceapi <span class="hljs-keyword">from</span> <span class="hljs-string">'face-api.js'</span>;
<span class="hljs-keyword">import</span> { useNavigate } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-router-dom'</span>;
</code></pre>
<p>First of all, we will set up the app by importing the necessary dependencies as highlighted in the code snippet above.</p>
<pre><code class="lang-javascript">
  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> loadModels = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">await</span> faceapi.nets.tinyFaceDetector.loadFromUri(<span class="hljs-string">'/models'</span>);
      <span class="hljs-keyword">await</span> faceapi.nets.faceLandmark68Net.loadFromUri(<span class="hljs-string">'/models'</span>);
      <span class="hljs-keyword">await</span> faceapi.nets.faceRecognitionNet.loadFromUri(<span class="hljs-string">'/models'</span>);
      <span class="hljs-keyword">await</span> faceapi.nets.faceExpressionNet.loadFromUri(<span class="hljs-string">'/models'</span>);
    };

    loadModels();
  }, []);

  <span class="hljs-keyword">const</span> handleVideoPlay = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> video = videoRef.current;
    <span class="hljs-keyword">const</span> canvas = canvasRef.current;

    <span class="hljs-keyword">const</span> displaySize = { <span class="hljs-attr">width</span>: video.width, <span class="hljs-attr">height</span>: video.height };
    faceapi.matchDimensions(canvas, displaySize);

    <span class="hljs-built_in">setInterval</span>(<span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">if</span> (!cameraActive) <span class="hljs-keyword">return</span>;

      <span class="hljs-keyword">const</span> detections = <span class="hljs-keyword">await</span> faceapi.detectAllFaces(
        video,
        <span class="hljs-keyword">new</span> faceapi.TinyFaceDetectorOptions()
      );

      <span class="hljs-keyword">const</span> resizedDetections = faceapi.resizeResults(detections, displaySize);
      canvas.getContext(<span class="hljs-string">'2d'</span>).clearRect(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, canvas.width, canvas.height);
      faceapi.draw.drawDetections(canvas, resizedDetections);

      <span class="hljs-keyword">const</span> detected = detections.length &gt; <span class="hljs-number">0</span>;
      <span class="hljs-keyword">if</span> (detected &amp;&amp; !faceDetected) {
        captureSnapshot();
      }

      setFaceDetected(detected);
    }, <span class="hljs-number">100</span>);
  };

  <span class="hljs-keyword">const</span> startVideo = <span class="hljs-function">() =&gt;</span> {
    navigator.mediaDevices
      .getUserMedia({ <span class="hljs-attr">video</span>: <span class="hljs-literal">true</span> })
      .then(<span class="hljs-function">(<span class="hljs-params">stream</span>) =&gt;</span> {
        videoRef.current.srcObject = stream;
      })
      .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error accessing webcam: "</span>, err));
  };

  <span class="hljs-keyword">const</span> stopVid = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> stream = videoRef.current.srcObject;
    <span class="hljs-keyword">if</span> (stream) {
      stream.getTracks().forEach(<span class="hljs-function">(<span class="hljs-params">track</span>) =&gt;</span> track.stop());
      videoRef.current.srcObject = <span class="hljs-literal">null</span>;
      setCameraActive(<span class="hljs-literal">false</span>);
    }
  };

  <span class="hljs-keyword">const</span> deleteImage = <span class="hljs-function">() =&gt;</span> {
    setSnapshot(<span class="hljs-literal">null</span>);
    setDescriptionValue(<span class="hljs-literal">null</span>);
    setFaceDetected(<span class="hljs-literal">false</span>);
    setCameraActive(<span class="hljs-literal">true</span>);
    startVideo();
  };

  <span class="hljs-keyword">const</span> captureSnapshot = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> canvas = snapshotRef.current;
    <span class="hljs-keyword">const</span> context = canvas.getContext(<span class="hljs-string">'2d'</span>);
    context.drawImage(videoRef.current, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, canvas.width, canvas.height);

    <span class="hljs-keyword">const</span> dataUrl = canvas.toDataURL(<span class="hljs-string">'image/jpeg'</span>);
    setSnapshot(dataUrl);
    stopVid();

    <span class="hljs-keyword">const</span> detection = <span class="hljs-keyword">await</span> faceapi
      .detectSingleFace(canvas, <span class="hljs-keyword">new</span> faceapi.TinyFaceDetectorOptions())
      .withFaceLandmarks()
      .withFaceDescriptor();

    <span class="hljs-keyword">if</span> (detection) {
      <span class="hljs-keyword">const</span> newDescriptor = detection.descriptor;
      setDescriptionValue(newDescriptor);
      <span class="hljs-built_in">console</span>.log(newDescriptor);
    }
  };
</code></pre>
<p>After initializing all the necessary dependencies, we also imported our models as we did in the registration page to detect the user’s face and then generate a face description. We also allowed for the user to delete the snapshot and retake the image as many times as possible.</p>
<pre><code class="lang-javascript">  <span class="hljs-keyword">const</span> FaceAuthenticate = <span class="hljs-keyword">async</span> (e) =&gt; {
    e.preventDefault();

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> axios.post(
        <span class="hljs-string">'http://localhost:5000/v1/auth/face-auth'</span>,
        { <span class="hljs-attr">faceDescriptor</span>: descriptionValue },
        { <span class="hljs-attr">withCredentials</span>: <span class="hljs-literal">true</span> }
      );

      <span class="hljs-built_in">console</span>.log(res?.data);
      navigate(<span class="hljs-string">'/chat'</span>);
    } <span class="hljs-keyword">catch</span> (err) {
      <span class="hljs-built_in">console</span>.log(err);
    }
  };
</code></pre>
<p>After the face descriptor object gets generated, we then sent it to the backend to compare it with the stored face descriptor obtained at the point of registration. If they match, we get redirected to the chat application. Otherwise, an appropriate error message denying us access to the chat application is displayed.</p>
<p>Here is the code to the <code>FaceAuth</code> page:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">'axios'</span>;
<span class="hljs-keyword">import</span> React, { useRef, useEffect, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> faceapi <span class="hljs-keyword">from</span> <span class="hljs-string">'face-api.js'</span>;
<span class="hljs-keyword">import</span> { useNavigate } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-router-dom'</span>;

<span class="hljs-keyword">const</span> FaceAuth = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> navigate = useNavigate(<span class="hljs-string">"/"</span>);

  <span class="hljs-keyword">const</span> videoRef = useRef(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> canvasRef = useRef(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> snapshotRef = useRef(<span class="hljs-literal">null</span>);

  <span class="hljs-keyword">const</span> [cameraActive, setCameraActive] = useState(<span class="hljs-literal">true</span>);
  <span class="hljs-keyword">const</span> [snapshot, setSnapshot] = useState(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> [descriptionValue, setDescriptionValue] = useState(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> [faceDetected, setFaceDetected] = useState(<span class="hljs-literal">false</span>);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> loadModels = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">await</span> faceapi.nets.tinyFaceDetector.loadFromUri(<span class="hljs-string">'/models'</span>);
      <span class="hljs-keyword">await</span> faceapi.nets.faceLandmark68Net.loadFromUri(<span class="hljs-string">'/models'</span>);
      <span class="hljs-keyword">await</span> faceapi.nets.faceRecognitionNet.loadFromUri(<span class="hljs-string">'/models'</span>);
      <span class="hljs-keyword">await</span> faceapi.nets.faceExpressionNet.loadFromUri(<span class="hljs-string">'/models'</span>);
    };

    loadModels();
  }, []);

  <span class="hljs-keyword">const</span> handleVideoPlay = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> video = videoRef.current;
    <span class="hljs-keyword">const</span> canvas = canvasRef.current;

    <span class="hljs-keyword">const</span> displaySize = { <span class="hljs-attr">width</span>: video.width, <span class="hljs-attr">height</span>: video.height };
    faceapi.matchDimensions(canvas, displaySize);

    <span class="hljs-built_in">setInterval</span>(<span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">if</span> (!cameraActive) <span class="hljs-keyword">return</span>;

      <span class="hljs-keyword">const</span> detections = <span class="hljs-keyword">await</span> faceapi.detectAllFaces(
        video,
        <span class="hljs-keyword">new</span> faceapi.TinyFaceDetectorOptions()
      );

      <span class="hljs-keyword">const</span> resizedDetections = faceapi.resizeResults(detections, displaySize);
      canvas.getContext(<span class="hljs-string">'2d'</span>).clearRect(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, canvas.width, canvas.height);
      faceapi.draw.drawDetections(canvas, resizedDetections);

      <span class="hljs-keyword">const</span> detected = detections.length &gt; <span class="hljs-number">0</span>;
      <span class="hljs-keyword">if</span> (detected &amp;&amp; !faceDetected) {
        captureSnapshot();
      }

      setFaceDetected(detected);
    }, <span class="hljs-number">100</span>);
  };

  <span class="hljs-keyword">const</span> startVideo = <span class="hljs-function">() =&gt;</span> {
    navigator.mediaDevices
      .getUserMedia({ <span class="hljs-attr">video</span>: <span class="hljs-literal">true</span> })
      .then(<span class="hljs-function">(<span class="hljs-params">stream</span>) =&gt;</span> {
        videoRef.current.srcObject = stream;
      })
      .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error accessing webcam: "</span>, err));
  };

  <span class="hljs-keyword">const</span> stopVid = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> stream = videoRef.current.srcObject;
    <span class="hljs-keyword">if</span> (stream) {
      stream.getTracks().forEach(<span class="hljs-function">(<span class="hljs-params">track</span>) =&gt;</span> track.stop());
      videoRef.current.srcObject = <span class="hljs-literal">null</span>;
      setCameraActive(<span class="hljs-literal">false</span>);
    }
  };

  <span class="hljs-keyword">const</span> deleteImage = <span class="hljs-function">() =&gt;</span> {
    setSnapshot(<span class="hljs-literal">null</span>);
    setDescriptionValue(<span class="hljs-literal">null</span>);
    setFaceDetected(<span class="hljs-literal">false</span>);
    setCameraActive(<span class="hljs-literal">true</span>);
    startVideo();
  };

  <span class="hljs-keyword">const</span> captureSnapshot = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> canvas = snapshotRef.current;
    <span class="hljs-keyword">const</span> context = canvas.getContext(<span class="hljs-string">'2d'</span>);
    context.drawImage(videoRef.current, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, canvas.width, canvas.height);

    <span class="hljs-keyword">const</span> dataUrl = canvas.toDataURL(<span class="hljs-string">'image/jpeg'</span>);
    setSnapshot(dataUrl);
    stopVid();

    <span class="hljs-keyword">const</span> detection = <span class="hljs-keyword">await</span> faceapi
      .detectSingleFace(canvas, <span class="hljs-keyword">new</span> faceapi.TinyFaceDetectorOptions())
      .withFaceLandmarks()
      .withFaceDescriptor();

    <span class="hljs-keyword">if</span> (detection) {
      <span class="hljs-keyword">const</span> newDescriptor = detection.descriptor;
      setDescriptionValue(newDescriptor);
      <span class="hljs-built_in">console</span>.log(newDescriptor);
    }
  };

  <span class="hljs-keyword">const</span> FaceAuthenticate = <span class="hljs-keyword">async</span> (e) =&gt; {
    e.preventDefault();

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> axios.post(
        <span class="hljs-string">'http://localhost:5000/v1/auth/face-auth'</span>,
        { <span class="hljs-attr">faceDescriptor</span>: descriptionValue },
        { <span class="hljs-attr">withCredentials</span>: <span class="hljs-literal">true</span> }
      );

      <span class="hljs-built_in">console</span>.log(res?.data);
      navigate(<span class="hljs-string">'/chat'</span>);
    } <span class="hljs-keyword">catch</span> (err) {
      <span class="hljs-built_in">console</span>.log(err);
    }
  };

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex w-full h-screen flex-col justify-center"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-col mx-auto items-center text-lg font-semibold mb-3"</span>&gt;</span>
          Take a snapshot to confirm your identity
        <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-center mb-4"</span>&gt;</span>Ensure that the picture is taken in a bright area<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
          <span class="hljs-attr">onClick</span>=<span class="hljs-string">{startVideo}</span>
          <span class="hljs-attr">className</span>=<span class="hljs-string">"flex w-[30%] mx-auto text-center items-center justify-center mb-5 h-[40px] bg-blue-600 rounded-md text-white"</span>
        &gt;</span>
          Turn on Webcam
        <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>

        {!snapshot ? (
          <span class="hljs-tag">&lt;&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">video</span>
              <span class="hljs-attr">className</span>=<span class="hljs-string">"flex mx-auto items-center rounded-md"</span>
              <span class="hljs-attr">ref</span>=<span class="hljs-string">{videoRef}</span>
              <span class="hljs-attr">width</span>=<span class="hljs-string">"240"</span>
              <span class="hljs-attr">height</span>=<span class="hljs-string">"180"</span>
              <span class="hljs-attr">onPlay</span>=<span class="hljs-string">{handleVideoPlay}</span>
              <span class="hljs-attr">autoPlay</span>
              <span class="hljs-attr">muted</span>
            /&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">canvas</span>
              <span class="hljs-attr">ref</span>=<span class="hljs-string">{snapshotRef}</span>
              <span class="hljs-attr">width</span>=<span class="hljs-string">"240"</span>
              <span class="hljs-attr">height</span>=<span class="hljs-string">"180"</span>
              <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">position:</span> '<span class="hljs-attr">absolute</span>', <span class="hljs-attr">top:</span> <span class="hljs-attr">0</span>, <span class="hljs-attr">left:</span> <span class="hljs-attr">0</span> }}
            /&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{captureSnapshot}</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"mt-4 mx-auto block text-sm text-blue-600 underline"</span>&gt;</span>
              Take a snapshot
            <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
          <span class="hljs-tag">&lt;/&gt;</span>
        ) : (
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex w-full justify-center"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">img</span>
              <span class="hljs-attr">src</span>=<span class="hljs-string">{snapshot}</span>
              <span class="hljs-attr">className</span>=<span class="hljs-string">"rounded-lg"</span>
              <span class="hljs-attr">width</span>=<span class="hljs-string">"240"</span>
              <span class="hljs-attr">height</span>=<span class="hljs-string">"180"</span>
              <span class="hljs-attr">alt</span>=<span class="hljs-string">"Face Snapshot"</span>
            /&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        )}

        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-row w-full justify-evenly mt-5"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
            <span class="hljs-attr">onClick</span>=<span class="hljs-string">{deleteImage}</span>
            <span class="hljs-attr">className</span>=<span class="hljs-string">"bg-purple-500 text-white p-2 h-[35px] rounded-lg"</span>
          &gt;</span>
            Delete Image
          <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
            <span class="hljs-attr">onClick</span>=<span class="hljs-string">{FaceAuthenticate}</span>
            <span class="hljs-attr">className</span>=<span class="hljs-string">"bg-purple-500 text-white p-2 h-[35px] rounded-lg"</span>
          &gt;</span>
            Upload Image
          <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/&gt;</span></span>
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> FaceAuth;
</code></pre>
<p>Displayed below is how the face authentication page should look like.</p>
<p><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXcPFsPVo9dymrTmMCyskCszbMf_SdG2n_j5gd7ayT1nQ6jOlhX8a_KFRG51cnqMCxUqFaVgTR2hrdGipmudd9B2TQpNfm4FrFMlYRo7bbu1gtRq1bKB5FmPi4QcbEPTLyDtAPbNEA?key=bLpVfispbJQQ4phtxWLC7w" alt="facial authentication page " width="600" height="400" loading="lazy"></p>
<p>Having set up the frontend, let's head to the backend and configure the registration and login endpoint for our project. The entire code to the backend project can be gotten <a target="_blank" href="http://github.com/oluwatobi2001/stream-backend.git">here</a>. We will only be highlighting the <code>faceAuth</code> backend function in this article.</p>
<p>To verify authentication, we will be using the sessions option instead of the JWT option. Important user information will be stored and accessed in the session cookies attached to the requests and responses to the frontend. Here is the <code>faceAuth</code> function:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> faceAuth = <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-built_in">console</span>.log(req.session);

    <span class="hljs-keyword">const</span> id = req.session.passport?.user;
    <span class="hljs-built_in">console</span>.log(id);


    <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> User.findById(id);
    <span class="hljs-built_in">console</span>.log(user);

    <span class="hljs-keyword">if</span> (user == <span class="hljs-literal">null</span>) {
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({ <span class="hljs-attr">err</span>: <span class="hljs-string">"User not found"</span> });
    }


  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-built_in">console</span>.error(err);
    res.status(<span class="hljs-number">500</span>).json({ <span class="hljs-attr">err</span>: <span class="hljs-string">"Internal Server Error"</span> });
  }
};
</code></pre>
<p>First, we defined an asynchronous function named <code>faceAuth</code>. We then obtained the unique ID of the user who had successfully scaled over the initial login process from the request session.</p>
<p>To confirm the similarity of the user's stored face descriptor and the picture sent from the frontend, we utilized the matching face function based on the Euclidean algorithm to confirm the user's identity as done below.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> isMatchingFace = <span class="hljs-function">(<span class="hljs-params">descriptor1, descriptor2, threshold = <span class="hljs-number">0.6</span></span>) =&gt;</span> {
  <span class="hljs-comment">// Convert the stored descriptors to Float32Array if they aren't already</span>
  <span class="hljs-keyword">if</span> (!(descriptor1 <span class="hljs-keyword">instanceof</span> <span class="hljs-built_in">Float32Array</span>)) {
    descriptor1 = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Float32Array</span>(<span class="hljs-built_in">Object</span>.values(descriptor1));
  }

  <span class="hljs-keyword">if</span> (!(descriptor2 <span class="hljs-keyword">instanceof</span> <span class="hljs-built_in">Float32Array</span>)) {
    descriptor2 = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Float32Array</span>(<span class="hljs-built_in">Object</span>.values(descriptor2));
  }

  <span class="hljs-keyword">const</span> distance = faceapi.euclideanDistance(descriptor1, descriptor2);
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Euclidean Distance:"</span>, distance);

  <span class="hljs-keyword">return</span> distance &lt; threshold;
};
</code></pre>
<p>As stated in the code above, the threshold of similarity of comparison used was 0.6. This is flexible and can be modified to suit the user's preference, as a higher threshold will provide better accuracy overall.<br>If the function returns true, then the user has been successfully authenticated and can then have access to our chat application. Here is the full code snippet.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> faceAuth = <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-built_in">console</span>.log(req.session);

    <span class="hljs-keyword">const</span> id = req.session.passport?.user;
    <span class="hljs-built_in">console</span>.log(id);

    <span class="hljs-keyword">const</span> { faceDescriptor } = req.body;
    <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> User.findById(id);
    <span class="hljs-built_in">console</span>.log(user);

    <span class="hljs-keyword">if</span> (user == <span class="hljs-literal">null</span>) {
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({ <span class="hljs-attr">err</span>: <span class="hljs-string">"User not found"</span> });
    }

    <span class="hljs-keyword">const</span> isMatchingFace = <span class="hljs-function">(<span class="hljs-params">descriptor1, descriptor2, threshold = <span class="hljs-number">0.6</span></span>) =&gt;</span> {
      <span class="hljs-comment">// Convert the stored descriptor (object) to a Float32Array</span>
      <span class="hljs-keyword">if</span> (!(descriptor1 <span class="hljs-keyword">instanceof</span> <span class="hljs-built_in">Float32Array</span>)) {
        descriptor1 = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Float32Array</span>(<span class="hljs-built_in">Object</span>.values(descriptor1));
      }

      <span class="hljs-keyword">if</span> (!(descriptor2 <span class="hljs-keyword">instanceof</span> <span class="hljs-built_in">Float32Array</span>)) {
        descriptor2 = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Float32Array</span>(<span class="hljs-built_in">Object</span>.values(descriptor2));
      }

      <span class="hljs-keyword">const</span> distance = faceapi.euclideanDistance(descriptor1, descriptor2);
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Euclidean Distance:"</span>, distance);

      <span class="hljs-keyword">return</span> distance &lt; threshold;
    };

    <span class="hljs-keyword">if</span> (isMatchingFace(faceDescriptor, user.faceDescriptor)) {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Face match successful"</span>);
      req.session.mfa = <span class="hljs-literal">true</span>;

      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">200</span>).json({
        <span class="hljs-attr">msg</span>: <span class="hljs-string">"User authentication was successful. Proceed to the chat app."</span>,
      });
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">401</span>).json({ <span class="hljs-attr">msg</span>: <span class="hljs-string">"Face does not match. Access denied."</span> });
    }
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-built_in">console</span>.log(err);
    res.status(<span class="hljs-number">500</span>).json({
      <span class="hljs-attr">err</span>: <span class="hljs-string">"User face couldn't be authenticated. Please try again later"</span>,
    });
  }
};
</code></pre>
<p>With the main hurdle completed, we can then navigate to our application and have a seamless chat experience.</p>
<p>Additionally, as a safety measure, a rate limiter is also in place to minimize the use of brute-force techniques by malicious individuals to gain access to the chat application.</p>
<h2 id="heading-additional-information-and-tips">Additional Information and Tips</h2>
<p>The overall aim of these efforts is to achieve a more scalable and secure method of user validation. The threshold can easily be modified and tweaked to improve application accuracy. Alternatively, the <a target="_blank" href="https://aws.amazon.com/rekognition/">AWS Rekognition</a> tool can sufficiently replace the Face API tool with efficient cloud-powered models. The limitations of facial recognition can also be overcome by exploring biometric authentication, as it’s a known fact that each individual's fingerprint is unique, greatly reducing the risk of user compromise.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>So far, we have walked through the process of creating an efficient multi-factor facial authentication-based tool to prevent intruder access to our chat application, ensuring and prioritizing the highest level of user privacy. Need an SDK that assures you of a seamless and secure chat experience? Try Stream.io today.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Clerk vs Kinde vs Better Auth: How to Choose the Right Next.js Authentication Library ]]>
                </title>
                <description>
                    <![CDATA[ Authentication is an important aspect when building applications, especially if they hold financial information or require users to sign into accounts. Building an auth library can be a lot of work, and there is no need to reinvent the wheel when so ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-choose-the-right-nextjs-authentication-library/</link>
                <guid isPermaLink="false">68d2074ea281a4364a16d324</guid>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ authentication ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Andrew Baisden ]]>
                </dc:creator>
                <pubDate>Tue, 23 Sep 2025 02:34:54 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1758594098828/8b2e3142-9067-4a02-b1e5-63319dde45de.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Authentication is an important aspect when building applications, especially if they hold financial information or require users to sign into accounts. Building an auth library can be a lot of work, and there is no need to reinvent the wheel when so many efficient libraries already exist.</p>
<p>In this article, we’ll compare some libraries that you can use for authentication in your Next app. They include: <a target="_blank" href="https://clerk.com/">Clerk</a>, <a target="_blank" href="https://kinde.com/">Kinde</a> and <a target="_blank" href="https://www.better-auth.com/">Better Auth</a>. You’ll learn how to set up these tools in a Next.js application, with the goal of creating at least one authenticated, protected page route.</p>
<p>The aim here is simply to see how each tool works when it comes to speed of setup and ease of use.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-are-clerk-kinde-and-better-auth">What are Clerk, Kinde and Better Auth?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-set-up-authentication-using-clerk">How to Set Up Authentication Using Clerk</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-set-up-authentication-using-kinde">How to Set Up Authentication Using Kinde</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-set-up-authentication-using-better-auth">How to Set Up Authentication Using Better Auth</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-when-to-use-each-library">When To Use Each Library</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-are-clerk-kinde-and-better-auth">What are Clerk, Kinde and Better Auth?</h2>
<p>Clerk, Kinde and Better Auth are basically modern authentication providers, much like <a target="_blank" href="https://authjs.dev/">Auth.js</a>, which have been built with developers in mind. Although they share similarities, they have certain aspects that differentiate them from each other.</p>
<p>To begin with, Clerk is more full-featured. It's more of a hosted solution that offers components which are ready-made, as well as user management and other integrations, which allow you to get up and running pretty quickly.</p>
<p>Kinde, on the other hand, is more of a developer platform, which has authentication, feature flags and team management all in one place.</p>
<p>Better Auth is more of an open-source and code-first place that gives developers the building blocks to create authentication without having to be locked into an ecosystem.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>The prerequisites for this tutorial are minimal, and the databases and Prisma ORM are only required for Better Auth. Alternatively, you can use any of the databases inside a Docker container instead of installing them locally, but that is outside of the scope of this tutorial.</p>
<p>You’ll need these to follow along:</p>
<ul>
<li><p>Node and npm installed</p>
</li>
<li><p><a target="_blank" href="https://www.prisma.io/">Prisma ORM</a></p>
</li>
<li><p>SQLite, PostgreSQL, or MySQL database set up locally</p>
</li>
<li><p>Code editor/IDE</p>
</li>
</ul>
<p>Let's see how to set up authentication with all three auth platforms in a Next.js application. We’ll use separate Next.js applications to set up each library so that the codebase will remain clean, and you can experience what it's like to set them up from scratch.</p>
<p>First, decide on a location for your project, like on the desktop and then use the command <code>npx create-next-app@latest</code> to set up a Next.js project. You can just use the default configuration. These are the settings I used:</p>
<p>✔ What is your project named? … my-app<br>✔ Would you like to use TypeScript? … No / <strong>Yes</strong><br>✔ Which linter would you like to use? › ESLint<br>✔ Would you like to use Tailwind CSS? … No / <strong>Yes</strong><br>✔ Would you like your code inside a <code>src/</code> directory? … No / <strong>Yes</strong><br>✔ Would you like to use App Router? (recommended) … No / <strong>Yes</strong><br>✔ Would you like to use Turbopack? (recommended) … No / <strong>Yes</strong><br>✔ Would you like to customize the import alias (<code>@/*</code> by default)? … <strong>No</strong> / Yes</p>
<p>We are creating three apps, so it's up to you if you want to duplicate the codebases now and give them different names like <code>my-app</code>, <code>my-app2</code> and <code>my-app3</code> or do them later when we reach each section.</p>
<h2 id="heading-how-to-set-up-authentication-using-clerk">How to Set Up Authentication Using Clerk</h2>
<p>With your Next.js project set up, <code>cd</code> into the <code>my-app</code> folder or whatever name you gave the project and run the following command to install the Next.js SDK for Clerk:</p>
<pre><code class="lang-shell">npm install @clerk/nextjs
</code></pre>
<p>Now we need to create a middleware file that will grant us access to user authentication throughout our entire app.</p>
<p>Create a <code>middleware.ts</code> file with this code inside the <code>/src</code> folder:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { clerkMiddleware } <span class="hljs-keyword">from</span> <span class="hljs-string">'@clerk/nextjs/server'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> clerkMiddleware()

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> config = {
  matcher: [
    <span class="hljs-comment">// Skip Next.js internals and all static files, unless found in search params</span>
    <span class="hljs-string">'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)'</span>,
    <span class="hljs-comment">// Always run for API routes</span>
    <span class="hljs-string">'/(api|trpc)(.*)'</span>,
  ],
}
</code></pre>
<p>With this file, authentication is set up for different page routes.</p>
<p>All that's left is to add the <code>&lt;ClerkProvider&gt;</code> component to your app's <code>layout.tsx</code> file so that authentication is available throughout your entire app.</p>
<p>Just replace all of the code inside of <code>src/app/layout.tsx</code> with this code here:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Metadata } <span class="hljs-keyword">from</span> <span class="hljs-string">'next'</span>;
<span class="hljs-keyword">import</span> { Geist, Geist_Mono } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/font/google'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'./globals.css'</span>;
<span class="hljs-keyword">import</span> {
  ClerkProvider,
  SignInButton,
  SignUpButton,
  SignedIn,
  SignedOut,
  UserButton,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@clerk/nextjs'</span>;

<span class="hljs-keyword">const</span> geistSans = Geist({
  variable: <span class="hljs-string">'--font-geist-sans'</span>,
  subsets: [<span class="hljs-string">'latin'</span>],
});

<span class="hljs-keyword">const</span> geistMono = Geist_Mono({
  variable: <span class="hljs-string">'--font-geist-mono'</span>,
  subsets: [<span class="hljs-string">'latin'</span>],
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> metadata: Metadata = {
  title: <span class="hljs-string">'Create Next App'</span>,
  description: <span class="hljs-string">'Generated by create next app'</span>,
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">RootLayout</span>(<span class="hljs-params">{
  children,
}: Readonly&lt;{
  children: React.ReactNode;
}&gt;</span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;ClerkProvider&gt;
      &lt;html lang=<span class="hljs-string">"en"</span>&gt;
        &lt;body
          className={<span class="hljs-string">`<span class="hljs-subst">${geistSans.variable}</span> <span class="hljs-subst">${geistMono.variable}</span> antialiased`</span>}
        &gt;
          &lt;header className=<span class="hljs-string">"flex justify-end items-center p-4 gap-4 h-16"</span>&gt;
            &lt;SignedOut&gt;
              &lt;SignInButton /&gt;
              &lt;SignUpButton&gt;
                &lt;button className=<span class="hljs-string">"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"</span>&gt;
                  Sign Up
                &lt;/button&gt;
              &lt;/SignUpButton&gt;
            &lt;/SignedOut&gt;
            &lt;SignedIn&gt;
              &lt;UserButton /&gt;
            &lt;/SignedIn&gt;
          &lt;/header&gt;
          {children}
        &lt;/body&gt;
      &lt;/html&gt;
    &lt;/ClerkProvider&gt;
  );
}
</code></pre>
<p>What we did was to import the <code>ClerkProvider</code>, as well as the buttons for signing in and out using Clerk authentication. These additions have been added to the <code>layout.tsx</code> file which means that they are available throughout our entire application. So every page should display the sign in flow at the top of the page.</p>
<p>The <code>ClerkProvider</code> component is needed for integrating Clerk inside of our application, so now we can use session and user context with Clerks hooks and components.</p>
<p>Now you can run your Next.js app with <code>npm run dev</code>, and you should see the homepage, as well as a sign-in and sign-up button at the top of the page, as shown here:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757428533274/31f43e8f-7826-4d4c-bf10-99b5ea7d8a76.png" alt="Next.js homepage with Clerk authentication setup" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Clicking the signup button will take you to a sign-up form where you can use an email address or sign in with Google, which is pretty easy.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757428605871/94e754e7-897f-42f2-84df-361d97226617.png" alt="Clerk Sign up form" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>When you have signed in, you should see your profile picture and account information in the top right-hand corner of the screen. That's the hard part done - all that's left is to create a page and then make the route protected so that only a signed-in user can access it.</p>
<p>To begin with, lets update our <code>middleware.ts</code> file with some code which lets us protect a route:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { clerkMiddleware, createRouteMatcher } <span class="hljs-keyword">from</span> <span class="hljs-string">'@clerk/nextjs/server'</span>

<span class="hljs-keyword">const</span> isProtectedRoute = createRouteMatcher([<span class="hljs-string">'/dashboard(.*)'</span>])

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> clerkMiddleware(<span class="hljs-keyword">async</span> (auth, req) =&gt; {
  <span class="hljs-keyword">if</span> (isProtectedRoute(req)) <span class="hljs-keyword">await</span> auth.protect()
})

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> config = {
  matcher: [
    <span class="hljs-comment">// Skip Next.js internals and all static files, unless found in search params</span>
    <span class="hljs-string">'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)'</span>,
    <span class="hljs-comment">// Always run for API routes</span>
    <span class="hljs-string">'/(api|trpc)(.*)'</span>,
  ],
}
</code></pre>
<p>We added some new imports for <code>createRouteMatcher</code>, which is a Clerk helper function that gives us the power to protect multiple routes. In this case, the dashboard page route in our application requires a user to be signed in to access the route. Now we need create a dashboard page. Create this folder and file inside the <code>app</code> folder: <code>dashboard/page.tsx</code>. Then complete the page by giving it some code like below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Dashboard</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;&gt;
      &lt;h1&gt;Dashboard Page&lt;/h1&gt;
    &lt;/&gt;
  );
}
</code></pre>
<p>We created a simple page which has a heading that says Dashboard Page.</p>
<p>Congratulations, you have successfully added authentication to your Next app and protected a page route, and it only took a few steps! When you navigate to <a target="_blank" href="http://localhost:3000/dashboard">http://localhost:3000/dashboard</a> as a non-signed-in user, you should be redirected to a sign-in form as shown below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757428660606/5ac6984d-7f8c-46e2-bd0b-732920d5743e.png" alt="Clerk sign in form page" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>If you are already signed in, then you should see the Dashboard page. You can learn more using the <a target="_blank" href="https://clerk.com/docs">Clerk official documentation</a>.</p>
<h2 id="heading-how-to-set-up-authentication-using-kinde">How to Set Up Authentication Using Kinde</h2>
<p>Create another Next.js application for this project if you have not done so already. Kinde will require you to create an account on their platform before using their authentication library. Let's go through the sign-up process.</p>
<p>Firstly, go to the <a target="_blank" href="https://kinde.com/">Kinde</a> website, and you should see a button that says "Start for free" or similar.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757428704023/dd6bf59e-2858-4f0a-8aa4-90645641ebe8.png" alt="Kinde website homepage" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Clicking that button should take you to a page where you can create an account:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757428737190/0cd172f6-ed9a-4a54-b599-182a8becfcfa.png" alt="Kinde create your account" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>An email code verification may be required:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757428803166/8a883160-6e95-4611-91d3-efbabd74653c.png" alt="Kinde email code verification" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>On the next screen, you should be able to enter your business details, which can be anything you want. Every time you set up authentication for an app, you will have to create an application for it on your account. Give it any name you want, like <code>app-clerk-test3272346214</code>. The same name will be used for the business and the domain.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757428845977/7d365554-9c15-41e0-b317-a12d58f3e7a6.png" alt="Kinde form business and domain" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>On the next screen, we’ll choose to use an existing codebase because we have a local project:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757428896749/cd468f14-1b36-4025-8459-b51509519fa0.png" alt="Kinde existing codebase select" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>The codebase is in Next.js, so select it from the list:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757428936691/cf13041f-bc74-4432-a7ba-4234daf7c2a3.png" alt="Kinde select tech stack" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>The next important step is choosing how users are going to sign in. I chose email and Google. You can select whichever options you desire:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757428976433/2885ed25-d4d0-496e-ac46-2f8b96c35a20.png" alt="Kinde user sign in form" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Now, on the last screen, choose to explore at your own pace.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757429011937/23a9d95f-2935-4a9c-947f-91d7adaf452e.png" alt="Kinde explore at own pace screen" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>And finally, we’ve reach the dashboard screen.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757429051925/2250e252-e84d-4ae9-bff8-a9e687692f70.png" alt="Kinde dashboard screen" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Viewing details lets you see your app keys and environment variables, among other useful information.</p>
<p>That's the long part out of the way, let's get to some code. Navigate to your project folder and then install the package for Kinde:</p>
<pre><code class="lang-shell">npm i @kinde-oss/kinde-auth-nextjs
</code></pre>
<p>Now, create a <code>.env.local</code> file and put it in the root folder of your project with your environment variables. You can find your environment variables in the Quick Start page of your application.</p>
<p>Here's an example:</p>
<pre><code class="lang-shell">KINDE_CLIENT_ID=&lt;your_kinde_client_id&gt;
KINDE_CLIENT_SECRET=&lt;your_kinde_client_secret&gt;
KINDE_ISSUER_URL=https://&lt;your_kinde_subdomain&gt;.kinde.com
KINDE_SITE_URL=http://localhost:3000
KINDE_POST_LOGOUT_REDIRECT_URL=http://localhost:3000
KINDE_POST_LOGIN_REDIRECT_URL=http://localhost:3000/dashboard
</code></pre>
<p>Next, you need to create the following API endpoint and folder structure and files as shown here <code>src/app/api/auth/[kindeAuth]/route.ts</code>.</p>
<p>This is the code needed for the <code>route.ts</code> file:</p>
<pre><code class="lang-shell">import {handleAuth} from "@kinde-oss/kinde-auth-nextjs/server";

export const GET = handleAuth();
</code></pre>
<p>With this code, Kinde can now handle auth endpoints inside our application.</p>
<p>Once again, you’ll need a <code>middleware.ts</code> file so that authentication can be set up properly in your app. The file should be in the root directory and needs this code added to it:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { withAuth } <span class="hljs-keyword">from</span> <span class="hljs-string">"@kinde-oss/kinde-auth-nextjs/middleware"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">middleware</span>(<span class="hljs-params">req</span>) </span>{
  <span class="hljs-keyword">return</span> withAuth(req);
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> config = {
  matcher: [
    <span class="hljs-comment">// Run on everything but Next internals and static files</span>
    <span class="hljs-string">'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)'</span>,
  ]
};
</code></pre>
<p>Like before, we can now protect page routes with this file. Your app has to be wrapped in a Kinde Auth Provider so that you can access the data throughout your app.</p>
<p>Create an <code>AuthProvider.tsx</code> file inside the <code>app</code> directory with this code:</p>
<pre><code class="lang-typescript"><span class="hljs-string">"use client"</span>;
<span class="hljs-keyword">import</span> {KindeProvider} <span class="hljs-keyword">from</span> <span class="hljs-string">"@kinde-oss/kinde-auth-nextjs"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> AuthProvider = <span class="hljs-function">(<span class="hljs-params">{children}</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> &lt;KindeProvider&gt;{children}&lt;/KindeProvider&gt;;
};
</code></pre>
<p>Kinde uses a React Context Provider to maintain its internal state throughout our application by using the <code>KindeProvider</code> component.</p>
<p>Next, replace and update the <code>layout.tsx</code> file, so it is wrapped in the Auth Provider:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Metadata } <span class="hljs-keyword">from</span> <span class="hljs-string">'next'</span>;
<span class="hljs-keyword">import</span> { Geist, Geist_Mono } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/font/google'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'./globals.css'</span>;
<span class="hljs-keyword">import</span> { AuthProvider } <span class="hljs-keyword">from</span> <span class="hljs-string">'./AuthProvider'</span>;
<span class="hljs-keyword">import</span> {
  RegisterLink,
  LoginLink,
  LogoutLink,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@kinde-oss/kinde-auth-nextjs/components'</span>;

<span class="hljs-keyword">const</span> geistSans = Geist({
  variable: <span class="hljs-string">'--font-geist-sans'</span>,
  subsets: [<span class="hljs-string">'latin'</span>],
});

<span class="hljs-keyword">const</span> geistMono = Geist_Mono({
  variable: <span class="hljs-string">'--font-geist-mono'</span>,
  subsets: [<span class="hljs-string">'latin'</span>],
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> metadata: Metadata = {
  title: <span class="hljs-string">'Create Next App'</span>,
  description: <span class="hljs-string">'Generated by create next app'</span>,
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">RootLayout</span>(<span class="hljs-params">{
  children,
}: Readonly&lt;{
  children: React.ReactNode;
}&gt;</span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;AuthProvider&gt;
      &lt;div className=<span class="hljs-string">"grid grid-flow-col gap2"</span>&gt;
        &lt;LoginLink&gt;Sign <span class="hljs-keyword">in</span>&lt;/LoginLink&gt;
        &lt;RegisterLink&gt;Sign up&lt;/RegisterLink&gt;
        &lt;LogoutLink&gt;Log out&lt;/LogoutLink&gt;
      &lt;/div&gt;
      &lt;html lang=<span class="hljs-string">"en"</span>&gt;
        &lt;body
          className={<span class="hljs-string">`<span class="hljs-subst">${geistSans.variable}</span> <span class="hljs-subst">${geistMono.variable}</span> antialiased`</span>}
        &gt;
          {children}
        &lt;/body&gt;
      &lt;/html&gt;
    &lt;/AuthProvider&gt;
  );
}
</code></pre>
<p>In this file, we also added buttons for signing up, signing in, and logging out which will be displayed at the top of every page as this is the main <code>layout.tsx</code> file. Our <code>AuthProvider</code> component is wrapped around our application which means we can now use Kinde throughout it.</p>
<p>The basic setup is now complete! Run the usual command to start the Next.js app, and you should see the sign-in flow buttons at the top of the screen.</p>
<p>You should be able to sign up and create an account. Doing so will redirect you to the dashboard screen, which shows a 404 error page. This is because we have not created a dashboard page yet.</p>
<p>This is what the Kinde register form looks like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757429157871/c7344f32-cf4e-48d3-b43e-225f81912988.png" alt="Kinde Register form" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>And this is what the Kinde sign-in form looks like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757429447577/3d30476a-9269-49a9-811c-dff0c16225d9.png" alt="Kinde sign in form" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Only one step remains now: creating a protected route for your authentication.</p>
<p>Create the following file structure and file for your dashboard page: <code>src/app/dashboard/page.tsx</code>.</p>
<p>Then add this code to the file:</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;

<span class="hljs-keyword">import</span> { useKindeBrowserClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@kinde-oss/kinde-auth-nextjs'</span>;
<span class="hljs-keyword">import</span> { LoginLink } <span class="hljs-keyword">from</span> <span class="hljs-string">'@kinde-oss/kinde-auth-nextjs/components'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Dashboard</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> { isAuthenticated, isLoading } = useKindeBrowserClient();

  <span class="hljs-keyword">if</span> (isLoading) <span class="hljs-keyword">return</span> &lt;div&gt;Loading...&lt;/div&gt;;

  <span class="hljs-keyword">return</span> isAuthenticated ? (
    &lt;div&gt;
      &lt;p&gt;
        Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque ut
        ante enim. Maecenas ut eros nec diam vulputate sollicitudin. Cras ut
        quam leo. Pellentesque semper, lacus sodales gravida suscipit, metus
        quam congue eros, nec sagittis est dolor eu turpis. Nulla congue
        tristique venenatis. Donec ac venenatis mauris. Donec commodo cursus
        magna, vitae tincidunt magna vestibulum eget.
      &lt;/p&gt;
    &lt;/div&gt;
  ) : (
    &lt;div&gt;
      You have to &lt;LoginLink&gt;Login&lt;/LoginLink&gt; to see <span class="hljs-built_in">this</span> page
    &lt;/div&gt;
  );
}
</code></pre>
<p>This page file checks to see if the user is authenticated and signed in. If they are signed in, they see the Lorem ipsum text, and if they are not signed in, they will see a message telling them that they have to log in to see the page.</p>
<p>All you have to do is go to the <a target="_blank" href="http://localhost:3000/dashboard">dashboard route</a> as a signed-in or signed-out user to see it for yourself. And that's pretty much the basics of authentication using the Kinde platform. See the <a target="_blank" href="https://docs.kinde.com/">online documentation</a> to learn everything there is to know about it.</p>
<h2 id="heading-how-to-set-up-authentication-using-better-auth">How to Set Up Authentication Using Better Auth</h2>
<p>And lastly, let's create a project that uses Better Auth. Better Auth requires a database to store user data, so the setup will require a few more steps. You can find the installation guide <a target="_blank" href="https://www.better-auth.com/docs/installation">here</a>, but, we’ll also go through it here.</p>
<p>Ok, just like before, create a Next.js project if you have not done so yet and then install the <code>better-auth</code> package with this command:</p>
<pre><code class="lang-shell">npm install better-auth
</code></pre>
<p>Next, you have to set up your environment variables, so create a <code>.env</code> file with these values:</p>
<pre><code class="lang-shell">BETTER_AUTH_SECRET= #Create your own secret key!
BETTER_AUTH_URL=http://localhost:3000 # Base URL of your app
</code></pre>
<p>Make sure that you create a secret key, as if you were generating a secure password with uppercase and lowercase letters and numbers.</p>
<p>Let's get our Prisma package and PostgreSQL database set up, so run these scripts to initialise them:</p>
<pre><code class="lang-shell">npm install prisma --save-dev
npx prisma init
npm install @prisma/client
</code></pre>
<p>In the next step, we have to create a better auth instance. In this case, we will put the file in the <code>src/lib/auth.ts</code>.</p>
<p>So create an <code>auth.ts</code> file with this code:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { betterAuth } <span class="hljs-keyword">from</span> <span class="hljs-string">'better-auth'</span>;
<span class="hljs-keyword">import</span> { anonymous } <span class="hljs-keyword">from</span> <span class="hljs-string">'better-auth/plugins'</span>;
<span class="hljs-keyword">import</span> { prismaAdapter } <span class="hljs-keyword">from</span> <span class="hljs-string">'better-auth/adapters/prisma'</span>;
<span class="hljs-comment">// If your Prisma file is located elsewhere, you can change the path</span>
<span class="hljs-keyword">import</span> { PrismaClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/generated/prisma'</span>;

<span class="hljs-keyword">const</span> prisma = <span class="hljs-keyword">new</span> PrismaClient();
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> auth = betterAuth({
  database: prismaAdapter(prisma, {
    provider: <span class="hljs-string">'postgresql'</span>, <span class="hljs-comment">// or "mysql", "postgresql", ...etc</span>
  }),
  plugins: [anonymous()],
});
</code></pre>
<p>In this file, we have configured Better Auth to use Prisma ORM for our database connection, and we will be connecting to a PostgreSQL database. If you want, you can change it to a different database, but the setup might be different, so bear that in mind. You could also use Docker if you know how to set it up. Anonymous sign-in is the default for users.</p>
<p>Now we need to create our database tables for saving user information, so use this command in the terminal to do that:</p>
<pre><code class="lang-shell">npx @better-auth/cli generate
</code></pre>
<p>You might see this warning, just select yes with "y":</p>
<pre><code class="lang-shell">prisma:warn In production, we recommend using `prisma generate --no-engine` (See: `prisma generate --help`)
✔ The file ./prisma/schema.prisma already exists. Do you want to overwrite the schema to the file? … yes
</code></pre>
<p>For handling API requests, we must have a route handler set up on our server. Create a folder structure and file for the <code>route.ts</code> file, like shown here <code>/app/api/auth/[...all]/route.ts</code>.</p>
<p>Add this code to the file:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { auth } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/auth'</span>; <span class="hljs-comment">// path to your auth file</span>
<span class="hljs-keyword">import</span> { toNextJsHandler } <span class="hljs-keyword">from</span> <span class="hljs-string">'better-auth/next-js'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> { POST, GET } = toNextJsHandler(auth);
</code></pre>
<p>This file lets you handle POST and GET requests for your auth file.</p>
<p>Lastly, we have to create a <code>lib/auth-client.ts</code> file. This file allows you to interact with the auth server, and it has a plugin so users can sign in anonymously.</p>
<p>And here is the code to put inside this file:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { createAuthClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'better-auth/react'</span>;
<span class="hljs-keyword">import</span> { anonymousClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'better-auth/client/plugins'</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> authClient = createAuthClient({
  <span class="hljs-comment">/** The base URL of the server (optional if you're using the same domain) */</span>
  baseURL: <span class="hljs-string">'http://localhost:3000'</span>,
  plugins: [anonymousClient()],
});
</code></pre>
<p>With this file, it's possible for users to sign in anonymously without having to even create an account or use social sign-in, thanks to the plugin.</p>
<p>All that remains is to create another dashboard page, which has authentication like before. Create another dashboard page with this structure: <code>app/dashboard/page.tsx</code>, and then add this code to the file:</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;

<span class="hljs-keyword">import</span> { useState, useEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { authClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/auth-client'</span>;

<span class="hljs-keyword">type</span> User = {
  id: <span class="hljs-built_in">string</span>;
  email: <span class="hljs-built_in">string</span>;
  emailVerified: <span class="hljs-built_in">boolean</span>;
  name: <span class="hljs-built_in">string</span>;
  createdAt: <span class="hljs-built_in">Date</span>;
  updatedAt: <span class="hljs-built_in">Date</span>;
  image?: <span class="hljs-built_in">string</span> | <span class="hljs-literal">null</span>;
  isAnonymous?: <span class="hljs-built_in">boolean</span> | <span class="hljs-literal">null</span>;
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Dashboard</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [user, setUser] = useState&lt;User | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> [isLoading, setIsLoading] = useState(<span class="hljs-literal">true</span>);
  <span class="hljs-keyword">const</span> [isSigningIn, setIsSigningIn] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> [error, setError] = useState&lt;<span class="hljs-built_in">string</span> | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> checkAuth = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> session = <span class="hljs-keyword">await</span> authClient.getSession();
        <span class="hljs-keyword">if</span> (session.data?.user) {
          setUser(session.data.user);
        }
      } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Auth check error:'</span>, err);
      } <span class="hljs-keyword">finally</span> {
        setIsLoading(<span class="hljs-literal">false</span>);
      }
    };

    checkAuth();
  }, []);

  <span class="hljs-keyword">const</span> handleAnonymousSignIn = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">try</span> {
      setIsSigningIn(<span class="hljs-literal">true</span>);
      setError(<span class="hljs-literal">null</span>);

      <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> authClient.signIn.anonymous();

      <span class="hljs-keyword">if</span> (result.data) {
        setUser(result.data.user);
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Anonymous user signed in:'</span>, result.data.user);
      } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (result.error) {
        setError(result.error.message || <span class="hljs-string">'Failed to sign in anonymously'</span>);
      }
    } <span class="hljs-keyword">catch</span> (err) {
      setError(<span class="hljs-string">'An unexpected error occurred'</span>);
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Anonymous sign-in error:'</span>, err);
    } <span class="hljs-keyword">finally</span> {
      setIsSigningIn(<span class="hljs-literal">false</span>);
    }
  };

  <span class="hljs-keyword">const</span> handleSignOut = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">await</span> authClient.signOut();
      setUser(<span class="hljs-literal">null</span>);
    } <span class="hljs-keyword">catch</span> (err) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Sign out error:'</span>, err);
    }
  };

  <span class="hljs-keyword">if</span> (isLoading) {
    <span class="hljs-keyword">return</span> (
      &lt;div className=<span class="hljs-string">"max-w-4xl mx-auto p-6"</span>&gt;
        &lt;div className=<span class="hljs-string">"flex items-center justify-center min-h-[400px]"</span>&gt;
          &lt;div className=<span class="hljs-string">"text-center"</span>&gt;
            &lt;div className=<span class="hljs-string">"animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"</span>&gt;&lt;/div&gt;
            &lt;p className=<span class="hljs-string">"text-gray-600"</span>&gt;Checking authentication...&lt;/p&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    );
  }

  <span class="hljs-keyword">if</span> (!user) {
    <span class="hljs-keyword">return</span> (
      &lt;div className=<span class="hljs-string">"max-w-4xl mx-auto p-6"</span>&gt;
        &lt;div className=<span class="hljs-string">"text-center"</span>&gt;
          &lt;h1 className=<span class="hljs-string">"text-3xl font-bold mb-6"</span>&gt;Access Required&lt;/h1&gt;
          &lt;p className=<span class="hljs-string">"text-gray-600 mb-8"</span>&gt;
            You need to be signed <span class="hljs-keyword">in</span> to access our dashboard. Choose an option
            below to <span class="hljs-keyword">continue</span>.
          &lt;/p&gt;

          {error &amp;&amp; (
            &lt;div className=<span class="hljs-string">"bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-6 max-w-md mx-auto"</span>&gt;
              {error}
            &lt;/div&gt;
          )}

          &lt;div className=<span class="hljs-string">"bg-gray-50 p-8 rounded-lg border max-w-md mx-auto"</span>&gt;
            &lt;h2 className=<span class="hljs-string">"text-xl font-semibold mb-4 text-black"</span>&gt;
              Sign In Options
            &lt;/h2&gt;

            &lt;button
              onClick={handleAnonymousSignIn}
              disabled={isSigningIn}
              className=<span class="hljs-string">"w-full bg-blue-500 hover:bg-blue-600 disabled:bg-blue-300 text-white px-6 py-3 rounded font-medium mb-4"</span>
            &gt;
              {isSigningIn ? <span class="hljs-string">'Signing in...'</span> : <span class="hljs-string">'Sign In Anonymously'</span>}
            &lt;/button&gt;

            &lt;p className=<span class="hljs-string">"text-sm text-gray-500 mb-4"</span>&gt;
              Anonymous access allows you to use our dashboard without creating
              an account. You can always link your account later.
            &lt;/p&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    );
  }

  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">"max-w-4xl mx-auto p-6"</span>&gt;
      &lt;div className=<span class="hljs-string">"bg-green-50 border border-green-200 rounded-lg p-4 mb-6"</span>&gt;
        &lt;div className=<span class="hljs-string">"flex items-center justify-between"</span>&gt;
          &lt;div&gt;
            &lt;h2 className=<span class="hljs-string">"text-lg font-semibold text-green-800"</span>&gt;
              Welcome, {user.isAnonymous ? <span class="hljs-string">'Anonymous User'</span> : user.name}!
            &lt;/h2&gt;
            &lt;p className=<span class="hljs-string">"text-sm text-green-600"</span>&gt;
              {user.isAnonymous
                ? <span class="hljs-string">'You are signed in anonymously'</span>
                : <span class="hljs-string">`Signed in as <span class="hljs-subst">${user.email}</span>`</span>}
            &lt;/p&gt;
          &lt;/div&gt;
          &lt;button
            onClick={handleSignOut}
            className=<span class="hljs-string">"bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded text-sm"</span>
          &gt;
            Sign Out
          &lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;h1 className=<span class="hljs-string">"text-3xl font-bold mb-6"</span>&gt;Dashboard&lt;/h1&gt;

      {process.env.NODE_ENV === <span class="hljs-string">'development'</span> &amp;&amp; (
        &lt;div className=<span class="hljs-string">"mt-8 bg-gray-100 p-4 rounded-lg"</span>&gt;
          &lt;h3 className=<span class="hljs-string">"font-semibold mb-2 text-black"</span>&gt;Debug Info:&lt;/h3&gt;
          &lt;pre className=<span class="hljs-string">"text-xs text-gray-600 overflow-auto"</span>&gt;
            {<span class="hljs-built_in">JSON</span>.stringify(user, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>)}
          &lt;/pre&gt;
        &lt;/div&gt;
      )}
    &lt;/div&gt;
  );
}
</code></pre>
<p>This code creates a dashboard page which requires a user to be authenticated and signed in to use it. After a user signs in anonymously, they can view some debug info about their profile. So basically, this page is an authentication flow for our dashboard view which integrates with our custom <code>authClient</code>. This page also handles loading error state and sign out for anonymous users.</p>
<p>Ok, now we probably need to reset our Prisma database again, or we could get schema and table errors.</p>
<p>First, make sure that your Prisma development database is running with this command:</p>
<pre><code class="lang-shell">npx prisma dev
</code></pre>
<p>And then run these commands to reset the database and apply the new migrations for the schema:</p>
<pre><code class="lang-shell">npx prisma migrate reset
npx prisma migrate dev
</code></pre>
<p>You might need to restart the Prisma development server and then run it with the command <code>npx prisma dev</code>.</p>
<p>Our Better Auth app needs two servers to be running.</p>
<ol>
<li><p>The Prisma development server</p>
</li>
<li><p>The Next.js application</p>
</li>
</ol>
<p>With the Prisma development server running, you can now start the Next.js app with the usual command <code>npm run dev</code>.</p>
<p>If you encounter any problems with the Prisma ORM or database, like tables missing or schema mismatches, then here are some useful commands which could hopefully resolve them.</p>
<pre><code class="lang-shell"># ⚠️ WARNING: This will drop the database, recreate it, and apply all migrations from scratch.
npx prisma migrate reset

# Applies any new migrations that haven’t been run yet (or creates a new one if your schema changed).
npx prisma migrate dev

# Starts Prisma Studio (a GUI for exploring and editing your database).
npx prisma dev

# Runs Better Auth CLI migrations (sets up any database tables/changes required for authentication).
npx @better-auth/cli migrate
</code></pre>
<p>These commands are self-explanatory and let us run migrations on our database when there are changes, and we can see our database inside of Prisma Studio.</p>
<p>This is what the dashboard page looks like when a user is not signed in:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757429500439/9c0846fc-cfc5-4232-9a02-4e313c2c4698.png" alt="Better Auth app Dashboard page" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Signing in will show the dashboard screen. To learn more about Better Auth, you can read their <a target="_blank" href="https://www.better-auth.com/">official documentation</a>.</p>
<h2 id="heading-when-to-use-each-library">When To Use Each Library</h2>
<p>Each of these authentication libraries have their pros and cons, and can suit various needs depending on your project. Knowing when to use each one can better prepare you for real-world conditions and when building for production, so let's go through them and see how they compare.</p>
<h3 id="heading-when-to-use-clerk">When to Use Clerk</h3>
<p>Clerk excels when you need to use auth quickly without worrying about managing multiple servers, and when you need to have pre-made interfaces and management systems. It's great for startups and teams that want to prioritize developer experience and fast implementation.</p>
<p>It has a streamlined and user-friendly setup, which is still able to support the bare minimum essentials, so it's a really good option for small teams and projects that must have an easy implementation. If you are building a SaaS that needs a good interface and components from the start, or if you require social logins without a ton of code, then it's a very good solution, especially if you want speed and cost effectiveness in a smaller project.</p>
<h3 id="heading-when-to-use-kinde">When to Use Kinde</h3>
<p>Kinde is a fantastic choice when you are keen on having transparent pricing and quick auth integration in more frameworks. Kinde has been designed to be a cost-effective alternative to Clerk and offers more transparent pricing and a more generous, free tier.</p>
<p>It's great for teams that need to have a reliable authentication option but want lower costs and better framework support. Kinde is effective when used in medium-sized projects that need more than a basic authentication system, but also don't have the need to have enterprise-grade tooling.</p>
<h3 id="heading-when-to-use-better-auth">When to Use Better Auth</h3>
<p>Better Auth is a great solution when you have a need for an expansive set of features out of the box. It is also worth noting that Better Auth has a plugin ecosystem that can simplify adding more advanced functionalities with only a few lines of code. Of all the options discussed in this article, Better Auth is by far the cleanest; however, it requires more coding knowledge and skills.</p>
<p>It's a good option if you are building a TypeScript application and want to have full control over the customization and auth data flows. The framework is agnostic and has features such as 2FA, multi-tenant support and other complex features so developers can get the best out of the tool, as there is no vendor lock-in. Functionality can easily be expanded with the plugin ecosystem, so developers can tailor it to their needs.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>All three platforms are fairly good at their job, and I do not doubt that they are going to remain popular options for adding authentication to our applications. Auth.js is one of the most well-known out there; however, Clerk, Kinde and Better Auth also appear to have growing followings, and judging by conversations on socials, they appear to be the first choice for many developers at the moment.</p>
<p>After experiencing what it's like to set them up for the first time, I would have to say that Clerk is the easiest to set up because you don't have to create an account, and you can get the authentication working fairly quickly with little troubleshooting. Kinde would be the second easiest to set up, in my opinion. You do have to register for an account to use the platform; however, the setup was also pretty easy and did not need any troubleshooting.</p>
<p>Better Auth is a great platform, but the setup requires a bit more work because a database is required for storing users' information, which makes the process slightly more difficult. I also found it easier to create authenticated routes with the other two auth options. However, the fact that the platform is open-source with no vendor lock-in works in its favour because developers can self-host and it's completely free, which means no paid plans.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Use Postman Scripts to Simplify Your API Authentication Process ]]>
                </title>
                <description>
                    <![CDATA[ Postman is a platform used by developers, API testers, technical writers and DevOps teams for testing, documenting and collaborating on API development. It provides a user-friendly interface for making different types of API requests (HTTP, GraphQL, ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-postman-scripts/</link>
                <guid isPermaLink="false">68bee731d2147595571c5b44</guid>
                
                    <category>
                        <![CDATA[ authentication ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ automation ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Orim Dominic Adah ]]>
                </dc:creator>
                <pubDate>Mon, 08 Sep 2025 14:24:49 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1757341168209/dc77dc00-a0a6-40f7-b766-ce07d0d8a637.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Postman is a platform used by developers, API testers, technical writers and DevOps teams for testing, documenting and collaborating on API development. It provides a user-friendly interface for making different types of API requests (HTTP, GraphQL, gRPC), inspecting responses, and organizing API calls into collections for collaboration and automation.</p>
<p>Performing repetitive tasks while testing APIs is stressful and time-wasting. For example, the process of retrieving, copying and pasting new authentication tokens for use in Postman is repetitive. You can simplify this process by using Postman scripts to store auth tokens and then reuse them without repeating the copy and paste steps.</p>
<p>To practice along with this guide, you should have:</p>
<ul>
<li><p>The <a target="_blank" href="https://www.postman.com/downloads/">Postman API client</a> installed on your computer</p>
</li>
<li><p>Experience in making API requests with Postman</p>
</li>
<li><p>A backend application that uses JWT authentication and has its documentation in your Postman client</p>
</li>
</ul>
<p>If you don’t have a backend application setup, I created one that you can clone from GitHub at <a target="_blank" href="https://github.com/orimdominic/freeCodeCamp-postman-api-jwt">orimdominic/freeCodeCamp-postman-api-jwt</a>.</p>
<p>By the end of this article, you should be able to simplify the process of obtaining and reusing authentication tokens across your API requests. You should also have a practical understanding of some scripts necessary for use in other areas of software testing with Postman.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-table-of-contents">Table of Contents</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-are-postman-scripts">What are Postman Scripts?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-simplify-your-jwt-authentication-process">How to Simplify Your JWT Authentication Process</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-authenticate-to-get-the-token">Authenticate to Get the Token</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-save-the-token-in-a-variable-with-a-postman-script">How to Save the Token in a Variable with a Postman Script</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-use-the-variable-in-a-request">How to Use the Variable in a Request</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-next-steps">Next Steps</a></p>
</li>
</ul>
<h2 id="heading-what-are-postman-scripts">What are Postman Scripts?</h2>
<p><a target="_blank" href="https://learning.postman.com/docs/tests-and-scripts/tests-and-scripts/">Postman scripts</a> are blocks of JavaScript code that you can write and run within the Postman API client to automate and enhance API testing workflows. You can use Postman scripts to add code to run before and after API requests. These scripts can be used to:</p>
<ul>
<li><p>Add logic and process data from API requests</p>
</li>
<li><p>Write test assertions for API responses</p>
</li>
<li><p>Run automated tests on API endpoints</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756577771526/161bd327-fbf7-48cb-acab-317ab1cad4c5.jpeg" alt="The Postman scripts tab" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>You can find Postman scripts under the <strong>Scripts</strong> tab of an API request. Code written in the <strong>Pre-request</strong> tab runs before the request is made and code written in the <strong>Post-response</strong> tab runs after the response is made.</p>
<h2 id="heading-how-to-simplify-your-jwt-authentication-process">How to Simplify Your JWT Authentication Process</h2>
<p>In summary, you will carry out the following steps to achieve the objective of this tutorial:</p>
<ol>
<li><p>Authenticate to get the token</p>
</li>
<li><p>Save the token in a collection variable with Postman scripts</p>
</li>
<li><p>Use the variable in an API request</p>
</li>
</ol>
<h3 id="heading-authenticate-to-get-the-token">Authenticate to Get the Token</h3>
<p>To get started, carry out the following steps:</p>
<ol>
<li><p>Start your backend application and make sure it is running successfully.</p>
</li>
<li><p>Open up your Postman application and go to the API request for signing in to get a JWT.</p>
</li>
<li><p>Make an API request to the sign in endpoint and take note of the JSON response schema.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756573137191/b5aad14c-5094-4a84-8876-1bbbb869064c.jpeg" alt="Authentication request response" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>The highlighted part of the image above shows the JSON response from a successful sign in request. In the response schema, the auth token to be used for authorization is in the <code>data.token</code> field. You will use Postman scripts to store this token in a variable and then use the variable in the <code>Authorization</code> header of requests that require authorization.</p>
<h3 id="heading-how-to-save-the-token-in-a-variable-with-a-postman-script">How to Save the Token in a Variable with a Postman Script</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756575948975/2b43493d-2803-45cd-aefe-0ca5694f75e8.jpeg" alt="Add logic in Post-response Postman script" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>In Postman, click on the <strong>Scripts</strong> tab next to the <strong>Body</strong> tab. If the Postman application window is small, you may need to click a dropdown to see it. Next, click on the <strong>Post-response</strong> tab. In the text area to the right, you will write the script to capture the auth token from the response and store it in a Postman variable. Copy the JavaScript code below and paste it into the text area.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">if</span> (pm.response.code == <span class="hljs-number">200</span>) {
    <span class="hljs-keyword">const</span> token = pm.response.json().data.token
    pm.collectionVariables.set(<span class="hljs-string">"auth_token"</span>, token)
}
</code></pre>
<p>Postman scripts use the <a target="_blank" href="https://learning.postman.com/docs/tests-and-scripts/write-scripts/postman-sandbox-api-reference/"><code>pm</code> identifier</a> to access and modify information in the Postman environment. The script above uses <code>pm</code> to first ensure that the request was successful by checking if the response status code is <code>200</code>.</p>
<p>Inside the conditional statement, <code>pm.response.json().data.token</code> is used to get the authentication token from the JSON response and store it in a collection variable called <code>auth_token</code>. If <code>auth_token</code> doesn’t exist already, it is created and its value is set to the value of <code>token</code>. If it exists already, its value is replaced.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756581970294/bed1fe89-9c00-4b94-9f71-173ea3bf1cd1.png" alt="Postman collection variable set by a script" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>To confirm that <code>auth_token</code> has been set, click on the name of the collection (labelled 1 in the screenshot above) and then click on the <strong>Variables</strong> tab (labelled 2 in the screenshot above). Next, instead of repeatedly copying the token and pasting it in the <code>Authorization</code> header of your requests, you will use <code>auth_token</code> in the <code>Authorization</code> header of your requests.</p>
<h3 id="heading-how-to-use-the-variable-in-a-request">How to Use the Variable in a Request</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756583915681/d3bf0f56-c406-4d3e-b7f1-df4cbc2a3cfc.png" alt="Use the Variable in a Request" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Reference the collection variable in the <code>Authorization</code> header by surrounding it with double curly braces <code>{{auth_token}}</code>. When you make an API request, Postman will use the value referenced by <code>{{auth_token}}</code> as the <code>Authorization</code> header.</p>
<p>If another authentication request causes the value of <code>auth_token</code> to be updated, you no longer need to copy the new auth token. The script in the post-response tab will update the <code>auth_token</code> value and you can go on with making API requests smoothly. No need for repeatedly copying and pasting - <strong>Don’t Repeat Yourself (DRY)</strong>.</p>
<h2 id="heading-next-steps">Next Steps</h2>
<p>In this tutorial, you have learnt how to use Postman scripts to set environment variables in Postman. You have also learnt how to eliminate the process of repeatedly copying and pasting auth tokens for use in API requests.</p>
<p>For guides on writing assertion tests for your APIs, check out the <a target="_blank" href="https://learning.postman.com/docs/tests-and-scripts/test-apis/test-apis/">Test API Functionality and Performance in Postman</a> guide by Postman.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Implement Zero-Trust Authentication in Your Web Apps ]]>
                </title>
                <description>
                    <![CDATA[ Your biggest security problem might be inside your own network. Hackers don't break in anymore - they just log in with stolen passwords. Old security systems trusted anyone who got inside the network. But now there's no clear "inside" or "outside." P... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-implement-zero-trust-authentication-in-your-web-apps/</link>
                <guid isPermaLink="false">6893afcc4ff769448b46934a</guid>
                
                    <category>
                        <![CDATA[ zerotrust ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ authentication ]]>
                    </category>
                
                    <category>
                        <![CDATA[ #cybersecurity ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tope Fasasi ]]>
                </dc:creator>
                <pubDate>Wed, 06 Aug 2025 19:41:00 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1754503273007/1b04e262-05de-4fac-be47-56c01eb44446.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Your biggest security problem might be inside your own network. Hackers don't break in anymore - they just log in with stolen passwords. Old security systems trusted anyone who got inside the network. But now there's no clear "inside" or "outside." People work from home, use cloud services, and fall for fake emails. Attackers can pretend to be real users for weeks without being caught.</p>
<p>Zero-Trust Authentication fixes this. Instead of trusting people once they log in, it checks every person, every device, and every request, every single time. The rule is simple: "Trust no one, verify everything."</p>
<p>This isn't just theory – it works. Companies using zero-trust security have smaller breaches, meet compliance rules easier, and control who sees what data. This matters because <a target="_blank" href="https://www.securityweek.com/cost-of-data-breach-in-2024-4-88-million-says-latest-ibm-study/">95% of data breaches happen due to human mistakes, and the average breach now costs $4.88 million</a>.</p>
<p>In this article, you will learn how to build a complete Zero-Trust Authentication system into your web app step by step. From multi-factor authentication (MFA) to behavioral anomaly detection, we will discuss the architecture decisions, code examples, and some real-world approaches you are likely able to implement right away.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-zero-trust-authentication">What Is Zero-Trust Authentication?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-architecture-overview">Architecture Overview</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-multi-factor-authentication-mfa">Multi-factor Authentication (MFA)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-jwt-token-management">JWT Token Management</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-session-security">Session Security</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-role-based-access-control-rbac">Role-Based Access Control (RBAC)</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-using-middleware-to-enforce-rbac">Using Middleware to Enforce RBAC</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-testing-access-control-logic">Testing Access Control Logic</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-continuous-verification">Continuous Verification</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-behavioral-analysis">Behavioral Analysis</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-up-authentication">Step-Up Authentication</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-security-monitoring">Security Monitoring</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-automating-threat-response">Automating Threat Response</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before implementing zero-trust, make sure your stack aligns with frequent calls for token checks, volumes of logging, and the additional auth step, all without impairing system performance on the users' end.</p>
<p>You should at least have knowledge of:</p>
<ul>
<li><p>JWT and secure session handling</p>
</li>
<li><p>MFA, specifically understanding TOTP</p>
</li>
<li><p>Basic understanding of middleware design</p>
</li>
</ul>
<p>Audit your system: examine login flows, token handling, protected routes, session termination, and identify weak spots like long sessions or unprotected routes.</p>
<h2 id="heading-what-is-zero-trust-authentication">What Is Zero-Trust Authentication?</h2>
<p><a target="_blank" href="https://www.civilsdaily.com/news/what-is-zero-trust-authentication-zta/">Zero-Trust Authentication</a> (ZTA) redefines how access is granted in contemporary applications. It doesn't take network location or a single login event into account – it demands the continuous validation of an identity, context, and intent.</p>
<p>Whereas perimeter-based models consider anyone inside a network "safe," zero-trust presumes every request can be compromised. This means that access decisions are made in real time over verified identity, device posture, and behavioral signals. In short, it’s a "security-first" approach designed for a cloud-native, threat-aware world.</p>
<h2 id="heading-architecture-overview">Architecture Overview</h2>
<p>Building a ZTA system means checking everyone and everything, all the time. The architecture you can see below demonstrates this "never trust, always verify" approach in action:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752183554393/4cfda450-14d8-49e3-944b-a0e4654a3dcc.png" alt="Zero Trust Security architecture diagram showing trust boundary encompassing internal network components, with external cloud services and internet connections, illustrating key zero trust principles" class="image--center mx-auto" width="779" height="401" loading="lazy"></p>
<p>Image source: <a target="_blank" href="https://www.civilsdaily.com/news/what-is-zero-trust-authentication-zta/">civilsdaily</a></p>
<p>Here's how it works:</p>
<ul>
<li><p>Every request gets checked: When anyone tries to access your network (from office, home, or mobile), they hit the authentication layer first. No exceptions.</p>
</li>
<li><p>Identity + context verification: The system doesn't just check passwords. It looks at who you are, what device you're using, where you're connecting from, and what you're trying to access.</p>
</li>
<li><p>Continuous protection: Once inside, the system keeps watching. It protects your data, devices, networks, people, and workloads through constant monitoring and access controls.</p>
</li>
<li><p>The big change: Traditional security created a "trusted inside" and "untrusted outside." Zero-trust eliminates this boundary. Whether you're connecting to cloud services (AWS, Office 365) or internal systems, every request goes through the same verification process.</p>
</li>
</ul>
<h2 id="heading-multi-factor-authentication-mfa">Multi-factor Authentication (MFA)</h2>
<p><a target="_blank" href="https://support.microsoft.com/en-gb/topic/what-is-multifactor-authentication-e5e39437-121c-be60-d123-eda06bddf661">MFA</a> is the foundation of zero-trust security. It requires users to prove who they are with multiple pieces of evidence before getting access. In ZTA, even the strongest password isn't enough on its own.</p>
<p>To begin, start with a strong password, then add a second factor. For example, <a target="_blank" href="https://en.wikipedia.org/wiki/Time-based_one-time_password">Time-based One-Time Password (TOTP)</a> is the most secure. TOTP is the best second factor because it works offline and doesn't rely on SMS or email (which can be intercepted). Apps like Google Authenticator generate a new code every 30 seconds.</p>
<p>Here’s an example of what that would look like:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> speakeasy = <span class="hljs-built_in">require</span>(<span class="hljs-string">'speakeasy'</span>);
<span class="hljs-keyword">const</span> QRCode = <span class="hljs-built_in">require</span>(<span class="hljs-string">'qrcode'</span>);

<span class="hljs-comment">// Generate TOTP secret for new user</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">generateTOTPSecret</span>(<span class="hljs-params">userEmail</span>) </span>{
  <span class="hljs-keyword">const</span> secret = speakeasy.generateSecret({
    <span class="hljs-attr">name</span>: userEmail,
    <span class="hljs-attr">issuer</span>: <span class="hljs-string">'YourApp'</span>,
    <span class="hljs-attr">length</span>: <span class="hljs-number">32</span>
  });

  <span class="hljs-keyword">return</span> {
    <span class="hljs-attr">secret</span>: secret.base32,
    <span class="hljs-attr">qrCodeUrl</span>: secret.otpauth_url
  };
}
</code></pre>
<p>When a new user signs up, this function creates a unique secret key just for them. The <code>name</code> is their email, <code>issuer</code> is your app name, and <code>length: 32</code> makes it extra secure. It returns two things: the secret key (in base32 format) and a special URL that creates a QR code for easy setup.</p>
<p>To verify the code from their app, you check it against the stored secret:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Verify TOTP token</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">verifyTOTP</span>(<span class="hljs-params">token, secret</span>) </span>{
  <span class="hljs-keyword">return</span> speakeasy.totp.verify({
    <span class="hljs-attr">secret</span>: secret,
    <span class="hljs-attr">token</span>: token,
    <span class="hljs-attr">window</span>: <span class="hljs-number">2</span>,
    <span class="hljs-attr">encoding</span>: <span class="hljs-string">'base32'</span>
  });
}
</code></pre>
<p>When the user enters their 6-digit code, this function checks if it's correct. The <code>window: 2</code> is smart – it allows for timing differences (like if their phone clock is slightly off). It returns true if the code is valid, false if not.</p>
<p>SMS verification can serve as a backup option. It’s less secure than TOTP but can work as a backup. Always limit how many SMS codes someone can request to prevent abuse:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// SMS verification with rate limiting</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sendSMSVerification</span>(<span class="hljs-params">phoneNumber, userId</span>) </span>{
  <span class="hljs-keyword">const</span> attempts = <span class="hljs-keyword">await</span> getRecentSMSAttempts(userId);
  <span class="hljs-keyword">if</span> (attempts &gt;= <span class="hljs-number">3</span>) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Too many SMS attempts. Please try again later.'</span>);
  }

  <span class="hljs-keyword">const</span> code = generateRandomCode(<span class="hljs-number">6</span>);
  <span class="hljs-keyword">await</span> storeSMSCode(userId, code, <span class="hljs-number">300</span>); <span class="hljs-comment">// 5-minute expiry</span>

  <span class="hljs-keyword">await</span> smsProvider.send(phoneNumber, <span class="hljs-string">`Your verification code: <span class="hljs-subst">${code}</span>`</span>);
}
</code></pre>
<p>Before sending an SMS, it checks how many times this user has already requested codes. If they've tried 3 times, it blocks them (prevents spam/abuse). If they're under the limit, it creates a random 6-digit code, saves it for 5 minutes (300 seconds), then sends it via SMS.</p>
<p>But what happens if a user loses their phone or authenticator app? Backup codes provide emergency access:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Generate backup codes</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">generateBackupCodes</span>(<span class="hljs-params">userId</span>) </span>{
  <span class="hljs-keyword">const</span> codes = [];
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">10</span>; i++) {
    codes.push(generateRandomCode(<span class="hljs-number">8</span>));
  }

  <span class="hljs-keyword">const</span> hashedCodes = codes.map(<span class="hljs-function"><span class="hljs-params">code</span> =&gt;</span> hashCode(code));
  storeBackupCodes(userId, hashedCodes);

  <span class="hljs-keyword">return</span> codes; <span class="hljs-comment">// Only show to user once</span>
}
</code></pre>
<p>This creates 10 emergency backup codes (each 8 characters long). The <code>for</code> loop runs 10 times, creating a new random code each time. Before storing them in the database, it "hashes" them (scrambles them for security). Then it returns the original codes to show the user once, but stores the scrambled versions so even if someone hacks your database, they can't see the real codes.</p>
<h2 id="heading-jwt-token-management">JWT Token Management</h2>
<p>JSON Web Tokens (JWTs) are stateless authentication in a zero-trust system. Using them safely is critical because you need to carefully think through payload design, implement short expiration policies, and implement token rotation and blocklisting that could prevent token theft, token reuse, or privilege escalation.</p>
<p>Let's walk through how to securely implement and manage JWTs in your web application.</p>
<p>First, define a minimal and secure structure for your access tokens. Only add information that’s necessary for making authorization decisions, and never put anything sensitive even if it is encrypted.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// JWT payload structure</span>
<span class="hljs-keyword">const</span> tokenPayload = {
  <span class="hljs-attr">sub</span>: userId,           <span class="hljs-comment">// Subject (user ID)</span>
  <span class="hljs-attr">email</span>: userEmail,      <span class="hljs-comment">// User identifier</span>
  <span class="hljs-attr">roles</span>: userRoles,      <span class="hljs-comment">// User roles array</span>
  <span class="hljs-attr">permissions</span>: userPermissions, <span class="hljs-comment">// Specific permissions</span>
  <span class="hljs-attr">iat</span>: <span class="hljs-built_in">Math</span>.floor(<span class="hljs-built_in">Date</span>.now() / <span class="hljs-number">1000</span>), <span class="hljs-comment">// Issued at</span>
  <span class="hljs-attr">exp</span>: <span class="hljs-built_in">Math</span>.floor(<span class="hljs-built_in">Date</span>.now() / <span class="hljs-number">1000</span>) + <span class="hljs-number">900</span>, <span class="hljs-comment">// Expires in 15 minutes</span>
  <span class="hljs-attr">jti</span>: generateUniqueId(), <span class="hljs-comment">// JWT ID for blocklisting</span>
  <span class="hljs-attr">aud</span>: <span class="hljs-string">'your-app'</span>,       <span class="hljs-comment">// Audience</span>
  <span class="hljs-attr">iss</span>: <span class="hljs-string">'your-auth-service'</span> <span class="hljs-comment">// Issuer</span>
};
</code></pre>
<p>In the code above, the payload consists of the user identity, roles, permissions, and metadata such as the issued time (<code>iat</code>), expiration (<code>exp</code>), and unique token ID (<code>jti</code>). While <code>aud</code> and <code>iss</code> describe the token's origin and audience for validation, <code>jti</code> is used for revocation. Thus, it keeps the payload as lean as possible to minimize exposure and overhead.</p>
<p>For security and usability, it’s better to use access tokens with a short lifespan and refresh tokens with a considerably longer duration, which minimizes the window for potential utilization of compromised tokens while providing a smooth user session.</p>
<p>Let's take this example:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Token generation service</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TokenService</span> </span>{
  generateTokenPair(user) {
    <span class="hljs-keyword">const</span> accessToken = jwt.sign(
      <span class="hljs-built_in">this</span>.createAccessTokenPayload(user),
      process.env.JWT_SECRET,
      { <span class="hljs-attr">expiresIn</span>: <span class="hljs-string">'15m'</span>, <span class="hljs-attr">algorithm</span>: <span class="hljs-string">'HS256'</span> }
    );

    <span class="hljs-keyword">const</span> refreshToken = jwt.sign(
      { <span class="hljs-attr">sub</span>: user.id, <span class="hljs-attr">type</span>: <span class="hljs-string">'refresh'</span> },
      process.env.REFRESH_SECRET,
      { <span class="hljs-attr">expiresIn</span>: <span class="hljs-string">'7d'</span>, <span class="hljs-attr">algorithm</span>: <span class="hljs-string">'HS256'</span> }
    );

    <span class="hljs-keyword">return</span> { accessToken, refreshToken };
  }

  <span class="hljs-keyword">async</span> refreshAccessToken(refreshToken) {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> decoded = jwt.verify(refreshToken, process.env.REFRESH_SECRET);

      <span class="hljs-comment">// Check if refresh token is blocklisted</span>
      <span class="hljs-keyword">if</span> (<span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.isTokenBlocklisted(decoded.jti)) {
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Token has been revoked'</span>);
      }

      <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> getUserById(decoded.sub);
      <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.generateTokenPair(user);
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Invalid refresh token'</span>);
    }
  }
}
</code></pre>
<p><code>generateTokenPair</code> will generate two signed JWTs – that is, an access token with a 15-minute expiration and a refresh token with a validity of 7 days. The refresh tokens are verified to grant new ones and are checked against a blocklist. This ensures that revoked tokens can’t be reused, even if they’re still technically valid.</p>
<p>If you choose, a sliding session can be implemented to reduce friction by renewing tokens for an active user without violating your expiration strategy.</p>
<p>Now, let's implement a <a target="_blank" href="https://stackoverflow.com/questions/48189866/sliding-session-on-web-api-request">sliding session</a> that automatically refreshes JWTs when they're close to expiring and the user is still active.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Sliding session implementation</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">extendSessionIfActive</span>(<span class="hljs-params">token</span>) </span>{
  <span class="hljs-keyword">const</span> decoded = jwt.decode(token);
  <span class="hljs-keyword">const</span> timeUntilExpiry = decoded.exp - <span class="hljs-built_in">Math</span>.floor(<span class="hljs-built_in">Date</span>.now() / <span class="hljs-number">1000</span>);

  <span class="hljs-comment">// If token expires within 5 minutes and user is active, refresh</span>
  <span class="hljs-keyword">if</span> (timeUntilExpiry &lt; <span class="hljs-number">300</span> &amp;&amp; <span class="hljs-keyword">await</span> isUserActive(decoded.sub)) {
    <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> getUserById(decoded.sub);
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.generateTokenPair(user);
  }

  <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
}
</code></pre>
<p>The above function checks for token expiration. If the token expires within 5 minutes and the user continues to interact, a new access token pair is issued. This way, the session is kept alive during real activity but still forces expiration for idle users.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Token blocklist service</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TokenBlocklistService</span> </span>{
  <span class="hljs-keyword">async</span> blocklistToken(token) {
    <span class="hljs-keyword">const</span> decoded = jwt.decode(token);
    <span class="hljs-keyword">const</span> expiresAt = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(decoded.exp * <span class="hljs-number">1000</span>);

    <span class="hljs-comment">// Store in Redis with automatic expiry</span>
    <span class="hljs-keyword">await</span> redis.setex(
      <span class="hljs-string">`blocklist:<span class="hljs-subst">${decoded.jti}</span>`</span>,
      <span class="hljs-built_in">Math</span>.max(<span class="hljs-number">0</span>, <span class="hljs-built_in">Math</span>.floor((expiresAt - <span class="hljs-built_in">Date</span>.now()) / <span class="hljs-number">1000</span>)),
      <span class="hljs-string">'revoked'</span>
    );
  }

  <span class="hljs-keyword">async</span> isTokenBlocklisted(jti) {
    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> redis.get(<span class="hljs-string">`blocklist:<span class="hljs-subst">${jti}</span>`</span>);
    <span class="hljs-keyword">return</span> result !== <span class="hljs-literal">null</span>;
  }
}
</code></pre>
<p>In the above code, when users log out or tokens are compromised, the <code>jti</code> is stored in <a target="_blank" href="https://redis.io/docs/latest/">Redis</a> with an expiration time of the remaining life of the token. You can block future uses of a token by checking if its ID exists on the blocklist. This allows for instant invalidation, even though JWTs are stateless.</p>
<h2 id="heading-session-security">Session Security</h2>
<p>In zero-trust environments, <a target="_blank" href="https://www.descope.com/learn/post/session-management">session management</a> goes far beyond keeping users logged in. A session must be treated as a constantly evaluated contract between the user, their device, and the system – and should be revoked the moment trust breaks down.</p>
<p>Here, we’ll build a session system that incorporates adaptive <a target="_blank" href="https://www.prove.com/blog/trust-score">trust scoring</a>, dynamic timeouts, real-time visibility, and <a target="_blank" href="https://www.researchgate.net/publication/354720916_Revocation_Mechanisms_for_Blockchain_Applications_A_Review">revocation mechanisms</a> – all aligned with zero-trust principles.</p>
<p>For example, when a user successfully authenticates, you don’t just store a session ID. Instead, you collect contextual metadata to evaluate ongoing risk. The function below demonstrates how to initialize a session that’s both secure and context-aware.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Comprehensive session creation</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createSecureSession</span>(<span class="hljs-params">userId, deviceInfo, clientInfo</span>) </span>{
  <span class="hljs-keyword">const</span> sessionId = generateSecureSessionId();

  <span class="hljs-keyword">const</span> session = {
    <span class="hljs-attr">id</span>: sessionId,
    <span class="hljs-attr">userId</span>: userId,
    <span class="hljs-attr">deviceFingerprint</span>: generateDeviceFingerprint(deviceInfo),
    <span class="hljs-attr">ipAddress</span>: clientInfo.ipAddress,
    <span class="hljs-attr">userAgent</span>: clientInfo.userAgent,
    <span class="hljs-attr">location</span>: <span class="hljs-keyword">await</span> resolveLocation(clientInfo.ipAddress),
    <span class="hljs-attr">createdAt</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(),
    <span class="hljs-attr">lastActivity</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(),
    <span class="hljs-attr">trustScore</span>: calculateInitialTrustScore(deviceInfo, clientInfo),
    <span class="hljs-attr">securityLevel</span>: determineSecurityLevel(userId, deviceInfo)
  };

  <span class="hljs-keyword">await</span> storeSession(session);
  <span class="hljs-keyword">return</span> session;
}
</code></pre>
<p>Many other tools are tracking concerning details during session creation. The device fingerprint, IP address, geolocation, and browser agent data are collected. These metadata are used to compute a trust score, and finally, a security level is assigned to the session to be used for dynamically adjusting policies later.</p>
<p>With this contextual information captured during session creation, the system can spot suspicious behavior during the sessions and, in turn, adapt policies like re-authentication of users or termination of the session.</p>
<p>Not all sessions should be treated equally. If a user logs in via an unfamiliar device or risky location, they should have less time for their session lifespan compared to a trusted setup's time. The following implementation changes timeout periods on the basis of trust and risk factors:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Adaptive session timeout</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SessionTimeoutManager</span> </span>{
  calculateTimeoutPeriod(session) {
    <span class="hljs-keyword">const</span> baseTimeout = <span class="hljs-number">30</span> * <span class="hljs-number">60</span> * <span class="hljs-number">1000</span>; <span class="hljs-comment">// 30 minutes</span>
    <span class="hljs-keyword">const</span> trustMultiplier = session.trustScore / <span class="hljs-number">100</span>;
    <span class="hljs-keyword">const</span> securityMultiplier = <span class="hljs-built_in">this</span>.getSecurityMultiplier(session.securityLevel);

    <span class="hljs-keyword">return</span> <span class="hljs-built_in">Math</span>.max(
      <span class="hljs-number">5</span> * <span class="hljs-number">60</span> * <span class="hljs-number">1000</span>, <span class="hljs-comment">// Minimum 5 minutes</span>
      baseTimeout * trustMultiplier * securityMultiplier
    );
  }

  <span class="hljs-keyword">async</span> checkSessionValidity(sessionId) {
    <span class="hljs-keyword">const</span> session = <span class="hljs-keyword">await</span> getSession(sessionId);
    <span class="hljs-keyword">if</span> (!session) <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;

    <span class="hljs-keyword">const</span> now = <span class="hljs-built_in">Date</span>.now();
    <span class="hljs-keyword">const</span> timeout = <span class="hljs-built_in">this</span>.calculateTimeoutPeriod(session);

    <span class="hljs-comment">// Check both idle timeout and absolute timeout</span>
    <span class="hljs-keyword">const</span> idleExpired = (now - session.lastActivity) &gt; timeout;
    <span class="hljs-keyword">const</span> absoluteExpired = (now - session.createdAt) &gt; <span class="hljs-number">8</span> * <span class="hljs-number">60</span> * <span class="hljs-number">60</span> * <span class="hljs-number">1000</span>; <span class="hljs-comment">// 8 hours max</span>

    <span class="hljs-keyword">return</span> !idleExpired &amp;&amp; !absoluteExpired;
  }
}
</code></pre>
<p>The above code keeps session duration adaptable to the risk context at hand. The timeout is calculated by adjusting the base value according to trust and security level, while imposing minimum and maximum bounds.</p>
<p>The system then periodically intervenes to see if the session has become invalid due to inactivity (idle timeout) or simply outlives its initial duration (absolute timeout). This provides a more flexible yet enforceable way of mitigating the risk behind stale or hijacked sessions.</p>
<p>Zero-trust should also mean visibility across all access points. The user should be able to view all active sessions associated with their account, and security systems should also allow them to control these sessions in fine-grained detail. The following code lets you manage those active sessions across devices.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Cross-device session management</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SessionManager</span> </span>{
  <span class="hljs-keyword">async</span> getUserSessions(userId) {
    <span class="hljs-keyword">const</span> sessions = <span class="hljs-keyword">await</span> getActiveSessionsForUser(userId);

    <span class="hljs-keyword">return</span> sessions.map(<span class="hljs-function"><span class="hljs-params">session</span> =&gt;</span> ({
      <span class="hljs-attr">id</span>: session.id,
      <span class="hljs-attr">deviceType</span>: <span class="hljs-built_in">this</span>.identifyDeviceType(session.userAgent),
      <span class="hljs-attr">location</span>: session.location,
      <span class="hljs-attr">lastActivity</span>: session.lastActivity,
      <span class="hljs-attr">current</span>: session.id === currentSessionId
    }));
  }

  <span class="hljs-keyword">async</span> revokeSession(sessionId, requestingSessionId) {
    <span class="hljs-keyword">const</span> session = <span class="hljs-keyword">await</span> getSession(sessionId);
    <span class="hljs-keyword">if</span> (!session) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Session not found'</span>);

    <span class="hljs-comment">// Verify requesting session has permission</span>
    <span class="hljs-keyword">const</span> requestingSession = <span class="hljs-keyword">await</span> getSession(requestingSessionId);
    <span class="hljs-keyword">if</span> (requestingSession.userId !== session.userId) {
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Unauthorized'</span>);
    }

    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.terminateSession(sessionId);
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.logSecurityEvent(<span class="hljs-string">'session_revoked'</span>, session);
  }
}
</code></pre>
<p>Here, users fetch a list of their active sessions along with identifying information such as device type and location. Any session can be securely revoked by the user who owns it, preventing unauthorized access if the session ID is compromised.</p>
<p>This also allows the user to detect suspicious activities in time. All revocations are logged for auditing purposes to enable post-incident investigations as well as compliance reports.</p>
<p>When a trust breaks due to credential theft, suspicious activity, or user-level actions such as password reset, all sessions have to be immediately revoked. This example guarantees a full revocation, promptly applied to all devices:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Real-time session revocation</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SessionRevocationService</span> </span>{
  <span class="hljs-keyword">async</span> revokeAllUserSessions(userId, reason) {
    <span class="hljs-keyword">const</span> sessions = <span class="hljs-keyword">await</span> getActiveSessionsForUser(userId);

    <span class="hljs-comment">// Blocklist all tokens for this user</span>
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(sessions.map(<span class="hljs-function"><span class="hljs-params">session</span> =&gt;</span> 
      <span class="hljs-built_in">this</span>.blocklistSessionTokens(session.id)
    ));

    <span class="hljs-comment">// Notify all active clients</span>
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(sessions.map(<span class="hljs-function"><span class="hljs-params">session</span> =&gt;</span> 
      <span class="hljs-built_in">this</span>.notifySessionTermination(session.id, reason)
    ));

    <span class="hljs-comment">// Clear session data</span>
    <span class="hljs-keyword">await</span> clearUserSessions(userId);

    <span class="hljs-comment">// Log security event</span>
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.logSecurityEvent(<span class="hljs-string">'all_sessions_revoked'</span>, {
      userId,
      reason,
      <span class="hljs-attr">sessionCount</span>: sessions.length
    });
  }
}
</code></pre>
<p>The above code permits full-scale revocation. It blocklists all session tokens, sends out termination notices to active clients (for example, through WebSockets), clears the session records on the server-side, and logs the event for auditing. It is an instantaneous and complete response to compromised accounts or states where user risk is very high. It is the foremost component of real-time zero-trust enforcement in any serious authentication system.</p>
<h2 id="heading-role-based-access-control-rbac">Role-Based Access Control (RBAC)</h2>
<p>Identity verification determines what users can access once they’re logged in. As the basis for any system that is aware of permissions and follows least privilege, <a target="_blank" href="https://en.wikipedia.org/wiki/Role-based_access_control">RBAC</a> doesn’t grant access on an individual basis – it groups users into roles that define the operations they are permitted to perform.</p>
<p>Before assigning roles to users, you need a structured system to define what each role can do. A set of granular permissions is first identified and then aggregated under these roles, optionally allowing inheritance and hierarchy. The code below shows how to build a basic permission system:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// RBAC permission system</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PermissionSystem</span> </span>{
  <span class="hljs-keyword">constructor</span>() {
    <span class="hljs-built_in">this</span>.permissions = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Map</span>();
    <span class="hljs-built_in">this</span>.roles = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Map</span>();
    <span class="hljs-built_in">this</span>.roleHierarchy = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Map</span>();
  }

  <span class="hljs-comment">// Define granular permissions</span>
  definePermission(name, description, resource, action) {
    <span class="hljs-built_in">this</span>.permissions.set(name, {
      name,
      description,
      resource,
      action,
      <span class="hljs-attr">createdAt</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>()
    });
  }

  <span class="hljs-comment">// Create role with inherited permissions</span>
  createRole(name, description, parentRole = <span class="hljs-literal">null</span>) {
    <span class="hljs-keyword">const</span> role = {
      name,
      description,
      <span class="hljs-attr">permissions</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Set</span>(),
      <span class="hljs-attr">createdAt</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>()
    };

    <span class="hljs-comment">// Inherit permissions from parent role</span>
    <span class="hljs-keyword">if</span> (parentRole &amp;&amp; <span class="hljs-built_in">this</span>.roles.has(parentRole)) {
      <span class="hljs-keyword">const</span> parent = <span class="hljs-built_in">this</span>.roles.get(parentRole);
      role.permissions = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Set</span>(parent.permissions);
      <span class="hljs-built_in">this</span>.roleHierarchy.set(name, parentRole);
    }

    <span class="hljs-built_in">this</span>.roles.set(name, role);
    <span class="hljs-keyword">return</span> role;
  }

  <span class="hljs-comment">// Add permission to role</span>
  addPermissionToRole(roleName, permissionName) {
    <span class="hljs-keyword">const</span> role = <span class="hljs-built_in">this</span>.roles.get(roleName);
    <span class="hljs-keyword">if</span> (!role) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Role not found'</span>);

    <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">this</span>.permissions.has(permissionName)) {
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Permission not found'</span>);
    }

    role.permissions.add(permissionName);
  }
}
</code></pre>
<p>The code above lets you specify fine-grained permissions like <code>documents.read.own</code> and organizes them into roles such as <code>employee</code> or <code>manager</code> that you can independently reuse. You can define roles to inherit from other roles, which avoids redundancy and promotes a consistent, scalable access control logic.</p>
<p>As a general rule to avoid privilege creep, permissions should always be as fine-grained as possible. This lets the application refine access decisions to specific actions or scopes: for example, allowing users to read only their documents versus reading all documents for their team.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Fine-grained permission definitions</span>
<span class="hljs-keyword">const</span> permissions = {
  <span class="hljs-comment">// User management</span>
  <span class="hljs-string">'users.read'</span>: { <span class="hljs-attr">resource</span>: <span class="hljs-string">'users'</span>, <span class="hljs-attr">action</span>: <span class="hljs-string">'read'</span> },
  <span class="hljs-string">'users.create'</span>: { <span class="hljs-attr">resource</span>: <span class="hljs-string">'users'</span>, <span class="hljs-attr">action</span>: <span class="hljs-string">'create'</span> },
  <span class="hljs-string">'users.update'</span>: { <span class="hljs-attr">resource</span>: <span class="hljs-string">'users'</span>, <span class="hljs-attr">action</span>: <span class="hljs-string">'update'</span> },
  <span class="hljs-string">'users.delete'</span>: { <span class="hljs-attr">resource</span>: <span class="hljs-string">'users'</span>, <span class="hljs-attr">action</span>: <span class="hljs-string">'delete'</span> },

  <span class="hljs-comment">// Document management</span>
  <span class="hljs-string">'documents.read.own'</span>: { <span class="hljs-attr">resource</span>: <span class="hljs-string">'documents'</span>, <span class="hljs-attr">action</span>: <span class="hljs-string">'read'</span>, <span class="hljs-attr">scope</span>: <span class="hljs-string">'own'</span> },
  <span class="hljs-string">'documents.read.team'</span>: { <span class="hljs-attr">resource</span>: <span class="hljs-string">'documents'</span>, <span class="hljs-attr">action</span>: <span class="hljs-string">'read'</span>, <span class="hljs-attr">scope</span>: <span class="hljs-string">'team'</span> },
  <span class="hljs-string">'documents.read.all'</span>: { <span class="hljs-attr">resource</span>: <span class="hljs-string">'documents'</span>, <span class="hljs-attr">action</span>: <span class="hljs-string">'read'</span>, <span class="hljs-attr">scope</span>: <span class="hljs-string">'all'</span> },
  <span class="hljs-string">'documents.create'</span>: { <span class="hljs-attr">resource</span>: <span class="hljs-string">'documents'</span>, <span class="hljs-attr">action</span>: <span class="hljs-string">'create'</span> },
  <span class="hljs-string">'documents.update.own'</span>: { <span class="hljs-attr">resource</span>: <span class="hljs-string">'documents'</span>, <span class="hljs-attr">action</span>: <span class="hljs-string">'update'</span>, <span class="hljs-attr">scope</span>: <span class="hljs-string">'own'</span> },
  <span class="hljs-string">'documents.delete.own'</span>: { <span class="hljs-attr">resource</span>: <span class="hljs-string">'documents'</span>, <span class="hljs-attr">action</span>: <span class="hljs-string">'delete'</span>, <span class="hljs-attr">scope</span>: <span class="hljs-string">'own'</span> },

  <span class="hljs-comment">// System administration</span>
  <span class="hljs-string">'system.logs.read'</span>: { <span class="hljs-attr">resource</span>: <span class="hljs-string">'system'</span>, <span class="hljs-attr">action</span>: <span class="hljs-string">'read'</span>, <span class="hljs-attr">subresource</span>: <span class="hljs-string">'logs'</span> },
  <span class="hljs-string">'system.config.update'</span>: { <span class="hljs-attr">resource</span>: <span class="hljs-string">'system'</span>, <span class="hljs-attr">action</span>: <span class="hljs-string">'update'</span>, <span class="hljs-attr">subresource</span>: <span class="hljs-string">'config'</span> }
};
</code></pre>
<p>With an array of permissions at its disposal, the app can undertake very precise access control decisions. Instead of merely addressing the binary "is admin" question, this capability enables the system to answer questions such as "can this user delete their own document but not others?"</p>
<p>Static roles are often insufficient. You may want to give people temporary or conditional access, for example, when the team lead takes over for a manager or when a user approves a higher access level for the sake of incident response.</p>
<p>To support these cases, the RBAC system must allow dynamic role assignment – that is, the ability to assign roles on the basis of time, context, or an external trigger such as a security workflow.</p>
<p>The code below assigns a temporary role to a user, notes the exact time at which the role was assigned to the user, and periodically revokes the right after some fixed amount of time. Also, it has a method to calculate a user's complete set of active rights, depending on their permanent rights, temporary rights, and role-based contextual rights.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Dynamic role assignment system</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DynamicRoleAssignment</span> </span>{
  <span class="hljs-keyword">async</span> assignTemporaryRole(userId, roleName, duration, reason) {
    <span class="hljs-keyword">const</span> assignment = {
      userId,
      roleName,
      <span class="hljs-attr">assignedAt</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(),
      <span class="hljs-attr">expiresAt</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(<span class="hljs-built_in">Date</span>.now() + duration * <span class="hljs-number">1000</span>),
      reason,
      <span class="hljs-attr">active</span>: <span class="hljs-literal">true</span>
    };

    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.storeRoleAssignment(assignment);
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.logRoleAssignment(assignment);

    <span class="hljs-comment">// Schedule automatic revocation</span>
    <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
      <span class="hljs-built_in">this</span>.revokeExpiredAssignment(assignment.id);
    }, duration * <span class="hljs-number">1000</span>);

    <span class="hljs-keyword">return</span> assignment;
  }

  <span class="hljs-keyword">async</span> getUserEffectivePermissions(userId, context = {}) {
    <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> getUserById(userId);
    <span class="hljs-keyword">const</span> permanentRoles = user.roles || [];
    <span class="hljs-keyword">const</span> temporaryRoles = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getActiveTemporaryRoles(userId);
    <span class="hljs-keyword">const</span> contextualRoles = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getContextualRoles(userId, context);

    <span class="hljs-keyword">const</span> allRoles = [...permanentRoles, ...temporaryRoles, ...contextualRoles];
    <span class="hljs-keyword">const</span> permissions = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Set</span>();

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> roleName <span class="hljs-keyword">of</span> allRoles) {
      <span class="hljs-keyword">const</span> rolePermissions = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getRolePermissions(roleName);
      rolePermissions.forEach(<span class="hljs-function"><span class="hljs-params">permission</span> =&gt;</span> permissions.add(permission));
    }

    <span class="hljs-keyword">return</span> <span class="hljs-built_in">Array</span>.from(permissions);
  }
}
</code></pre>
<p>This allows for more flexible security configurations. Temporary roles that are granted have an automatic expiration. The context roles may be added dynamically depending on contextual factors such as location or type of device. Permanent roles are combined with temporary and context roles to compute the aggregate permission set for the user on a per-request basis, which maintains flexibility without compromising control.</p>
<h3 id="heading-using-middleware-to-enforce-rbac">Using Middleware to Enforce RBAC</h3>
<p>The RBAC policies have to be enforced before any request reaches a protected route or protected data. <a target="_blank" href="https://aws.amazon.com/what-is/middleware/">Middleware</a> is a good place to run such checks in the scope of a web application. We’ll now look into how the reusable middleware function for authorization works.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Authorization middleware</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createAuthorizationMiddleware</span>(<span class="hljs-params">requiredPermission</span>) </span>{
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">async</span> (req, res, next) =&gt; {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-comment">// Extract user from validated JWT</span>
      <span class="hljs-keyword">const</span> user = req.user;
      <span class="hljs-keyword">if</span> (!user) {
        <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">401</span>).json({ <span class="hljs-attr">error</span>: <span class="hljs-string">'Authentication required'</span> });
      }

      <span class="hljs-comment">// Get user's effective permissions</span>
      <span class="hljs-keyword">const</span> context = {
        <span class="hljs-attr">ipAddress</span>: req.ip,
        <span class="hljs-attr">userAgent</span>: req.get(<span class="hljs-string">'User-Agent'</span>),
        <span class="hljs-attr">resourceId</span>: req.params.id,
        <span class="hljs-attr">timestamp</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>()
      };

      <span class="hljs-keyword">const</span> permissions = <span class="hljs-keyword">await</span> roleSystem.getUserEffectivePermissions(
        user.id,
        context
      );

      <span class="hljs-comment">// Check if user has required permission</span>
      <span class="hljs-keyword">if</span> (!permissions.includes(requiredPermission)) {
        <span class="hljs-keyword">await</span> logUnauthorizedAccess(user.id, requiredPermission, context);
        <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">403</span>).json({ <span class="hljs-attr">error</span>: <span class="hljs-string">'Insufficient permissions'</span> });
      }

      <span class="hljs-comment">// Add permissions to request for downstream use</span>
      req.userPermissions = permissions;
      next();
    } <span class="hljs-keyword">catch</span> (error) {
      res.status(<span class="hljs-number">500</span>).json({ <span class="hljs-attr">error</span>: <span class="hljs-string">'Authorization check failed'</span> });
    }
  };
}

<span class="hljs-comment">// Usage in routes</span>
app.get(<span class="hljs-string">'/api/users'</span>, 
  authenticateToken,
  createAuthorizationMiddleware(<span class="hljs-string">'users.read'</span>),
  getUsersController
);
</code></pre>
<p>In the code above, the middleware will validate user identities in real-time, check if adequate permissions are granted, and allow or deny access accordingly. It’s a central mechanism for enforcing access rules in a uniform way across your routes, and it even records unauthorized attempts for auditing.</p>
<h3 id="heading-testing-access-control-logic">Testing Access Control Logic</h3>
<p>Once you’ve implemented the RBAC system, testing becomes a must. You want to guarantee that permissions are inherited properly, that access is actually denied when a user isn’t authorized, and that your roles behave as designed in the real world as well as in edge-case scenarios.</p>
<p>The following example uses a testing framework to demonstrate the verification of two fundamental behaviors: inheritance of permissions from parent roles and rejection of unauthorized access.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// RBAC testing suite</span>
describe(<span class="hljs-string">'RBAC System'</span>, <span class="hljs-function">() =&gt;</span> {
  test(<span class="hljs-string">'should inherit permissions from parent roles'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> manager = <span class="hljs-keyword">await</span> roleSystem.createRole(<span class="hljs-string">'manager'</span>, <span class="hljs-string">'Team Manager'</span>, <span class="hljs-string">'employee'</span>);
    <span class="hljs-keyword">await</span> roleSystem.addPermissionToRole(<span class="hljs-string">'manager'</span>, <span class="hljs-string">'team.manage'</span>);

    <span class="hljs-keyword">const</span> permissions = <span class="hljs-keyword">await</span> roleSystem.getRolePermissions(<span class="hljs-string">'manager'</span>);
    expect(permissions).toContain(<span class="hljs-string">'documents.read.own'</span>); <span class="hljs-comment">// From employee</span>
    expect(permissions).toContain(<span class="hljs-string">'team.manage'</span>); <span class="hljs-comment">// Manager-specific</span>
  });

  test(<span class="hljs-string">'should deny access without proper permissions'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> user = { <span class="hljs-attr">id</span>: <span class="hljs-number">1</span>, <span class="hljs-attr">roles</span>: [<span class="hljs-string">'employee'</span>] };
    <span class="hljs-keyword">const</span> req = { user, <span class="hljs-attr">params</span>: { <span class="hljs-attr">id</span>: <span class="hljs-string">'doc123'</span> } };
    <span class="hljs-keyword">const</span> res = { <span class="hljs-attr">status</span>: jest.fn().mockReturnThis(), <span class="hljs-attr">json</span>: jest.fn() };

    <span class="hljs-keyword">const</span> middleware = createAuthorizationMiddleware(<span class="hljs-string">'documents.delete.all'</span>);
    <span class="hljs-keyword">await</span> middleware(req, res, <span class="hljs-function">() =&gt;</span> {}); <span class="hljs-comment">// Middleware call simulating request</span>

    expect(res.status).toHaveBeenCalledWith(<span class="hljs-number">403</span>);
  });
});
</code></pre>
<p>The tests represent the positive and negative validations of the access rules. The first test determines whether inherited permissions flow freely from the parent to child roles. The second test blocks any user without the required permission, returning a status code appropriately.</p>
<p>Over time, you can enrich test coverage to include temporary role assignments, contextual conditions, and session-aware behavior to alert you to any regressions before they start affecting production access.</p>
<h2 id="heading-continuous-verification">Continuous Verification</h2>
<p>Modern access security is not a one-shot check but an ongoing process. A strong system must continuously verify user identity and context throughout the ongoing session while adapting to newly emerging risk signals.</p>
<p>In <a target="_blank" href="https://spot.io/resources/gitops/continuous-verification/">continuous verification</a>, it’s an assurance that access stays appropriate while the user behavior, device posture, or environment changes mid-session.</p>
<p>To uniquely identify a device, you can combine subtle traits like browser settings, hardware specs, and plugin data. This forms a device “fingerprint,” which helps flag new or suspicious devices attempting access.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Advanced device fingerprinting</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DeviceFingerprintService</span> </span>{
  generateFingerprint(deviceInfo) {
    <span class="hljs-keyword">const</span> components = [
      deviceInfo.userAgent,
      deviceInfo.screenResolution,
      deviceInfo.timezone,
      deviceInfo.language,
      deviceInfo.platform,
      deviceInfo.hardwareConcurrency,
      deviceInfo.memorySize,
      deviceInfo.availableFonts?.join(<span class="hljs-string">','</span>),
      deviceInfo.plugins?.map(<span class="hljs-function"><span class="hljs-params">p</span> =&gt;</span> p.name).join(<span class="hljs-string">','</span>),
      deviceInfo.webglRenderer,
      deviceInfo.audioContext
    ];

    <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.hashComponents(components);
  }

  calculateTrustScore(currentFingerprint, knownFingerprints) {
    <span class="hljs-keyword">if</span> (knownFingerprints.length === <span class="hljs-number">0</span>) <span class="hljs-keyword">return</span> <span class="hljs-number">50</span>; <span class="hljs-comment">// Neutral for new device</span>
    <span class="hljs-keyword">const</span> similarities = knownFingerprints.map(<span class="hljs-function"><span class="hljs-params">known</span> =&gt;</span>
      <span class="hljs-built_in">this</span>.calculateSimilarity(currentFingerprint, known)
    );
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">Math</span>.min(<span class="hljs-number">100</span>, <span class="hljs-built_in">Math</span>.max(...similarities) * <span class="hljs-number">100</span>);
  }

  <span class="hljs-keyword">async</span> updateDeviceTrust(userId, deviceFingerprint, securityEvents) {
    <span class="hljs-keyword">const</span> device = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getOrCreateDevice(userId, deviceFingerprint);
    <span class="hljs-keyword">let</span> trustAdjustment = <span class="hljs-number">0</span>;

    securityEvents.forEach(<span class="hljs-function"><span class="hljs-params">event</span> =&gt;</span> {
      <span class="hljs-keyword">switch</span> (event.type) {
        <span class="hljs-keyword">case</span> <span class="hljs-string">'successful_login'</span>: trustAdjustment += <span class="hljs-number">5</span>; <span class="hljs-keyword">break</span>;
        <span class="hljs-keyword">case</span> <span class="hljs-string">'failed_login'</span>: trustAdjustment -= <span class="hljs-number">10</span>; <span class="hljs-keyword">break</span>;
        <span class="hljs-keyword">case</span> <span class="hljs-string">'suspicious_activity'</span>: trustAdjustment -= <span class="hljs-number">25</span>; <span class="hljs-keyword">break</span>;
      }
    });

    device.trustScore = <span class="hljs-built_in">Math</span>.max(<span class="hljs-number">0</span>, <span class="hljs-built_in">Math</span>.min(<span class="hljs-number">100</span>, device.trustScore + trustAdjustment));
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.updateDevice(device);
    <span class="hljs-keyword">return</span> device.trustScore;
  }
}
</code></pre>
<p>Generating a fingerprint hash from device traits, this service uses historical events to dynamically adjust the device's trust score. Step-up authentication may be prompted by low scores, or access may be denied altogether.</p>
<h3 id="heading-behavioral-analysis">Behavioral Analysis</h3>
<p>People tend to use apps rather consistently – they type a certain way, move the mouse in a particular manner, or browse varied content. <a target="_blank" href="https://zimperium.com/glossary/behavioral-analysis">Behavioral analysis</a> tries to detect that anomaly by comparing ongoing activities to known ones.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Behavioral analysis system</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BehaviorAnalysisService</span> </span>{
  <span class="hljs-keyword">async</span> analyzeUserBehavior(userId, currentSession) {
    <span class="hljs-keyword">const</span> historicalBehavior = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getUserBehaviorProfile(userId);
    <span class="hljs-keyword">const</span> anomalies = [];

    <span class="hljs-keyword">const</span> typingAnomaly = <span class="hljs-built_in">this</span>.analyzeTypingPatterns(
      currentSession.typingData,
      historicalBehavior.typingProfile
    );
    <span class="hljs-keyword">if</span> (typingAnomaly.score &gt; <span class="hljs-number">0.7</span>) {
      anomalies.push({ <span class="hljs-attr">type</span>: <span class="hljs-string">'typing_pattern'</span>, <span class="hljs-attr">score</span>: typingAnomaly.score, <span class="hljs-attr">details</span>: typingAnomaly.details });
    }

    <span class="hljs-keyword">const</span> navigationAnomaly = <span class="hljs-built_in">this</span>.analyzeNavigationPatterns(
      currentSession.navigationData,
      historicalBehavior.navigationProfile
    );
    <span class="hljs-keyword">if</span> (navigationAnomaly.score &gt; <span class="hljs-number">0.6</span>) {
      anomalies.push({ <span class="hljs-attr">type</span>: <span class="hljs-string">'navigation_pattern'</span>, <span class="hljs-attr">score</span>: navigationAnomaly.score, <span class="hljs-attr">details</span>: navigationAnomaly.details });
    }

    <span class="hljs-keyword">const</span> timeAnomaly = <span class="hljs-built_in">this</span>.analyzeTimePatterns(
      currentSession.timestamp,
      historicalBehavior.timeProfile
    );
    <span class="hljs-keyword">if</span> (timeAnomaly.score &gt; <span class="hljs-number">0.5</span>) {
      anomalies.push({ <span class="hljs-attr">type</span>: <span class="hljs-string">'time_pattern'</span>, <span class="hljs-attr">score</span>: timeAnomaly.score, <span class="hljs-attr">details</span>: timeAnomaly.details });
    }

    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">overallRiskScore</span>: <span class="hljs-built_in">this</span>.calculateOverallRisk(anomalies),
      anomalies,
      <span class="hljs-attr">recommendations</span>: <span class="hljs-built_in">this</span>.generateRecommendations(anomalies)
    };
  }

  analyzeTypingPatterns(currentData, historicalProfile) {
    <span class="hljs-keyword">if</span> (!currentData || !historicalProfile) <span class="hljs-keyword">return</span> { <span class="hljs-attr">score</span>: <span class="hljs-number">0</span> };
    <span class="hljs-keyword">const</span> dwellTimeVariance = <span class="hljs-built_in">this</span>.calculateVariance(currentData.dwellTimes, historicalProfile.averageDwellTime);
    <span class="hljs-keyword">const</span> flightTimeVariance = <span class="hljs-built_in">this</span>.calculateVariance(currentData.flightTimes, historicalProfile.averageFlightTime);
    <span class="hljs-keyword">const</span> score = <span class="hljs-built_in">Math</span>.max(dwellTimeVariance, flightTimeVariance);
    <span class="hljs-keyword">return</span> { score, <span class="hljs-attr">details</span>: { dwellTimeVariance, flightTimeVariance, <span class="hljs-attr">sampleSize</span>: currentData.keystrokes.length } };
  }
}
</code></pre>
<p>This will detect suspicious changes in user behavior and typing characteristics as early warning indicators of session hijacking or insider threat.</p>
<p>Access from a new country or city can either be harmless or highly suspicious. Comparing login geography against historical patterns helps flag impossible travel or access from banned regions.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Location-based access control</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LocationAccessControl</span> </span>{
  <span class="hljs-keyword">async</span> validateLocationAccess(userId, ipAddress, session) {
    <span class="hljs-keyword">const</span> location = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.resolveLocation(ipAddress);
    <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> getUserById(userId);
    <span class="hljs-keyword">const</span> historicalLocations = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getUserLocations(userId);
    <span class="hljs-keyword">const</span> locationRisk = <span class="hljs-built_in">this</span>.assessLocationRisk(location, historicalLocations);

    <span class="hljs-keyword">const</span> lastLocation = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getLastKnownLocation(userId);
    <span class="hljs-keyword">if</span> (lastLocation) {
      <span class="hljs-keyword">const</span> impossibleTravel = <span class="hljs-built_in">this</span>.checkImpossibleTravel(lastLocation, location, session.lastActivity);
      <span class="hljs-keyword">if</span> (impossibleTravel.detected) {
        <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.logSecurityEvent(<span class="hljs-string">'impossible_travel'</span>, {
          userId, <span class="hljs-attr">fromLocation</span>: lastLocation, <span class="hljs-attr">toLocation</span>: location,
          <span class="hljs-attr">timeWindow</span>: impossibleTravel.timeWindow,
          <span class="hljs-attr">minimumTravelTime</span>: impossibleTravel.minimumTravelTime
        });
        <span class="hljs-keyword">return</span> { <span class="hljs-attr">allowed</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">reason</span>: <span class="hljs-string">'impossible_travel'</span>, <span class="hljs-attr">requiresStepUp</span>: <span class="hljs-literal">true</span> };
      }
    }

    <span class="hljs-keyword">if</span> (user.allowedCountries &amp;&amp; !user.allowedCountries.includes(location.country)) {
      <span class="hljs-keyword">return</span> { <span class="hljs-attr">allowed</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">reason</span>: <span class="hljs-string">'country_restriction'</span>, <span class="hljs-attr">requiresStepUp</span>: <span class="hljs-literal">true</span> };
    }

    <span class="hljs-keyword">const</span> highRiskCountries = [<span class="hljs-string">'XX'</span>, <span class="hljs-string">'YY'</span>, <span class="hljs-string">'ZZ'</span>];
    <span class="hljs-keyword">if</span> (highRiskCountries.includes(location.country)) {
      <span class="hljs-keyword">return</span> { <span class="hljs-attr">allowed</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">reason</span>: <span class="hljs-string">'high_risk_location'</span>, <span class="hljs-attr">requiresStepUp</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">additionalVerification</span>: [<span class="hljs-string">'sms'</span>, <span class="hljs-string">'email'</span>] };
    }

    <span class="hljs-keyword">return</span> { <span class="hljs-attr">allowed</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">riskScore</span>: locationRisk, location };
  }

  checkImpossibleTravel(fromLocation, toLocation, lastActivity) {
    <span class="hljs-keyword">const</span> distance = <span class="hljs-built_in">this</span>.calculateDistance(fromLocation, toLocation);
    <span class="hljs-keyword">const</span> timeElapsed = <span class="hljs-built_in">Date</span>.now() - lastActivity;
    <span class="hljs-keyword">const</span> maximumSpeed = <span class="hljs-number">900</span>; <span class="hljs-comment">// km/h</span>
    <span class="hljs-keyword">const</span> minimumTravelTime = (distance / maximumSpeed) * <span class="hljs-number">3600000</span>;
    <span class="hljs-keyword">return</span> { <span class="hljs-attr">detected</span>: timeElapsed &lt; minimumTravelTime, <span class="hljs-attr">timeWindow</span>: timeElapsed, minimumTravelTime, distance };
  }
}
</code></pre>
<p>This logic prevents abuse via VPNs or stolen credentials by requiring step-up verification when impossible travel or unusual locations are detected.</p>
<h3 id="heading-step-up-authentication">Step-Up Authentication</h3>
<p><a target="_blank" href="https://doubleoctopus.com/security-wiki/authentication/step-up-authentication/">Step-up security</a> introduces friction only when truly needed. With lower risk considered, users move freely. When risk levels rises, they're asked for stronger proofs, such as biometrics or hardware tokens.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Step-up authentication system</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">StepUpAuthenticationService</span> </span>{
  <span class="hljs-keyword">async</span> evaluateStepUpRequirement(userId, requestContext, resourceSensitivity) {
    <span class="hljs-keyword">const</span> riskFactors = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.calculateRiskFactors(userId, requestContext);
    <span class="hljs-keyword">const</span> stepUpRequired = <span class="hljs-built_in">this</span>.shouldRequireStepUp(riskFactors, resourceSensitivity);

    <span class="hljs-keyword">if</span> (stepUpRequired.required) {
      <span class="hljs-keyword">return</span> {
        <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">methods</span>: <span class="hljs-built_in">this</span>.selectAuthenticationMethods(riskFactors, stepUpRequired.level),
        <span class="hljs-attr">expiresIn</span>: <span class="hljs-built_in">this</span>.calculateStepUpDuration(stepUpRequired.level),
        <span class="hljs-attr">reason</span>: stepUpRequired.reason
      };
    }

    <span class="hljs-keyword">return</span> { <span class="hljs-attr">required</span>: <span class="hljs-literal">false</span> };
  }

  <span class="hljs-keyword">async</span> calculateRiskFactors(userId, context) {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">deviceTrust</span>: <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getDeviceTrustScore(userId, context.deviceFingerprint),
      <span class="hljs-attr">locationRisk</span>: <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getLocationRiskScore(userId, context.ipAddress),
      <span class="hljs-attr">behaviorAnomaly</span>: <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getBehaviorAnomalyScore(userId, context.sessionData),
      <span class="hljs-attr">timeSinceLastAuth</span>: <span class="hljs-built_in">Date</span>.now() - context.lastAuthTime,
      <span class="hljs-attr">resourceSensitivity</span>: context.resourceSensitivity || <span class="hljs-string">'medium'</span>
    };
  }

  shouldRequireStepUp(riskFactors, sensitivity) {
    <span class="hljs-keyword">let</span> score = <span class="hljs-number">0</span>;
    <span class="hljs-keyword">if</span> (riskFactors.deviceTrust &lt; <span class="hljs-number">70</span>) score += <span class="hljs-number">30</span>;
    <span class="hljs-keyword">if</span> (riskFactors.deviceTrust &lt; <span class="hljs-number">40</span>) score += <span class="hljs-number">20</span>;
    <span class="hljs-keyword">if</span> (riskFactors.locationRisk &gt; <span class="hljs-number">0.6</span>) score += <span class="hljs-number">25</span>;
    <span class="hljs-keyword">if</span> (riskFactors.locationRisk &gt; <span class="hljs-number">0.8</span>) score += <span class="hljs-number">15</span>;
    <span class="hljs-keyword">if</span> (riskFactors.behaviorAnomaly &gt; <span class="hljs-number">0.5</span>) score += <span class="hljs-number">20</span>;
    <span class="hljs-keyword">if</span> (riskFactors.behaviorAnomaly &gt; <span class="hljs-number">0.7</span>) score += <span class="hljs-number">10</span>;
    <span class="hljs-keyword">const</span> hours = riskFactors.timeSinceLastAuth / (<span class="hljs-number">1000</span> * <span class="hljs-number">60</span> * <span class="hljs-number">60</span>);
    <span class="hljs-keyword">if</span> (hours &gt; <span class="hljs-number">8</span>) score += <span class="hljs-number">10</span>;
    <span class="hljs-keyword">if</span> (hours &gt; <span class="hljs-number">24</span>) score += <span class="hljs-number">15</span>;

    score *= { <span class="hljs-attr">low</span>: <span class="hljs-number">0.7</span>, <span class="hljs-attr">medium</span>: <span class="hljs-number">1.0</span>, <span class="hljs-attr">high</span>: <span class="hljs-number">1.3</span>, <span class="hljs-attr">critical</span>: <span class="hljs-number">1.6</span> }[sensitivity] || <span class="hljs-number">1.0</span>;

    <span class="hljs-keyword">if</span> (score &gt;= <span class="hljs-number">80</span>) <span class="hljs-keyword">return</span> { <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">level</span>: <span class="hljs-string">'high'</span>, <span class="hljs-attr">reason</span>: <span class="hljs-string">'high_risk_detected'</span> };
    <span class="hljs-keyword">if</span> (score &gt;= <span class="hljs-number">50</span>) <span class="hljs-keyword">return</span> { <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">level</span>: <span class="hljs-string">'medium'</span>, <span class="hljs-attr">reason</span>: <span class="hljs-string">'moderate_risk_detected'</span> };
    <span class="hljs-keyword">if</span> (score &gt;= <span class="hljs-number">25</span>) <span class="hljs-keyword">return</span> { <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">level</span>: <span class="hljs-string">'low'</span>, <span class="hljs-attr">reason</span>: <span class="hljs-string">'low_risk_detected'</span> };
    <span class="hljs-keyword">return</span> { <span class="hljs-attr">required</span>: <span class="hljs-literal">false</span> };
  }

  selectAuthenticationMethods(riskFactors, level) {
    <span class="hljs-keyword">const</span> methods = [];
    <span class="hljs-keyword">if</span> (level === <span class="hljs-string">'high'</span>) {
      methods.push(<span class="hljs-string">'hardware_token'</span>, <span class="hljs-string">'biometric'</span>);
      <span class="hljs-keyword">if</span> (riskFactors.deviceTrust &lt; <span class="hljs-number">30</span>) methods.push(<span class="hljs-string">'admin_approval'</span>);
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (level === <span class="hljs-string">'medium'</span>) {
      methods.push(<span class="hljs-string">'totp'</span>, <span class="hljs-string">'sms'</span>);
      <span class="hljs-keyword">if</span> (riskFactors.locationRisk &gt; <span class="hljs-number">0.7</span>) methods.push(<span class="hljs-string">'email_verification'</span>);
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (level === <span class="hljs-string">'low'</span>) {
      methods.push(<span class="hljs-string">'totp'</span>);
    }
    <span class="hljs-keyword">return</span> methods;
  }
}
</code></pre>
<p>The service uses this balancing technique between critical resources and risks while keeping normal workflows intact when things look safe.</p>
<h2 id="heading-security-monitoring">Security Monitoring</h2>
<p>Security monitoring provides the observability layer that’s essential for detecting, analyzing, and responding to threats in real time. A strong system must log every authentication event, highlight anomalies, and allow for rapid and automated response to threats. This phase further builds trust by constantly evaluating access patterns and acting on them when signals of risk emerge.</p>
<p>Logging is visibility at its base. These days, every authentication attempt, be it successful, failed, or suspicious, needs to be logged with exhaustive context. This very information helps forensic analysis, alerting, and compliance reporting.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Comprehensive authentication event logging</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthenticationLogger</span> </span>{
  <span class="hljs-keyword">async</span> logAuthenticationEvent(eventType, userId, context, result) {
    <span class="hljs-keyword">const</span> logEntry = {
      <span class="hljs-attr">timestamp</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toISOString(),
      eventType,
      userId,
      <span class="hljs-attr">sessionId</span>: context.sessionId,
      <span class="hljs-attr">ipAddress</span>: context.ipAddress,
      <span class="hljs-attr">userAgent</span>: context.userAgent,
      <span class="hljs-attr">deviceFingerprint</span>: context.deviceFingerprint,
      <span class="hljs-attr">location</span>: context.location,
      <span class="hljs-attr">authenticationMethod</span>: context.authMethod,
      <span class="hljs-attr">result</span>: result.success ? <span class="hljs-string">'success'</span> : <span class="hljs-string">'failure'</span>,
      <span class="hljs-attr">failureReason</span>: result.failureReason,
      <span class="hljs-attr">riskScore</span>: result.riskScore,
      <span class="hljs-attr">additionalFactorsRequired</span>: result.stepUpRequired,
      <span class="hljs-attr">processingTime</span>: result.processingTime,
      <span class="hljs-attr">correlationId</span>: context.correlationId
    };

    <span class="hljs-comment">// Store in multiple destinations for redundancy</span>
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all([
      <span class="hljs-built_in">this</span>.writeToDatabase(logEntry),
      <span class="hljs-built_in">this</span>.sendToLogAggregator(logEntry),
      <span class="hljs-built_in">this</span>.updateRealTimeMetrics(logEntry)
    ]);

    <span class="hljs-comment">// Trigger real-time alerts for critical events</span>
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.isCriticalEvent(logEntry)) {
      <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.triggerSecurityAlert(logEntry);
    }
  }

  isCriticalEvent(logEntry) {
    <span class="hljs-keyword">const</span> criticalConditions = [
      logEntry.result === <span class="hljs-string">'failure'</span> &amp;&amp; logEntry.failureReason === <span class="hljs-string">'brute_force_detected'</span>,
      logEntry.riskScore &gt; <span class="hljs-number">80</span>,
      logEntry.eventType === <span class="hljs-string">'impossible_travel_detected'</span>,
      logEntry.eventType === <span class="hljs-string">'account_takeover_suspected'</span>
    ];

    <span class="hljs-keyword">return</span> criticalConditions.some(<span class="hljs-function"><span class="hljs-params">condition</span> =&gt;</span> condition);
  }

  <span class="hljs-keyword">async</span> generateSecurityReport(userId, timeRange) {
    <span class="hljs-keyword">const</span> events = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getAuthenticationEvents(userId, timeRange);

    <span class="hljs-keyword">const</span> analysis = {
      <span class="hljs-attr">totalEvents</span>: events.length,
      <span class="hljs-attr">successfulLogins</span>: events.filter(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> e.result === <span class="hljs-string">'success'</span>).length,
      <span class="hljs-attr">failedAttempts</span>: events.filter(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> e.result === <span class="hljs-string">'failure'</span>).length,
      <span class="hljs-attr">uniqueDevices</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Set</span>(events.map(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> e.deviceFingerprint)).size,
      <span class="hljs-attr">uniqueLocations</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Set</span>(events.map(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> e.location?.country)).size,
      <span class="hljs-attr">averageRiskScore</span>: events.reduce(<span class="hljs-function">(<span class="hljs-params">sum, e</span>) =&gt;</span> sum + e.riskScore, <span class="hljs-number">0</span>) / events.length,
      <span class="hljs-attr">timePatterns</span>: <span class="hljs-built_in">this</span>.analyzeTimePatterns(events),
      <span class="hljs-attr">locationPatterns</span>: <span class="hljs-built_in">this</span>.analyzeLocationPatterns(events),
      <span class="hljs-attr">devicePatterns</span>: <span class="hljs-built_in">this</span>.analyzeDevicePatterns(events)
    };

    <span class="hljs-keyword">return</span> analysis;
  }
}
</code></pre>
<p>In the above code, the class logs detailed authentication events such as the approximate device and location from which it was initiated, the authentication methods used, and the risk score.</p>
<p>From a security perspective, it’s envisaged to generate security reports with the advantage of flagging critical events such as brute-force attempts or logins from suspicious geographies that can send real-time alerts.</p>
<p>Monitoring authentication events isn’t enough – the system must be able to interpret patterns and flag suspicious behavior. This detection system combines static rule-based checks with dynamic anomaly detection powered by machine learning. It identifies threats like brute-force attacks, credential stuffing, and unusual geographic access, then escalates them automatically for further action.</p>
<p>The following code performs real-time threat detection by analyzing recent authentication events and contextual data. Here's what it does, broken down clearly:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Suspicious activity detection system</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SuspiciousActivityDetector</span> </span>{
  <span class="hljs-keyword">constructor</span>() {
    <span class="hljs-built_in">this</span>.detectionRules = <span class="hljs-built_in">this</span>.initializeDetectionRules();
    <span class="hljs-built_in">this</span>.mlModel = <span class="hljs-built_in">this</span>.loadAnomalyDetectionModel();
  }

  <span class="hljs-keyword">async</span> analyzeActivity(userId, recentEvents, context) {
    <span class="hljs-keyword">const</span> suspiciousPatterns = [];

    <span class="hljs-comment">// Rule-based detection</span>
    <span class="hljs-keyword">const</span> ruleViolations = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.checkDetectionRules(userId, recentEvents);
    suspiciousPatterns.push(...ruleViolations);

    <span class="hljs-comment">// ML-based anomaly detection</span>
    <span class="hljs-keyword">const</span> anomalies = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.detectAnomalies(userId, recentEvents, context);
    suspiciousPatterns.push(...anomalies);

    <span class="hljs-comment">// Threat intelligence correlation</span>
    <span class="hljs-keyword">const</span> threatMatches = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.correlateThreatIntelligence(context);
    suspiciousPatterns.push(...threatMatches);

    <span class="hljs-keyword">if</span> (suspiciousPatterns.length &gt; <span class="hljs-number">0</span>) {
      <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.escalateSuspiciousActivity(userId, suspiciousPatterns);
    }

    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">suspicious</span>: suspiciousPatterns.length &gt; <span class="hljs-number">0</span>,
      <span class="hljs-attr">patterns</span>: suspiciousPatterns,
      <span class="hljs-attr">riskScore</span>: <span class="hljs-built_in">this</span>.calculateSuspiciousActivityRisk(suspiciousPatterns)
    };
  }

  initializeDetectionRules() {
    <span class="hljs-keyword">return</span> [
      {
        <span class="hljs-attr">name</span>: <span class="hljs-string">'brute_force_detection'</span>,
        <span class="hljs-attr">condition</span>: <span class="hljs-function">(<span class="hljs-params">events</span>) =&gt;</span> {
          <span class="hljs-keyword">const</span> failedAttempts = events.filter(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span>
            e.result === <span class="hljs-string">'failure'</span> &amp;&amp;
            <span class="hljs-built_in">Date</span>.now() - <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(e.timestamp).getTime() &lt; <span class="hljs-number">300000</span> <span class="hljs-comment">// 5 minutes</span>
          );
          <span class="hljs-keyword">return</span> failedAttempts.length &gt;= <span class="hljs-number">5</span>;
        },
        <span class="hljs-attr">severity</span>: <span class="hljs-string">'high'</span>,
        <span class="hljs-attr">action</span>: <span class="hljs-string">'temporary_lockout'</span>
      },
      {
        <span class="hljs-attr">name</span>: <span class="hljs-string">'credential_stuffing'</span>,
        <span class="hljs-attr">condition</span>: <span class="hljs-function">(<span class="hljs-params">events</span>) =&gt;</span> {
          <span class="hljs-keyword">const</span> recentFailures = events.filter(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span>
            e.result === <span class="hljs-string">'failure'</span> &amp;&amp;
            <span class="hljs-built_in">Date</span>.now() - <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(e.timestamp).getTime() &lt; <span class="hljs-number">3600000</span> <span class="hljs-comment">// 1 hour</span>
          );
          <span class="hljs-keyword">const</span> uniqueUsernames = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Set</span>(recentFailures.map(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> e.username));
          <span class="hljs-keyword">return</span> uniqueUsernames.size &gt;= <span class="hljs-number">10</span>;
        },
        <span class="hljs-attr">severity</span>: <span class="hljs-string">'medium'</span>,
        <span class="hljs-attr">action</span>: <span class="hljs-string">'rate_limiting'</span>
      },
      {
        <span class="hljs-attr">name</span>: <span class="hljs-string">'suspicious_location_pattern'</span>,
        <span class="hljs-attr">condition</span>: <span class="hljs-function">(<span class="hljs-params">events</span>) =&gt;</span> {
          <span class="hljs-keyword">const</span> locations = events.map(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> e.location?.country).filter(<span class="hljs-built_in">Boolean</span>);
          <span class="hljs-keyword">const</span> uniqueCountries = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Set</span>(locations);
          <span class="hljs-keyword">return</span> uniqueCountries.size &gt;= <span class="hljs-number">3</span> &amp;&amp; events.length &gt;= <span class="hljs-number">5</span>;
        },
        <span class="hljs-attr">severity</span>: <span class="hljs-string">'medium'</span>,
        <span class="hljs-attr">action</span>: <span class="hljs-string">'enhanced_verification'</span>
      }
    ];
  }

  <span class="hljs-keyword">async</span> detectAnomalies(userId, events, context) {
    <span class="hljs-keyword">const</span> features = <span class="hljs-built_in">this</span>.extractFeatures(events, context);
    <span class="hljs-keyword">const</span> anomalyScore = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.mlModel.predict(features);

    <span class="hljs-keyword">if</span> (anomalyScore &gt; <span class="hljs-number">0.7</span>) {
      <span class="hljs-keyword">return</span> [{
        <span class="hljs-attr">type</span>: <span class="hljs-string">'ml_anomaly'</span>,
        <span class="hljs-attr">score</span>: anomalyScore,
        <span class="hljs-attr">features</span>: features,
        <span class="hljs-attr">description</span>: <span class="hljs-string">'Machine learning model detected anomalous behavior pattern'</span>
      }];
    }

    <span class="hljs-keyword">return</span> [];
  }
}
</code></pre>
<p>This class applies multiple techniques to detect threats. It first evaluates authentication history using static rules for brute-force attempts, large-scale credential reuse, or location anomalies. It then passes <a target="_blank" href="https://www.fullstory.com/blog/behavioral-data/">behavioral data</a> through a trained ML model to spot subtle patterns missed by rules. If any suspicious pattern is detected, it returns a structured risk report and initiates escalation.</p>
<h3 id="heading-automating-threat-response">Automating Threat Response</h3>
<p>Most times, systems respond in real-time. Automated threat response follows predefined actions and includes locking an account, alerting users, or blocking an IP, among others, when a high-risk event occurs.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Automated threat response system</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AutomatedThreatResponse</span> </span>{
  <span class="hljs-keyword">constructor</span>() {
    <span class="hljs-built_in">this</span>.responsePlaybooks = <span class="hljs-built_in">this</span>.initializeResponsePlaybooks();
    <span class="hljs-built_in">this</span>.escalationPolicies = <span class="hljs-built_in">this</span>.loadEscalationPolicies();
  }

  <span class="hljs-keyword">async</span> processSecurityEvent(event) {
    <span class="hljs-keyword">const</span> threatLevel = <span class="hljs-built_in">this</span>.assessThreatLevel(event);
    <span class="hljs-keyword">const</span> applicablePlaybooks = <span class="hljs-built_in">this</span>.selectPlaybooks(event, threatLevel);

    <span class="hljs-keyword">const</span> responses = [];
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> playbook <span class="hljs-keyword">of</span> applicablePlaybooks) {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.executePlaybook(playbook, event);
      responses.push(response);
    }

    <span class="hljs-keyword">if</span> (threatLevel === <span class="hljs-string">'critical'</span> || responses.some(<span class="hljs-function"><span class="hljs-params">r</span> =&gt;</span> !r.success)) {
      <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.escalateToHuman(event, responses);
    }

    <span class="hljs-keyword">return</span> {
      event,
      threatLevel,
      responses,
      <span class="hljs-attr">timestamp</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>()
    };
  }

  initializeResponsePlaybooks() {
    <span class="hljs-keyword">return</span> [
      {
        <span class="hljs-attr">name</span>: <span class="hljs-string">'brute_force_response'</span>,
        <span class="hljs-attr">triggers</span>: [<span class="hljs-string">'brute_force_detected'</span>],
        <span class="hljs-attr">actions</span>: [
          { <span class="hljs-attr">type</span>: <span class="hljs-string">'temporary_lockout'</span>, <span class="hljs-attr">duration</span>: <span class="hljs-number">900</span> },
          { <span class="hljs-attr">type</span>: <span class="hljs-string">'rate_limiting'</span>, <span class="hljs-attr">factor</span>: <span class="hljs-number">10</span> },
          { <span class="hljs-attr">type</span>: <span class="hljs-string">'notify_user'</span>, <span class="hljs-attr">method</span>: <span class="hljs-string">'email'</span> },
          { <span class="hljs-attr">type</span>: <span class="hljs-string">'log_security_event'</span>, <span class="hljs-attr">level</span>: <span class="hljs-string">'high'</span> }
        ]
      },
      {
        <span class="hljs-attr">name</span>: <span class="hljs-string">'account_takeover_response'</span>,
        <span class="hljs-attr">triggers</span>: [<span class="hljs-string">'impossible_travel'</span>, <span class="hljs-string">'behavior_anomaly_high'</span>],
        <span class="hljs-attr">actions</span>: [
          { <span class="hljs-attr">type</span>: <span class="hljs-string">'terminate_all_sessions'</span> },
          { <span class="hljs-attr">type</span>: <span class="hljs-string">'require_password_reset'</span> },
          { <span class="hljs-attr">type</span>: <span class="hljs-string">'notify_user'</span>, <span class="hljs-attr">method</span>: <span class="hljs-string">'multiple'</span> },
          { <span class="hljs-attr">type</span>: <span class="hljs-string">'freeze_account'</span>, <span class="hljs-attr">duration</span>: <span class="hljs-number">7200</span> }
        ]
      }
    ];
  }

  <span class="hljs-keyword">async</span> executePlaybook(playbook, event) {
    <span class="hljs-keyword">const</span> execution = {
      <span class="hljs-attr">playbookName</span>: playbook.name,
      <span class="hljs-attr">eventId</span>: event.id,
      <span class="hljs-attr">actions</span>: [],
      <span class="hljs-attr">success</span>: <span class="hljs-literal">true</span>
    };

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> action <span class="hljs-keyword">of</span> playbook.actions) {
      <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.executeAction(action, event);
        execution.actions.push(result);
        <span class="hljs-keyword">if</span> (!result.success) {
          execution.success = <span class="hljs-literal">false</span>;
          <span class="hljs-keyword">break</span>;
        }
      } <span class="hljs-keyword">catch</span> (err) {
        execution.success = <span class="hljs-literal">false</span>;
        execution.error = err.message;
      }
    }

    <span class="hljs-keyword">return</span> execution;
  }

  <span class="hljs-keyword">async</span> executeAction(action, event) {
    <span class="hljs-keyword">switch</span> (action.type) {
      <span class="hljs-keyword">case</span> <span class="hljs-string">'temporary_lockout'</span>:
        <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.lockoutUser(event.userId, action.duration);
        <span class="hljs-keyword">return</span> { <span class="hljs-attr">success</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">type</span>: action.type };
      <span class="hljs-keyword">case</span> <span class="hljs-string">'notify_user'</span>:
        <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.notifyUser(event.userId, action.method, event);
        <span class="hljs-keyword">return</span> { <span class="hljs-attr">success</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">type</span>: action.type };
      <span class="hljs-keyword">default</span>:
        <span class="hljs-keyword">return</span> { <span class="hljs-attr">success</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">type</span>: action.type, <span class="hljs-attr">error</span>: <span class="hljs-string">'Unknown action'</span> };
    }
  }
}
</code></pre>
<p>Here, the system uses playbooks – predefined actions to be taken in response to threats. For example, locks user from further brute-force attempts for some time and sends them an email notification. Freezing the account and ending all sessions are some reactive measures you can take if suspicious behavior indicates a takeover. These measures ensure fast and consistent action to mitigate damage even before humans can get involved.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Zero-trust authentication creates a strong line of distinction going against classic perimeter-based security. It must be painstakingly planned, implemented in layers, and constantly improved. This article offers a structured path, from basic MFA to intelligent behavioral monitoring and automated threat response.</p>
<p>Complementing the improvement of security, zero-trust promises better user experience, compliance readiness, and decreased incident risk. When organizations maintain a perpetual position of zero trust, we can see an actual positive impact on their ability to detect, prevent, and respond to threats in real time.</p>
<p>To have long-term success with this approach, you’ll need to continuously monitor your setup, perform periodic assessments, and be responsive to evolving attack patterns. Feedback loops and performance data are essential to keep the system secure yet user-friendly.</p>
<p>As threats grow more sophisticated, so must our defenses. ZTA provides a durable foundation – ready to evolve with emerging technologies like adaptive biometrics and AI-driven risk engines. Organizations investing in it today will be better equipped to meet tomorrow’s security and usability demands.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ What Are JSON Web Tokens (JWT)? ]]>
                </title>
                <description>
                    <![CDATA[ When you’re working with any website, application, or API, you'll inevitably need to log in and authenticate your user base. One of the more commonly used methods of passing around authentication credentials from one system to another is using a JSON... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/what-are-json-web-tokens-jwt/</link>
                <guid isPermaLink="false">686c430b1a4ac707c0692874</guid>
                
                    <category>
                        <![CDATA[ JWT ]]>
                    </category>
                
                    <category>
                        <![CDATA[ authentication ]]>
                    </category>
                
                    <category>
                        <![CDATA[ authorization ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Tutorial ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Grant Riordan ]]>
                </dc:creator>
                <pubDate>Mon, 07 Jul 2025 21:58:35 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1751819356361/352ef68a-fa20-4a69-b666-393f7a17fa40.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>When you’re working with any website, application, or API, you'll inevitably need to log in and authenticate your user base. One of the more commonly used methods of passing around authentication credentials from one system to another is using a JSON Web Token (JWT).</p>
<p>In this article, you'll learn about:</p>
<ul>
<li><p>What a JSON Web Token (JWT) is</p>
</li>
<li><p>How JWTs are structured and created</p>
</li>
<li><p>Different JWT signing techniques and algorithms (Symmetric vs. Asymmetric)</p>
</li>
<li><p>How JWTs are used in real-world authentication flows</p>
</li>
<li><p>Important security best practices for using JWTs</p>
</li>
</ul>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-a-jwt">What is a JWT?</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-json-web-tokens-are-made-of-three-elements">JSON Web Tokens Are Made Of Three Elements</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-asymmetric-signing-rs256-explained">Asymmetric Signing (RS256) Explained</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-analogy-the-lock-and-key-for-asymmetric-signatures">Analogy: The Lock and Key for Asymmetric Signatures</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-symmetric-signing-hs256-hmac-with-sha-256">Symmetric Signing: HS256 (HMAC with SHA-256)</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-how-hs256-verification-works">How HS256 Verification Works</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-jwts-in-action-a-typical-authentication-flow">JWTs in Action: A Typical Authentication Flow</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-jwt-security-best-practices-amp-considerations">JWT Security Best Practices &amp; Considerations</a></p>
</li>
</ul>
<h2 id="heading-what-is-a-jwt">What Is a JWT?</h2>
<blockquote>
<p>JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.<br>— Introduction to JWT by jwt.io</p>
</blockquote>
<p>While accurate, this definition can be a bit dense at first glance. Imagine you want to send someone a sealed, tamper-proof message. That's essentially what a JSON Web Token (JWT) is. It's a secure message, a special kind of message designed to be sent between two parties which can be assured it came from an expected sender.</p>
<p>Each JWT is digitally signed using either a <strong>secret code</strong> (for symmetric algorithms like HMAC) or a <strong>private key</strong> (for asymmetric algorithms like RSA or ECDSA).</p>
<p>This secret code or private key is known only to the system that issues the JWT (often called an <em>authentication provider</em>, like Auth0, AWS Cognito, or Firebase Auth, which handles user logins and identity).</p>
<p>This signature proves two things:</p>
<ol>
<li><p><strong>Authenticity:</strong> It proves the message really came from who it claims to be from.</p>
</li>
<li><p><strong>Integrity:</strong> It proves that the message hasn't been changed or tampered with since it was signed. If even one character is altered, the signature won't match, and you'll know something is wrong, meaning the contents of the JWT can't be trusted.</p>
</li>
</ol>
<h3 id="heading-json-web-tokens-are-made-of-three-elements">JSON Web Tokens Are Made of Three Elements</h3>
<p>JWTs are made up of 3 key parts:</p>
<ol>
<li><p>Header</p>
</li>
<li><p>Payload</p>
</li>
<li><p>Signature</p>
</li>
</ol>
<h4 id="heading-header">Header</h4>
<p>The header contains metadata information about the token. Think of it like a label on a package – it tells you what’s inside and how it was prepared.</p>
<p>Typically, the header contains:</p>
<p><code>alg</code>: This specifies the <strong>algorithm</strong> used to sign the JWT. Common algorithms are <code>HS256</code> (HMAC with SHA-256) or <code>RS256</code> (RSA with SHA-256).</p>
<p><code>typ</code>: This specifies the <strong>type</strong> of token, which is almost always <code>JWT</code> for standard JSON Web Tokens.</p>
<p><strong>Example (decoded):</strong></p>
<pre><code class="lang-json">{ 
  <span class="hljs-attr">"alg"</span>: <span class="hljs-string">"RS256"</span>,
  <span class="hljs-attr">"typ"</span>: <span class="hljs-string">"JWT"</span> 
}
</code></pre>
<h4 id="heading-payload">Payload</h4>
<p>This is the second part of the JWT, and it's where the real data or "claims" are stored. Claims are statements about an entity (usually a user) and any other additional data. Using the previous analogy of a package, think of it as the “contents” of the package.</p>
<p>There are three types of claims:</p>
<p><strong>1. Registered Claims:</strong> These are predefined claims that are recommended for common use cases. They are not mandatory but are very useful for interoperability. These include:</p>
<ul>
<li><p><code>iss</code> – issuer, who issued the token (for example, your application’s domain)</p>
</li>
<li><p><code>sub</code> – subject, the subject of the token (for example, a User’s ID)</p>
</li>
<li><p><code>aud</code> – audience, the audience of the token (that is, who the token is intended for – for example, a specific API)</p>
</li>
<li><p><code>exp</code> – <strong>expiration</strong>, the expiry date as a timestamp</p>
</li>
<li><p><code>iat</code> – issued at, when the token was issued as a timestamp</p>
</li>
<li><p><code>nbf</code> – not before, when the token becomes valid (that is, the token cannot be used or deemed valid before this timestamp)</p>
</li>
<li><p><code>jti</code> – JWT ID, a unique identifier for the token, useful for preventing replay attacks or blacklisting</p>
</li>
</ul>
<p><strong>2. Public Claims:</strong> These can be defined by anyone using JWTs. To avoid naming conflicts, it's a good practice to register them or define them using a unique identifier like a URI.</p>
<p><strong>3. Private Claims:</strong> These are custom claims created to share specific information between parties who agree on using them. They are entirely up to you and your application's needs.</p>
<p><strong>Example payload:</strong></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"sub"</span>: <span class="hljs-string">"1234567890"</span>, <span class="hljs-comment">//  subject</span>
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"John Doe"</span>, <span class="hljs-comment">// private claim</span>
  <span class="hljs-attr">"admin"</span>: <span class="hljs-literal">true</span>, <span class="hljs-comment">// private claim / role</span>
  <span class="hljs-attr">"iat"</span>: <span class="hljs-number">1678886400</span>, <span class="hljs-comment">// Issued at a specific timestamp</span>
  <span class="hljs-attr">"exp"</span>: <span class="hljs-number">1678890000</span>  <span class="hljs-comment">// Expires at a specific timestamp</span>
}
</code></pre>
<p>Like the header, this JSON object is also <strong>Base64Url encoded</strong> (a URL-safe variant of Base64 encoding) to form the second part of the JWT string.</p>
<p><strong>Important Note:</strong> <em>The payload is</em> <strong><em>encoded*</em></strong>, not encrypted.* This means that anyone can easily decode the JWT and read its contents. Never put sensitive information (like passwords) directly into the payload unless the entire JWT itself is encrypted (which is a separate process called JWE - JSON Web Encryption). The security of a standard JWT comes entirely from the signature, which prevents tampering.</p>
<h4 id="heading-signature">Signature</h4>
<p>The signature, as we've already discussed, is the most important part of the JWT. Without it, there's no protection applied to the JWT, meaning no way to validate the origin of the token or its integrity.</p>
<p>The signature is created by taking the <strong>encoded header</strong>, the <strong>encoded payload</strong>, and a <strong>secret key</strong> (or a private key if using asymmetric algorithms like RSA). These are then run through the cryptographic algorithm specified in the header (<code>alg</code> field). For HS256, a shared secret key is used. For RS256, a private key is used to sign, and a corresponding public key is used to verify. We’ll get on to verification soon.</p>
<p>Think of it like a tamper-proof seal on your package, or even better, a wax seal on a letter. If you receive your letter and the wax seal has been broken, you'd naturally believe the contents of the letter may not be original and therefore can't be trusted.</p>
<p>In pseudo-code it would look like this:</p>
<p><code>Signature = Algorithm( Base64Url(Header) + "." + Base64Url(Payload), SecretKey )</code></p>
<p>The result of this signing process is the signature, which is also Base64Url encoded to form the third part of the JWT string.</p>
<p>At the end of the whole process your JWT would look like this:</p>
<p><code>base64EncodedHeader.base64EncodedPayload.base64EncodedSignature</code></p>
<h3 id="heading-asymmetric-signing-rs256-explained">Asymmetric Signing (RS256) Explained</h3>
<p>When a JWT uses an algorithm like RS256 (RSA Signature with SHA-256), it employs an <strong>asymmetric cryptographic</strong> process involving a <strong>public</strong> and <strong>private</strong> key pair. This is where the core magic of proving authenticity and integrity happens without needing to share a secret.</p>
<h4 id="heading-the-signing-process-by-the-issuer">The Signing Process (by the Issuer)</h4>
<p>The <strong>sender</strong> (the server that issues the JWT, like Auth0) possesses the <strong>private key</strong>. This key is kept absolutely secret and secure. Here are the steps:</p>
<ol>
<li><p><strong>Prepare the data:</strong> The server takes the header (which includes the algorithm) and the payload. It Base64Url-encodes them, and then concatenates them with a dot: <code>Base64Url(Header) + "." + Base64Url(Payload)</code>.</p>
<p> For example, with this header:</p>
<pre><code class="lang-json"> {
   <span class="hljs-attr">"typ"</span>: <span class="hljs-string">"JWT"</span>,
   <span class="hljs-attr">"alg"</span>: <span class="hljs-string">"RS256"</span>
 }
</code></pre>
<p> And this payload:</p>
<pre><code class="lang-json"> {
   <span class="hljs-attr">"sub"</span>: <span class="hljs-string">"1234567890"</span>,
   <span class="hljs-attr">"name"</span>: <span class="hljs-string">"John Doe"</span>,
   <span class="hljs-attr">"admin"</span>: <span class="hljs-literal">true</span>,
   <span class="hljs-attr">"iat"</span>: <span class="hljs-number">1751494086</span>,
   <span class="hljs-attr">"exp"</span>: <span class="hljs-number">1751497686</span>
 }
</code></pre>
<p> This would create a Base64Url-encoded <code>header.payload</code> string like the animated example below:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751810671075/128fa652-fe2a-4413-a238-71531bfe67ae.gif" alt="Animated gif of the process converting header and payload to base64 encoded string" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
</li>
<li><p><strong>Calculate the hash:</strong> It then calculates a <strong>hash</strong> (using SHA-256 in this case) of this combined header and payload string.</p>
</li>
<li><p><strong>Sign the hash:</strong> Finally, it signs this hash using its private key. This cryptographically transformed hash is the signature part of the JWT.</p>
</li>
</ol>
<p>The JWT is then formed by concatenating the Base64Url-encoded header, the Base64Url-encoded payload, and the Base64Url-encoded signature, separated by dots: <code>header.payload.signature</code>.</p>
<p>The top segment below shows the full JWT token (header.payload.signature):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751830473890/31da21a0-50db-4a48-b4c3-378c2ac1616a.png" alt="Image displaying the fully formed JWT token along with original header and payload" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-the-verification-process">The Verification Process</h3>
<p>This is where the magic happens, and it's often a point of confusion. The public key doesn't "decrypt" the original data like a symmetric key does. Instead, it performs a unique <strong>verification</strong> process.</p>
<p>The receiver (the client or another server that needs to verify the JWT) possesses the <strong>public key</strong>. This key does <strong>not</strong> need to be kept secret – it can be freely distributed.</p>
<p>Here's a step-by-step explanation:</p>
<ol>
<li><p><strong>Separate the parts:</strong> The first thing the receiver does is split the incoming JWT string into its three Base64Url-encoded components: the Header, the Payload, and the Signature.</p>
</li>
<li><p><strong>Obtain the public key:</strong> The verifier needs the <strong>public key</strong> that corresponds to the private key used by the issuer. Public keys are often available via a <strong>JWKS (JSON Web Key Set) endpoint</strong> (for example, <code>your-domain.com/.well-known/jwks.json</code>).</p>
</li>
<li><p><strong>Re-create the data to be hashed:</strong> The receiver takes the received, Base64Url-encoded Header and the received, Base64Url-encoded Payload. It then combines them exactly as the issuer did: <code>EncodedHeader.EncodedPayload</code>.</p>
</li>
<li><p><strong>Compute a local hash (Hash A):</strong> This combined string is then put through the same hashing algorithm (for example, SHA-256) that was specified in the JWT's header. This produces a new, locally computed hash (let's call this <strong>"Hash A"</strong>). This local hash represents what the content <em>should</em> look like if it hasn't been tampered with.</p>
</li>
<li><p><strong>"Unsign" the received signature with the public key to get the original signed hash (Hash B):</strong> This is the core cryptographic step. The verifier uses the public key (obtained in step 2) to perform a mathematical operation on the received signature. This operation does <em>not</em> create a new signature for comparison. Instead, it effectively "unsigns" or "decrypts" the signature to reveal the <strong>original hash ("Hash B")</strong> that was produced by the issuer's private key.</p>
<ul>
<li><strong>Crucial Point:</strong> This process is for <strong>verifying authenticity</strong>, not decrypting confidential data. The public key confirms that the signature was indeed created by the corresponding private key, and as part of that confirmation, it returns the original hash that <em>was signed</em>.</li>
</ul>
</li>
<li><p><strong>Compare the hashes:</strong> The verifier now has two hashes:</p>
<ul>
<li><p><strong>Hash A:</strong> The hash it <strong>computed locally</strong> from the received header and payload (from step 4).</p>
</li>
<li><p><strong>Hash B:</strong> The original hash that was extracted from the received Signature using the public key (from step 5)</p>
</li>
</ul>
</li>
<li><p><strong>If Hash A matches Hash B:</strong> It proves two critical things:</p>
<ol>
<li><p><strong>Authenticity:</strong> The token was indeed signed by the legitimate holder of the corresponding private key (for example, Auth0).</p>
</li>
<li><p><strong>Integrity:</strong> The content of the header and payload has <strong>not been tampered with</strong> since it was originally signed. If even a single character in the header or payload were changed, Hash A would be different, and it would not match Hash B. In this case, the JWT is considered valid and its contents can be trusted.</p>
</li>
</ol>
</li>
<li><p><strong>If the hashes do NOT match:</strong> The token is considered invalid and <strong>must be rejected</strong>. This indicates either that the JWT was signed by an unauthorised party (a forged token) or that its header or payload has been altered after it was signed.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751813812291/de32c7b0-1f3c-4f26-995e-b5c618b104b3.png" alt=" Flow diagram of asymmetric verification process" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-analogy-the-lock-and-key-for-asymmetric-signatures">Analogy: The Lock and Key for Asymmetric Signatures</h3>
<p><strong>Private Key:</strong> A special, unique key that can <strong>lock</strong> a box (create a signature). Only the owner has this key.</p>
<p><strong>Public Key:</strong> A widely distributed key that can <strong>test</strong> if a box was locked by the corresponding private key. It can't lock a new box, but it can confirm if an existing lock is authentic.</p>
<p>You don't re-lock the box with the public key. You use the public key to check if the existing lock (the signature) is genuine and corresponds to the contents of the box.</p>
<h2 id="heading-symmetric-signing-hs256-hmac-with-sha-256">Symmetric Signing: HS256 (HMAC With SHA-256)</h2>
<p>While RS256 uses a pair of keys (private for signing, public for verifying), many JWTs you'll encounter are signed symmetrically, most commonly with the HS256 algorithm. HS256 stands for <strong>HMAC (Hash-based Message Authentication Code) with SHA-256</strong>.</p>
<p>The fundamental difference here is the use of a single, shared secret key for <em>both</em> signing and verification.</p>
<h3 id="heading-how-hs256-signing-works">How HS256 Signing Works</h3>
<ol>
<li><p><strong>Shared secret key:</strong> The issuer (for example, your authentication provider) possesses a single, confidential secret key. This key is known <em>only</em> to the issuer and any parties (like your API) that need to verify the token.</p>
</li>
<li><p><strong>Combine header and payload:</strong> Just like with asymmetric signing, the issuer takes the Base64Url-encoded Header (which specifies <code>"alg": "HS256"</code>) and the Base64Url-encoded Payload, and <strong>joins</strong> them with a dot.</p>
</li>
<li><p><strong>Apply HMAC-SHA256:</strong> This combined string is then fed into the HMAC-SHA256 algorithm along with the secret key. The HMAC algorithm uses the secret key to create a unique hash (the signature) of the data. In pseudo-code, it looks like this:</p>
<p> <code>Signature = HMAC-SHA256( Base64Url(Header) + "." + Base64Url(Payload), SecretKey )</code></p>
</li>
<li><p><strong>Form the JWT:</strong> The resulting signature (which is also Base64Url-encoded) is appended to the header and payload with a dot, forming the complete JWT: <code>base64EncodedHeader.base64EncodedPayload.base64EncodedSignature</code>.</p>
</li>
</ol>
<h3 id="heading-how-hs256-verification-works">How HS256 Verification Works</h3>
<p>When a receiver gets an HS256-signed JWT, it goes through a verification process.</p>
<p>First, it separates the parts. The JWT is split into its three Base64Url-encoded components: Header, Payload, and Signature, as we did with asymmetric JWTs.</p>
<p>Then, it obtains the shared secret key. The receiver must also possess the <strong>exact same secret key</strong> that the issuer used to sign the token. This key is <em>not</em> publicly distributed like a public key – it must be securely provisioned to any entity that needs to verify tokens.</p>
<p>Next, it re-calculates the signature. The receiver does this by taking the received Base64Url-encoded Header and Payload, combining them, and then re-applying the HMAC-SHA256 algorithm using the <em>same secret key</em>. This produces a new, locally computed signature.</p>
<p>Finally, the receiver compares the signature it just calculated locally with the signature it received as part of the JWT.</p>
<ul>
<li><p><strong>If the two signatures match:</strong> The token is considered valid. This confirms its authenticity (it came from someone who knows the secret) and integrity (it hasn't been tampered with).</p>
</li>
<li><p><strong>If the signatures do NOT match:</strong> The token is invalid and must be rejected. This indicates either tampering or that it was signed with a different, unknown secret key.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751814851136/a73b7af6-e92d-40f3-b1e3-c4bd2406ede9.png" alt="Flow diagram of symmetric verification process" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-key-differences-and-considerations">Key Differences and Considerations:</h3>
<ul>
<li><p><strong>Key management:</strong> With HS256, the secret key must be securely shared and kept confidential by <em>all</em> parties involved in both signing and verifying. This can be more challenging to manage securely at scale compared to the public/private key model, where only the private key needs strict secrecy.</p>
</li>
<li><p><strong>Performance:</strong> HS256 is generally faster to compute than asymmetric algorithms like RS256, making it suitable for high-volume scenarios where the secret key can be securely distributed.</p>
</li>
</ul>
<h2 id="heading-jwts-in-action-a-typical-authentication-flow">JWTs in Action: A Typical Authentication Flow</h2>
<p>Now that you understand how JWTs are structured and signed, let's look at how they're typically used in a real-world web application. This authentication flow is a common pattern you'd encounter.</p>
<h3 id="heading-step-1-user-logs-in"><strong>Step 1: User Logs In:</strong></h3>
<p>A user opens a client application (for example, a web browser, mobile app) and enters their login credentials (username and password).</p>
<p>The client sends these credentials securely (always over HTTPS!) to an <strong>authentication server</strong> (like Auth0, AWS Cognito, or your own backend's authentication endpoint).</p>
<h3 id="heading-step-2-authentication-server-issues-jwt"><strong>Step 2: Authentication Server Issues JWT:</strong></h3>
<p>Then the authentication server verifies the user's credentials. If valid, it generates a new JWT. This JWT contains claims (like the user's ID, roles, expiration time) in its payload and is digitally signed by the server's <strong>private key</strong> (for asymmetric algorithms like RS256) or <strong>secret key</strong> (for symmetric algorithms like HS256).</p>
<p>The server then sends this signed JWT back to the client.</p>
<h3 id="heading-step-3-client-stores-jwt"><strong>Step 3: Client Stores JWT:</strong></h3>
<p>The client receives the JWT and typically stores it in a secure location, such as browser memory storage, session storage, or an HTTP-only cookie. The method of storage depends on the client type and security considerations.</p>
<h3 id="heading-step-4-client-makes-api-calls"><strong>Step 4: Client Makes API Calls:</strong></h3>
<p>When the user wants to access a protected resource on a backend API (for example, their profile data, a private feed), the client includes the JWT in the request.</p>
<p>The standard way to do this is by sending the token in the <code>Authorization</code> header of the HTTP request, prefixed with the word <code>Bearer</code>:</p>
<p><code>Authorization: Bearer &lt;your_jwt_here&gt;</code></p>
<h3 id="heading-step-5-api-verifies-jwt-amp-authorises-request"><strong>Step 5: API Verifies JWT &amp; Authorises Request:</strong></h3>
<p>Now, the backend API receives the request and extracts the JWT from the <code>Authorization</code> header. The API then performs the JWT verification process depending on the algorithm:</p>
<ul>
<li><p>It checks the token's claims, especially the <code>exp</code> (expiration) claim, to ensure it's still valid.</p>
</li>
<li><p>If the token is valid, the API trusts the claims within the payload (for example, the user's ID) and proceeds to fulfill the request, potentially using the user's roles to determine if they have permission to access the requested resource.</p>
</li>
<li><p>If the token is invalid (bad signature, expired, and so on), the API rejects the request, typically with an HTTP 401 Unauthorised status.</p>
</li>
</ul>
<p>This flow is powerful because JWTs are <strong>stateless</strong>: once issued, the authentication server doesn't need to keep a record of active sessions. The API can verify the token independently, which simplifies scaling and reduces server load.</p>
<h2 id="heading-jwt-security-best-practices-and-considerations">JWT Security Best Practices and Considerations</h2>
<p>While JWTs offer powerful authentication capabilities, using them securely requires careful attention to best practices. Misconfigurations or oversight can lead to significant vulnerabilities.</p>
<h3 id="heading-always-use-httpstls"><strong>Always Use HTTPS/TLS:</strong></h3>
<p><strong>Crucial:</strong> JWTs are <strong>encoded, not encrypted, by default</strong>. This means anyone who intercepts the token during transmission can easily read its payload. Therefore, JWTs (and all authentication traffic) <strong>must always be transmitted over HTTPS (TLS)</strong> to encrypt the communication channel itself and prevent eavesdropping.</p>
<h3 id="heading-protect-your-signing-keys"><strong>Protect Your Signing Keys:</strong></h3>
<p>Whether it's a private key (for RS256) or a shared secret key (for HS256), these keys are paramount. If an attacker gains access to your signing key, they can forge valid JWTs, impersonate users, and compromise your system. Store these keys securely, preferably in dedicated key management services.</p>
<h3 id="heading-keep-access-tokens-short-lived-exp-claim"><strong>Keep Access Tokens Short-Lived (</strong><code>exp</code> claim):</h3>
<p>You should always set short expiration times (for example, 5-15 minutes) for your JWTs used as access tokens. This minimises the window of opportunity for an attacker if a token is compromised.</p>
<p>Since JWTs are stateless, they are hard to revoke immediately once issued. A short lifespan is your primary defense against compromised tokens.</p>
<h3 id="heading-implement-refresh-tokens-for-longer-sessions"><strong>Implement Refresh Tokens (for Longer Sessions):</strong></h3>
<p>To maintain user experience with short-lived access tokens, use <strong>refresh tokens</strong>. A refresh token is a separate, longer-lived token (usually stored more securely) that can be exchanged for a new, short-lived access token when the current one expires, without requiring the user to re-authenticate. Refresh tokens <em>can</em> be revoked by the server, offering better control.</p>
<h3 id="heading-never-put-sensitive-data-in-the-payload"><strong>Never Put Sensitive Data in the Payload:</strong></h3>
<p>Reiterating this crucial point: the JWT payload is Base64Url encoded, which is easily reversible. Do not put passwords, highly sensitive PII (Personally Identifiable Information), or confidential business data directly into the JWT payload. Only include non-sensitive or publicly available information, or data that's already encrypted by other means.</p>
<h3 id="heading-validate-all-claims-on-verification"><strong>Validate ALL Claims on Verification:</strong></h3>
<p>When verifying a JWT, don't just check the signature. Always validate all relevant claims, including:</p>
<ul>
<li><p><code>exp</code> (Expiration): Ensure the token hasn't expired.</p>
</li>
<li><p><code>iss</code> (Issuer): Verify the token came from the expected authentication server.</p>
</li>
<li><p><code>aud</code> (Audience): Ensure the token is intended for your specific API/application.</p>
</li>
<li><p><code>nbf</code> (Not Before): Check if the token is active yet.</p>
</li>
</ul>
<h3 id="heading-consider-token-revocation-for-critical-cases"><strong>Consider Token Revocation (for critical cases):</strong></h3>
<p>For situations requiring immediate revocation (for example, user password change, account deactivation), typical stateless JWTs are challenging. Strategies include:</p>
<ul>
<li><p>Short expiration times (as above).</p>
</li>
<li><p>A blacklist/revocation list: Store the <code>jti</code> (JWT ID) of revoked tokens in a database, checking this list on every request. This adds a stateful lookup but provides immediate revocation.</p>
</li>
</ul>
<h2 id="heading-thanks-for-reading">Thanks for reading!</h2>
<p>I hope you’ve found this tutorial useful, and as always if you want to ask any questions or hear about upcoming articles, you can always follow me on ‘X’, my handle is @grantdotdev and follow by clicking <a target="_blank" href="https://x.com/grantdotdev">here</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build Secure SSR Authentication with Supabase, Astro, and Cloudflare Turnstile ]]>
                </title>
                <description>
                    <![CDATA[ In this guide, you'll build a full server-side rendered (SSR) authentication system using Astro, Supabase, and Cloudflare Turnstile to protect against bots. By the end, you'll have a fully functional authentication system with Astro actions, magic li... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-secure-ssr-authentication-with-supabase-astro-and-cloudflare-turnstile/</link>
                <guid isPermaLink="false">685594145aea0dba325c37e1</guid>
                
                    <category>
                        <![CDATA[ supabase ss ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Astro ]]>
                    </category>
                
                    <category>
                        <![CDATA[ authentication ]]>
                    </category>
                
                    <category>
                        <![CDATA[ supabase ]]>
                    </category>
                
                    <category>
                        <![CDATA[ supabase auth ]]>
                    </category>
                
                    <category>
                        <![CDATA[ magic links ]]>
                    </category>
                
                    <category>
                        <![CDATA[ cloudflare ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Cloudflare Turnstile ]]>
                    </category>
                
                    <category>
                        <![CDATA[ SSR ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Fatuma Abdullahi ]]>
                </dc:creator>
                <pubDate>Fri, 20 Jun 2025 17:02:12 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1750438909287/d36c0c01-e779-4eea-aa41-b797fcbb05f6.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this guide, you'll build a full server-side rendered (SSR) authentication system using Astro, Supabase, and Cloudflare Turnstile to protect against bots.</p>
<p>By the end, you'll have a fully functional authentication system with Astro actions, magic link authentication using Supabase, bot protection via Cloudflare Turnstile, protected routes and middleware, and secure session management.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-understanding-the-technologies">Understanding the Technologies</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-astro">What is Astro?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-are-astro-actions">What are Astro Actions?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-supabase">What is Supabase?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-cloudflare-turnstile">What is Cloudflare Turnstile?</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-understanding-ssr-authentication">Understanding SSR Authentication</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-ssr-vs-spa-authentication">SSR vs. SPA Authentication</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-why-protect-auth-forms">Why Protect Auth Forms?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-part-1-how-to-set-up-the-backend">Part 1: How to Set Up the Backend</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-set-up-supabase-backend">Set Up Supabase Backend</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-set-up-cloudflare-turnstile">Set Up Cloudflare Turnstile</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-part-2-how-to-set-up-the-frontend">Part 2: How to Set Up the Frontend</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-create-the-astro-project">Create the Astro Project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-configure-astro-for-ssr">Configure Astro for SSR</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-install-supabase-dependencies">Install Supabase Dependencies</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-configure-environment-variables">Configure Environment Variables</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-part-3-how-to-set-up-supabase-ssr">Part 3: How to Set Up Supabase SSR</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-create-the-supabase-client">Create the Supabase Client</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-create-middleware-for-route-protection">Create Middleware for Route Protection</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-part-4-how-to-build-the-user-interface">Part 4: How to Build the User Interface</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-update-the-layout">Update the Layout</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-create-the-sign-in-page">Create the Sign-In Page</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-create-the-protected-page">Create the Protected Page</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-part-5-how-to-set-up-astro-actions">Part 5: How to Set Up Astro Actions</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-create-the-authentication-actions">Create the Authentication Actions</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-create-the-code-exchange-api-route">Create the Code Exchange API Route</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-part-6-how-to-test-your-application">Part 6: How to Test Your Application</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-notes-and-additional-resources">Notes and Additional Resources</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-useful-documentation">Useful Documentation</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-complete-code-repository">Complete Code Repository</a></p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>This tutorial assumes you are familiar with:</p>
<ul>
<li><p>Web development frameworks</p>
</li>
<li><p><a target="_blank" href="https://www.freecodecamp.org/news/set-up-authentication-in-apps-with-supabase/">Basic authentication flows</a></p>
</li>
<li><p>Basic Backend-as-a-Service (BaaS) concepts</p>
</li>
</ul>
<h2 id="heading-understanding-the-technologies">Understanding the Technologies</h2>
<h3 id="heading-what-is-astro">What is Astro?</h3>
<p><a target="_blank" href="https://docs.astro.build/en/getting-started/">Astro</a> is a UI-agnostic web framework that renders <a target="_blank" href="https://docs.astro.build/en/concepts/why-astro/#server-first">server-first</a> by default. It <a target="_blank" href="https://docs.astro.build/en/guides/integrations-guide/#official-integrations">can be used with any UI framework</a>, including <a target="_blank" href="https://docs.astro.build/en/guides/client-side-scripts/">Astro client components</a>.</p>
<h3 id="heading-what-are-astro-actions">What are Astro Actions?</h3>
<p><a target="_blank" href="https://docs.astro.build/en/guides/actions/">Astro actions</a> allow you to write server-side functions that can be called without explicitly setting up API routes. They provide many useful utilities that simplify the process of running server logic and can be called from both client and server environments.</p>
<h3 id="heading-what-is-supabase">What is Supabase?</h3>
<p><a target="_blank" href="https://supabase.com/docs">Supabase</a> is an open-source Backend-as-a-Service that builds upon <a target="_blank" href="https://www.postgresql.org/docs/">Postgres</a>. It provides key features such as authentication, real-time capabilities, edge functions, storage, and more. Supabase offers both a hosted version for easy scaling and a self-hostable version for full control.</p>
<h3 id="heading-what-is-cloudflare-turnstile">What is Cloudflare Turnstile?</h3>
<p>Turnstile is <a target="_blank" href="https://www.cloudflare.com/en-gb/application-services/products/turnstile/">Cloudflare's replacement for CAPTCHAs</a>, which are visual puzzles used to differentiate between genuine users and bots. Unlike traditional CAPTCHAs, which are visually clunky, annoying, and <a target="_blank" href="https://blog.cloudflare.com/turnstile-ga/">sometimes difficult to solve</a>, Turnstile detects malicious activity without requiring users to solve puzzles, while providing a better user experience.</p>
<h2 id="heading-understanding-ssr-authentication">Understanding SSR Authentication</h2>
<p>Server-side rendered (SSR) auth refers to handling authentication on the server using a <a target="_blank" href="https://www.freecodecamp.org/news/set-up-authentication-in-apps-with-supabase/#how-does-authentication-work">cookie-based authentication method</a>.</p>
<p>The flow works as follows:</p>
<ol>
<li><p>The server creates a session and stores a session ID in a cookie sent to the client</p>
</li>
<li><p>The browser receives the cookie and automatically includes it in future requests</p>
</li>
<li><p>The server uses the cookie to determine if the user is authenticated</p>
</li>
</ol>
<p>Since browsers cannot modify HTTP-only cookies and servers cannot access local storage, SSR authentication requires careful management to prevent security risks such as session hijacking and stale sessions.</p>
<h3 id="heading-ssr-vs-spa-authentication">SSR vs. SPA Authentication</h3>
<p>Single-Page Applications (SPAs), like traditional React apps, handle authentication on the client side because they don't have direct access to a server. SPAs typically use JWTs stored in local storage, cookies, or session storage, sending these tokens in HTTP headers when communicating with servers.</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/HdE3dk8VkRU" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
<p> </p>
<h2 id="heading-why-protect-auth-forms">Why Protect Auth Forms?</h2>
<p>Authentication protects sensitive resources from unauthorized access, making auth forms primary targets for bots and malicious actors. Taking extra precautions is important for maintaining security.</p>
<h2 id="heading-part-1-how-to-set-up-the-backend">Part 1: How to Set Up the Backend</h2>
<h3 id="heading-set-up-supabase-backend">Set Up Supabase Backend</h3>
<p>First, you'll need <a target="_blank" href="https://supabase.com/dashboard/">a Supabase account</a>. Create a project, then:</p>
<ol>
<li><p>Go to the Authentication tab in the sidebar</p>
</li>
<li><p>Click the Sign In / Up tab under Configuration</p>
</li>
<li><p>Enable user sign-ups</p>
</li>
<li><p>Scroll down to Auth Providers and enable email (disable email confirmation for this tutorial)</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1742054137964/a379192b-4eaf-491f-bcf4-a0e1e0deef94.png" alt="Supabase authentication configuration interface showing user signup options and email provider enabled" width="2480" height="1448" loading="lazy"></p>
<h3 id="heading-set-up-cloudflare-turnstile">Set Up Cloudflare Turnstile</h3>
<ol>
<li><p><a target="_blank" href="https://dash.cloudflare.com/login">Log in or register for a Cloudflare account</a></p>
</li>
<li><p>Click the Turnstile tab in the sidebar</p>
</li>
<li><p>Click the "Add widget" button</p>
</li>
<li><p>Name your widget and add "localhost" as the hostname</p>
</li>
<li><p>Leave all other settings as default, and create the widget</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750260766060/95ec02e5-8ee7-4438-a66c-76866ec068c1.png" alt="Cloudflare Turnstile widget creation interface" width="2200" height="1796" loading="lazy"></p>
<p>After creating the widget, copy the secret key and add it to your Supabase dashboard:</p>
<ol>
<li><p>Go back to Supabase Authentication settings</p>
</li>
<li><p>Navigate to the Auth Protection tab under Configuration</p>
</li>
<li><p>Turn on Captcha protection</p>
</li>
<li><p>Choose Cloudflare as the provider</p>
</li>
<li><p>Paste your secret key</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750260776990/56ef5fc1-3321-45f0-ab9a-878679a08e88.png" alt="Supabase Attack Protection settings with Turnstile configuration" width="2302" height="986" loading="lazy"></p>
<h2 id="heading-part-2-how-to-set-up-the-frontend">Part 2: How to Set Up the Frontend</h2>
<h3 id="heading-create-the-astro-project">Create the Astro Project</h3>
<p>Next, you will need to create an Astro project. Open your preferred IDE or Text editor’s integrated terminal and run the following command to scaffold an Astro project in a folder named “ssr-auth.” Feel free to use any name you like.</p>
<pre><code class="lang-bash">npm create astro@latest ssr-auth
</code></pre>
<p>Follow the provided prompts and choose a basic template to start with. When it’s done, change into the folder, then run <code>npm install</code> to install dependencies, followed by <code>npm run dev</code> to start the server, and your site will be available at <a target="_blank" href="http://localhost:4321"><code>localhost:4321</code></a>.</p>
<h3 id="heading-configure-astro-for-ssr">Configure Astro for SSR</h3>
<p>Set Astro to run in SSR mode by adding <code>output: "server",</code> to the <code>defineConfig</code> function found in the <code>astro.config.mjs</code> file at the root of the folder.</p>
<p>Next, <a target="_blank" href="https://docs.astro.build/en/guides/integrations-guide/node/">add an adapter</a> to create a server runtime. For this, use the Node.js adapter by running this command in a terminal: <code>npx astro add node</code>. This will add it and automatically make all relevant changes.</p>
<p>Finally, add Tailwind for styling. Run this command in a terminal window: <code>npx astro add tailwind</code>. Follow the prompts, and it will make any changes necessary.</p>
<p>At this stage, your <code>astro.config.mjs</code> should look like this:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// @ts-check</span>
<span class="hljs-keyword">import</span> { defineConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">"astro/config"</span>;
<span class="hljs-keyword">import</span> node <span class="hljs-keyword">from</span> <span class="hljs-string">"@astrojs/node"</span>;
<span class="hljs-keyword">import</span> tailwindcss <span class="hljs-keyword">from</span> <span class="hljs-string">"@tailwindcss/vite"</span>;

<span class="hljs-comment">// https://astro.build/config</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineConfig({
  output: <span class="hljs-string">"server"</span>,
  adapter: node({
    mode: <span class="hljs-string">"standalone"</span>,
  }),
  vite: {
    plugins: [tailwindcss()],
  },
});
</code></pre>
<h3 id="heading-install-supabase-dependencies">Install Supabase Dependencies</h3>
<p>You can do this by running the following command:</p>
<pre><code class="lang-bash">npm install @supabase/supabase-js @supabase/ssr
</code></pre>
<h3 id="heading-configure-environment-variables">Configure Environment Variables</h3>
<p>Create a <code>.env</code> file in the project root and add the following. Remember to replace with your actual credentials:</p>
<pre><code class="lang-bash">SUPABASE_URL=&lt;YOUR_URL&gt;
SUPABASE_ANON_KEY=&lt;YOUR_ANON_KEY&gt;
TURNSTILE_SITE_KEY=&lt;YOUR_TURNSTILE_SITE_KEY&gt;
</code></pre>
<p>You can get the Supabase values from the dashboard:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1742054292788/8aeec326-259c-49bd-a6f8-b885e9a9e6ea.png" alt="Supabase project connection interface showing environment variables" width="2132" height="802" loading="lazy"></p>
<p><strong>💡Note:</strong> In Astro, environment variables accessed on the client side must be prefixed with 'PUBLIC'. But since we're using Astro actions that run on the server, the prefix is not required.</p>
<h2 id="heading-part-3-how-to-set-up-supabase-ssr">Part 3: How to Set Up Supabase SSR</h2>
<h3 id="heading-create-the-supabase-client">Create the Supabase Client</h3>
<p>Create <code>src/lib/supabase.ts</code>:</p>
<pre><code class="lang-typescript">
<span class="hljs-keyword">import</span> { createServerClient, parseCookieHeader } <span class="hljs-keyword">from</span> <span class="hljs-string">"@supabase/ssr"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { AstroCookies } <span class="hljs-keyword">from</span> <span class="hljs-string">"astro"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createClient</span>(<span class="hljs-params">{
    request,
    cookies,
}: {
    request: Request;
    cookies: AstroCookies;
}</span>) </span>{
    <span class="hljs-keyword">const</span> cookieHeader = request.headers.get(<span class="hljs-string">"Cookie"</span>) || <span class="hljs-string">""</span>;

    <span class="hljs-keyword">return</span> createServerClient(
        <span class="hljs-keyword">import</span>.meta.env.SUPABASE_URL,
        <span class="hljs-keyword">import</span>.meta.env.SUPABASE_ANON_KEY,
        {
            cookies: {
                getAll() {
                    <span class="hljs-keyword">const</span> cookies = parseCookieHeader(cookieHeader);
                    <span class="hljs-keyword">return</span> cookies.map(<span class="hljs-function">(<span class="hljs-params">{ name, value }</span>) =&gt;</span> ({
                        name,
                        value: value ?? <span class="hljs-string">""</span>,
                    }));
                },
                setAll(cookiesToSet) {
                    cookiesToSet.forEach(<span class="hljs-function">(<span class="hljs-params">{ name, value, options }</span>) =&gt;</span>
                        cookies.set(name, value, options)
                    );
                },
            },
        }
    );
}
</code></pre>
<p>This sets up Supabase to handle <a target="_blank" href="https://supabase.com/docs/guides/auth/server-side/creating-a-client?queryGroups=framework&amp;framework=astro&amp;queryGroups=environment&amp;environment=astro-browser">cookies in a server-rendered application</a> and exports a function that takes the request and cookies object as input. The function is set up like this because Astro has three ways to access request and cookie information:</p>
<ul>
<li><p>Through Astro’s global object, which is only available on Astro pages.</p>
</li>
<li><p>Through <code>AstroAPIContext</code> object, which is only available in Astro actions.</p>
</li>
<li><p>Through <code>APIContext</code> which is a subset of the global object and is available through API routes and middleware.</p>
</li>
</ul>
<p>So the <code>createClient</code> function accepts the <code>request</code> and <code>cookies</code> objects separately to make it flexible and applicable in the various contexts in which it may be used.</p>
<h3 id="heading-create-middleware-for-route-protection">Create Middleware for Route Protection</h3>
<p>Next, create a <code>middleware.ts</code> file in the <code>src</code> folder and paste this into it:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { defineMiddleware } <span class="hljs-keyword">from</span> <span class="hljs-string">"astro:middleware"</span>;
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"./lib/supabase"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> onRequest = defineMiddleware(<span class="hljs-keyword">async</span> (context, next) =&gt; {
    <span class="hljs-keyword">const</span> { pathname } = context.url;

    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Middleware executing for path:"</span>, pathname);

    <span class="hljs-keyword">const</span> supabase = createClient({
        request: context.request,
        cookies: context.cookies,
    });

    <span class="hljs-keyword">if</span> (pathname === <span class="hljs-string">"/protected"</span>) {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Checking auth for protected route"</span>);

        <span class="hljs-keyword">const</span> { data } = <span class="hljs-keyword">await</span> supabase.auth.getUser();

        <span class="hljs-comment">// If no user, redirect to index</span>
        <span class="hljs-keyword">if</span> (!data.user) {
            <span class="hljs-keyword">return</span> context.redirect(<span class="hljs-string">"/"</span>);
        }
    }

    <span class="hljs-keyword">return</span> next();
});
</code></pre>
<p>This middleware checks for an active user when accessing the protected route and redirects unauthenticated users to the index page.</p>
<h2 id="heading-part-4-how-to-build-the-user-interface">Part 4: How to Build the User Interface</h2>
<h3 id="heading-update-the-layout">Update the Layout</h3>
<p>First, update <code>src/layouts/Layout.astro</code> to include the Turnstile script. Add this just above the closing <code>&lt;/head&gt;</code> tag:</p>
<pre><code class="lang-typescript">&lt;script
    src=<span class="hljs-string">"https://challenges.cloudflare.com/turnstile/v0/api.js"</span>
    <span class="hljs-keyword">async</span>
    defer&gt;
&lt;/script&gt;
</code></pre>
<h3 id="heading-create-the-sign-in-page">Create the Sign-In Page</h3>
<p>Replace the contents of <code>src/pages/index.astro</code>:</p>
<pre><code class="lang-typescript">---
<span class="hljs-keyword">import</span> Layout <span class="hljs-keyword">from</span> <span class="hljs-string">"../layouts/Layout.astro"</span>;
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"../lib/supabase"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"../styles/global.css"</span>;

<span class="hljs-keyword">const</span> supabase = createClient({
    request: Astro.request,
    cookies: Astro.cookies,
});

<span class="hljs-keyword">const</span> { data } = <span class="hljs-keyword">await</span> supabase.auth.getUser();

<span class="hljs-keyword">if</span> (data.user) {
    <span class="hljs-keyword">return</span> Astro.redirect(<span class="hljs-string">"/protected"</span>);
}

<span class="hljs-keyword">const</span> apiKey = <span class="hljs-keyword">import</span>.meta.env.TURNSTILE_SITE_KEY;
---

&lt;Layout&gt;
    &lt;section <span class="hljs-keyword">class</span>=<span class="hljs-string">"flex flex-col items-center justify-center m-30"</span>&gt;
        &lt;h1 <span class="hljs-keyword">class</span>=<span class="hljs-string">"text-4xl text-left font-bold mb-12"</span>&gt;Sign In to Your Account&lt;/h1&gt;
        &lt;form id=<span class="hljs-string">"signin-form"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">"flex flex-col gap-2 w-1/2"</span>&gt;
            &lt;label <span class="hljs-keyword">for</span>=<span class="hljs-string">"email"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">""</span>&gt;Enter your email&lt;/label&gt;
            &lt;input
                <span class="hljs-keyword">type</span>=<span class="hljs-string">"email"</span>
                name=<span class="hljs-string">"email"</span>
                id=<span class="hljs-string">"email"</span>
                placeholder=<span class="hljs-string">"youremail@example.com"</span>
                <span class="hljs-keyword">class</span>=<span class="hljs-string">"border border-gray-500 rounded-md p-2"</span>
                required
            /&gt;
            &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"cf-turnstile"</span> data-sitekey={apiKey}&gt;&lt;/div&gt;
            &lt;button
                <span class="hljs-keyword">type</span>=<span class="hljs-string">"submit"</span>
                id=<span class="hljs-string">"sign-in"</span>
                <span class="hljs-keyword">class</span>=<span class="hljs-string">"bg-gray-600 hover:bg-gray-700 p-2 rounded-md text-white font-bold w-full cursor-pointer disabled:bg-gray-500 disabled:hover:bg-gray-500 disabled:cursor-not-allowed"</span>
                &gt;Sign In&lt;/button
            &gt;
        &lt;/form&gt;
    &lt;/section&gt;
&lt;/Layout&gt;
</code></pre>
<p>Here, the frontmatter creates a Supabase server client and then uses it to check if we have an active user. It redirects based on this information. This works because the front matter runs on the server side, and the project is set to server output.</p>
<p>The template displays a simple form with an email input. To complete it, add this below the closing <code>&lt;/Layout&gt;</code> tag:</p>
<pre><code class="lang-typescript">
&lt;script&gt;
    <span class="hljs-keyword">import</span> { actions } <span class="hljs-keyword">from</span> <span class="hljs-string">"astro:actions"</span>;

    <span class="hljs-keyword">declare</span> <span class="hljs-built_in">global</span> {
        <span class="hljs-keyword">interface</span> Window {
            turnstile?: {
                reset: <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">void</span>;
            };
        }
    }

    <span class="hljs-keyword">const</span> signInForm = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">"#signin-form"</span>) <span class="hljs-keyword">as</span> HTMLFormElement;
    <span class="hljs-keyword">const</span> formSubmitBtn = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"sign-in"</span>) <span class="hljs-keyword">as</span> HTMLButtonElement;

    signInForm?.addEventListener(<span class="hljs-string">"submit"</span>, <span class="hljs-keyword">async</span> (e) =&gt; {
        e.preventDefault();
        formSubmitBtn.disabled = <span class="hljs-literal">true</span>;
        formSubmitBtn.textContent = <span class="hljs-string">"Signing in..."</span>;

        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">const</span> turnstileToken = (
                <span class="hljs-built_in">document</span>.querySelector(
                    <span class="hljs-string">"[name='cf-turnstile-response']"</span>
                ) <span class="hljs-keyword">as</span> HTMLInputElement
            )?.value;

            <span class="hljs-keyword">if</span> (!turnstileToken) {
                <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"verification_missing"</span>);
            }

            <span class="hljs-keyword">const</span> formData = <span class="hljs-keyword">new</span> FormData(signInForm);
            formData.append(<span class="hljs-string">"captchaToken"</span>, turnstileToken);

            <span class="hljs-keyword">const</span> results = <span class="hljs-keyword">await</span> actions.signIn(formData);

            <span class="hljs-keyword">if</span> (!results.data?.success) {
                <span class="hljs-keyword">if</span> (results.data?.message?.includes(<span class="hljs-string">"captcha protection"</span>)) {
                    alert(<span class="hljs-string">"Verification failed. Please try again."</span>);
                    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">window</span>.turnstile) {
                        <span class="hljs-built_in">window</span>.turnstile.reset();
                    }
                    formSubmitBtn.disabled = <span class="hljs-literal">false</span>;
                    formSubmitBtn.textContent = <span class="hljs-string">"Sign In"</span>;
                    <span class="hljs-keyword">return</span>;
                } <span class="hljs-keyword">else</span> {
                    alert(<span class="hljs-string">"Oops! Could not sign in. Please try again"</span>);
                    formSubmitBtn.disabled = <span class="hljs-literal">false</span>;
                    formSubmitBtn.textContent = <span class="hljs-string">"Sign In"</span>;
                    <span class="hljs-keyword">return</span>;
                }
            }

            formSubmitBtn.textContent = <span class="hljs-string">"Sign In"</span>;
            alert(<span class="hljs-string">"Please check your email to sign in"</span>);
        } <span class="hljs-keyword">catch</span> (error) {
            <span class="hljs-keyword">if</span> (<span class="hljs-built_in">window</span>.turnstile) {
                <span class="hljs-built_in">window</span>.turnstile.reset();
            }
            formSubmitBtn.disabled = <span class="hljs-literal">false</span>;
            formSubmitBtn.textContent = <span class="hljs-string">"Sign In"</span>;
            <span class="hljs-built_in">console</span>.log(error);
            alert(<span class="hljs-string">"Something went wrong. Please try again"</span>);
        }
    });
&lt;/script&gt;
</code></pre>
<p>This adds some vanilla JavaScript that calls the <code>SignIn</code> Upon form submission. This action provides user feedback through alerts and manages the button’s text and disabled state. This effectively adds client-side interactivity to the page.</p>
<h3 id="heading-create-the-protected-page">Create the Protected Page</h3>
<p>Create <code>src/pages/protected.astro</code>:</p>
<pre><code class="lang-typescript">---
<span class="hljs-keyword">import</span> Layout <span class="hljs-keyword">from</span> <span class="hljs-string">"../layouts/Layout.astro"</span>;
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"../lib/supabase"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"../styles/global.css"</span>;

<span class="hljs-keyword">const</span> supabase = createClient({
    request: Astro.request,
    cookies: Astro.cookies,
});

<span class="hljs-keyword">const</span> { data } = <span class="hljs-keyword">await</span> supabase.auth.getUser();
---

&lt;Layout&gt;
    &lt;section <span class="hljs-keyword">class</span>=<span class="hljs-string">"flex flex-col items-center justify-center m-30"</span>&gt;
        &lt;h1 <span class="hljs-keyword">class</span>=<span class="hljs-string">"text-4xl text-left font-bold mb-12"</span>&gt;You are logged <span class="hljs-keyword">in</span>!&lt;/h1&gt;
        &lt;p <span class="hljs-keyword">class</span>=<span class="hljs-string">"mb-6"</span>&gt;Your user Id: {data.user?.id}&lt;/p&gt;
        &lt;button
            id=<span class="hljs-string">"sign-out"</span>
            <span class="hljs-keyword">class</span>=<span class="hljs-string">"bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded-md text-white font-bold cursor-pointer disabled:bg-gray-500 disabled:hover:bg-gray-500 disabled:cursor-not-allowed"</span>
            &gt;Sign Out&lt;/button
        &gt;
    &lt;/section&gt;
&lt;/Layout&gt;

&lt;script&gt;
    <span class="hljs-keyword">import</span> { actions } <span class="hljs-keyword">from</span> <span class="hljs-string">"astro:actions"</span>;
    <span class="hljs-keyword">const</span> signOutBtn = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"sign-out"</span>) <span class="hljs-keyword">as</span> HTMLButtonElement;

    signOutBtn?.addEventListener(<span class="hljs-string">"click"</span>, <span class="hljs-keyword">async</span> (e) =&gt; {
        e.preventDefault();
        signOutBtn!.disabled = <span class="hljs-literal">true</span>;
        signOutBtn!.textContent = <span class="hljs-string">"Signing out..."</span>;

        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">const</span> results = <span class="hljs-keyword">await</span> actions.signOut();

            <span class="hljs-keyword">if</span> (!results.data?.success) {
                signOutBtn!.disabled = <span class="hljs-literal">false</span>;
                signOutBtn!.textContent = <span class="hljs-string">"Sign Out"</span>;
                <span class="hljs-keyword">return</span> alert(<span class="hljs-string">"Oops! Could not sign Out. Please try again"</span>);
            }
            <span class="hljs-keyword">return</span> <span class="hljs-built_in">window</span>.location.reload();
        } <span class="hljs-keyword">catch</span> (error) {
            signOutBtn.disabled = <span class="hljs-literal">false</span>;
            signOutBtn.textContent = <span class="hljs-string">"Sign Out"</span>;
            <span class="hljs-built_in">console</span>.log(error);
            <span class="hljs-keyword">return</span> alert(<span class="hljs-string">"Something went wrong. Please try again"</span>);
        }
    });
&lt;/script&gt;
</code></pre>
<p>This page retrieves the user data server-side in the front matter and displays it in the template, along with a sign-out button.</p>
<p>The JavaScript in the <code>script</code> tags handle calling the sign-out action, user feedback, and button state, as in the <code>index.astro</code> page.</p>
<h2 id="heading-part-5-how-to-set-up-astro-actions">Part 5: How to Set Up Astro Actions</h2>
<h3 id="heading-create-the-authentication-actions">Create the Authentication Actions</h3>
<p>Finally, add an <code>actions</code> folder in the <code>src</code> folder and create an <code>index.ts</code> file to hold our logic. Paste the following into it:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { defineAction, <span class="hljs-keyword">type</span> ActionAPIContext } <span class="hljs-keyword">from</span> <span class="hljs-string">"astro:actions"</span>;
<span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">"astro:schema"</span>;
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"../lib/supabase"</span>;

<span class="hljs-keyword">const</span> emailSignUp = <span class="hljs-keyword">async</span> (
    {
        email,
        captchaToken,
    }: {
        email: <span class="hljs-built_in">string</span>;
        captchaToken: <span class="hljs-built_in">string</span>;
    },
    context: ActionAPIContext
) =&gt; {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Sign up action"</span>);
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> supabase = createClient({
            request: context.request,
            cookies: context.cookies,
        });

        <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase.auth.signInWithOtp({
            email,
            options: {
                captchaToken,
                emailRedirectTo: <span class="hljs-string">"http://localhost:4321/api/exchange"</span>,
            },
        });

        <span class="hljs-keyword">if</span> (error) {
            <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Sign up error"</span>, error);
            <span class="hljs-keyword">return</span> {
                success: <span class="hljs-literal">false</span>,
                message: error.message,
            };
        } <span class="hljs-keyword">else</span> {
            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Sign up success"</span>, data);
            <span class="hljs-keyword">return</span> {
                success: <span class="hljs-literal">true</span>,
                message: <span class="hljs-string">"Successfully logged in"</span>,
            };
        }
    } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"SignUp action other error"</span>, err);
        <span class="hljs-keyword">return</span> {
            success: <span class="hljs-literal">false</span>,
            message: <span class="hljs-string">"Unexpected error"</span>,
        };
    }
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> server = {
    signIn: defineAction({
        accept: <span class="hljs-string">"form"</span>,
        input: z.object({
            email: z.string().email(),
            captchaToken: z.string(),
        }),
        handler: <span class="hljs-keyword">async</span> (input, context) =&gt; {
            <span class="hljs-keyword">return</span> emailSignUp(input, context);
        },
    }),
    signOut: defineAction({
        handler: <span class="hljs-keyword">async</span> (_, context) =&gt; {
            <span class="hljs-keyword">const</span> supabase = createClient({
                request: context.request,
                cookies: context.cookies,
            });
            <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.signOut();
            <span class="hljs-keyword">if</span> (error) {
                <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Sign out error"</span>, error);
                <span class="hljs-keyword">return</span> {
                    success: <span class="hljs-literal">false</span>,
                    message: error.message,
                };
            }
            <span class="hljs-keyword">return</span> {
                success: <span class="hljs-literal">true</span>,
                message: <span class="hljs-string">"Successfully signed out"</span>,
            };
        },
    }),
};
</code></pre>
<p>This action handles both sign-in and sign-out methods. A Supabase server instance is created during the sign-in method, and the magic link method is used for sign-in. It passes a redirect URL, which we have yet to create, and handles any errors that may occur.</p>
<p>It also passes the token verification, allowing Supabase to perform verification on our behalf, eliminating the need to call <a target="_blank" href="https://developers.cloudflare.com/turnstile/get-started/server-side-validation/">Cloudflare’s verify APIs</a> directly.</p>
<p>The sign-out method calls Supabase’s sign-out method and handles any potential errors.</p>
<p>The redirect URL refers to an API route that exchanges the code from the email Supabase sends for a session that Supabase handles.</p>
<h3 id="heading-create-the-code-exchange-api-route">Create the Code Exchange API Route</h3>
<p>Create <code>src/pages/api/exchange.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { APIRoute } <span class="hljs-keyword">from</span> <span class="hljs-string">"astro"</span>;
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"../../lib/supabase"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> GET: APIRoute = <span class="hljs-keyword">async</span> ({ request, cookies, redirect }) =&gt; {
    <span class="hljs-keyword">const</span> url = <span class="hljs-keyword">new</span> URL(request.url);
    <span class="hljs-keyword">const</span> code = url.searchParams.get(<span class="hljs-string">"code"</span>);

    <span class="hljs-keyword">if</span> (!code) {
        <span class="hljs-keyword">return</span> redirect(<span class="hljs-string">"/"</span>);
    }

    <span class="hljs-keyword">const</span> supabase = createClient({ request, cookies });
    <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.exchangeCodeForSession(code);

    <span class="hljs-keyword">if</span> (error) {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error exchanging code for session:"</span>, error);
        <span class="hljs-keyword">return</span> redirect(<span class="hljs-string">"/404"</span>);
    }

    <span class="hljs-keyword">return</span> redirect(<span class="hljs-string">"/protected"</span>);
};
</code></pre>
<p>This grabs the code from the URL in the magic link sent, creates a server client, and calls the <code>exchangeCodeForSession</code> method with the code. It handles any error by redirecting to Astro’s built-in not-found page.</p>
<p>Otherwise, it will redirect to the protected page as Supabase handles the session implementation details.</p>
<h2 id="heading-part-6-how-to-test-your-application">Part 6: How to Test Your Application</h2>
<p>Start your development server: <code>npm run dev</code></p>
<p>Visit the provided localhost URL. You should see the sign-in page with the Turnstile widget:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750267075336/66ad5f39-67c6-458a-96ea-4dfe1123b015.png" alt="Sign-in page with Turnstile verification and email input field" width="2356" height="956" loading="lazy"></p>
<p>If you try to access the <code>/protected</code> page, it will redirect you back to this view until you sign in. Now, sign in, and you should get an email with a link that will redirect you to the <code>/protected</code> page. This is what you should see:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750335131827/f85cde2f-f9bb-46b0-a09e-6ae6456cd49f.png" alt="Text reads: &quot;You are logged in!&quot; with a field labeled &quot;Your user Id&quot; and a &quot;Sign Out&quot; button below." width="1200" height="502" loading="lazy"></p>
<p>And with that, you've successfully built a comprehensive auth system that leverages Astro actions, Supabase auth, and Cloudflare Turnstile's bot protection. This setup provides a secure, user-friendly authentication experience while protecting your application from malicious actors.</p>
<h2 id="heading-notes-and-additional-resources">Notes and Additional Resources</h2>
<h3 id="heading-useful-documentation">Useful Documentation</h3>
<ul>
<li><p><a target="_blank" href="https://supabase.com/docs/guides/auth/server-side/advanced-guide">Supabase's advanced guide to SSR</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/supabase/ssr">Supabase SSR package</a></p>
</li>
<li><p><a target="_blank" href="https://docs.astro.build/en/reference/api-reference/#cookies">Astro Cookies documentation</a></p>
</li>
<li><p><a target="_blank" href="https://supabase.com/docs/guides/auth/sessions/pkce-flow">Supabase PKCE flow documentation</a></p>
</li>
<li><p><a target="_blank" href="https://docs.astro.build/en/guides/actions/">Astro Actions documentation</a></p>
</li>
<li><p><a target="_blank" href="https://developers.cloudflare.com/turnstile/get-started/">Get started with Turnstile</a></p>
</li>
</ul>
<h3 id="heading-complete-code-repository">Complete Code Repository</h3>
<p>The complete code for this project is available on GitHub:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/FatumaA/supa-ssr">Base authentication setup</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/FatumaA/supa-ssr/tree/add-cloudflare">With Cloudflare Turnstile</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ A Developer's Guide to Protecting Personal Data: Best Practices and Tools ]]>
                </title>
                <description>
                    <![CDATA[ Think about it: you're sitting there enjoying your morning coffee, reading the headlines when again another data breach is making headlines. Millions of users' personal information – gone. You can't help but cringe as a developer at the prospect. Cou... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/developers-guide-to-protecting-personal-data/</link>
                <guid isPermaLink="false">680102ccf67e471495d5a624</guid>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Personal data protection ]]>
                    </category>
                
                    <category>
                        <![CDATA[ encryption ]]>
                    </category>
                
                    <category>
                        <![CDATA[ authentication ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Databases ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Alex Tray ]]>
                </dc:creator>
                <pubDate>Thu, 17 Apr 2025 13:31:56 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1744839185611/b3e49efc-6eee-4a0b-9522-20407b1782e3.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Think about it: you're sitting there enjoying your morning coffee, reading the headlines when again another data breach is making headlines. Millions of users' personal information – gone. You can't help but cringe as a developer at the prospect. Could it happen on your watch?</p>
<p>The reality is, keeping personal data safe isn't something you should be doing because it's good practice – it's something you have to do. Users are trusting developers to care for their data day in and day out, and power must be wielded wisely. If you're writing code that involves getting, processing, or storing someone's personal data, then you should be being proactive about keeping it safe.</p>
<p>So the question is: how do you safely keep personal data?</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<p></p><dl><p></p>
<p></p><ul><p></p>
<p></p><li><a href="heading-know-what-youre-protecting">Know What You</a></li><p></p>
<h2 id="heading-know-what-youre-protecting">Know What You're Protecting</h2>
<p>If you must protect information, first determine what information must be protected. It is crucial to <a target="_blank" href="https://blog.incogni.com/opt-out-guides/">protect sensitive information</a> from unauthorized access to ensure data security. Below is a list of some common types of sensitive data:</p>
<ul>
<li><p>Personally Identifiable Information (PII): name, address, phone number, email, Social Security number.</p>
</li>
<li><p>Financial Data: bank details, payment history, credit card number.</p>
</li>
<li><p>Authentication Data: password, auth tokens, API keys, security question responses.</p>
</li>
<li><p>Health Info: any kind of <a target="_blank" href="https://www.jotform.com/what-is-hipaa-compliance/">HIPAA</a>-protected information about the health and medical history of the user.</p>
</li>
</ul>
<p>Once you know what information has to be rendered secure, then you can go ahead and render it secure.</p>
<h2 id="heading-best-practices-in-data-security">Best Practices in Data Security</h2>
<h3 id="heading-1-encrypt-everything">1. Encrypt Everything</h3>
<p>Your best protection against hacking is encryption. When data is encrypted, even if hackers have access to it, they cannot do anything with it in the absence of the decryption key.</p>
<p>For stored sensitive information, use <strong>hashing with a salt</strong>, a process that turns a password into an irreversible value. This way, even if someone gains access to the stored data, the actual password isn't exposed.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> hashlib
<span class="hljs-keyword">import</span> os

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">hash_password</span>(<span class="hljs-params">password</span>):</span>
    salt = os.urandom(<span class="hljs-number">32</span>)  <span class="hljs-comment"># Generate a new salt</span>
    hashed_password = hashlib.pbkdf2_hmac(<span class="hljs-string">'sha256'</span>, password.encode(<span class="hljs-string">'utf-8'</span>), salt, <span class="hljs-number">100000</span>)
    <span class="hljs-keyword">return</span> salt + hashed_password
</code></pre>
<p>For data in transit, always use HTTPS:</p>
<pre><code class="lang-bash">sudo certbot --nginx -d yourdomain.com
</code></pre>
<p>This ensures data is encrypted between your server and the user. You can also reduce how often data is in transit by using <a target="_blank" href="https://www.suse.com/c/what-is-edge-computing/">edge computing</a>. Rather than sending sensitive data to external servers, increasing risk, it allows data to be stored and processed locally.</p>
<h3 id="heading-2-perform-secure-authentication">2. Perform Secure Authentication</h3>
<p>Weak authentication is an extremely critical security vulnerability.</p>
<p><strong>Authentication</strong> is the process of verifying who a user is (for example, logging in), while <strong>authorization</strong> is verifying what they're allowed to do (for example, access admin features).</p>
<p>Make sure that you:</p>
<ul>
<li><p>Perform strong password habits.</p>
</li>
<li><p>Perform multi-factor authentication (MFA). MFA requires users to present two or more verification factors (for example password and one-time code from a mobile device), making it much harder for attackers to gain access.</p>
</li>
<li><p>Perform OAuth 2.0 or OpenID Connect third-party authentication. These are secure industry-standard protocols that allow users to authenticate via trusted platforms like Google or Facebook, reducing the need to store credentials yourself.</p>
</li>
</ul>
<p>Example: Here’s an authentication setup using JWT (JSON Web Tokens) in Python:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> jwt
<span class="hljs-keyword">import</span> datetime

SECRET_KEY = <span class="hljs-string">"your_secret_key"</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">generate_token</span>(<span class="hljs-params">user_id</span>):</span>
    payload = {
        <span class="hljs-string">"user_id"</span>: user_id,
        <span class="hljs-string">"exp"</span>: datetime.datetime.utcnow() + datetime.timedelta(hours=<span class="hljs-number">1</span>)
    }
    <span class="hljs-keyword">return</span> jwt.encode(payload, SECRET_KEY, algorithm=<span class="hljs-string">'HS256'</span>)
</code></pre>
<p>This function generates a secure token for a user. The token contains the user ID and an expiration time, and it's signed using a secret key. Clients send this token with each request, and servers verify it to ensure the request comes from an authenticated user.</p>
<h3 id="heading-3-minimize-the-data-you-need-to-store">3. Minimize the Data You Need to Store</h3>
<p>One of the simplest things you can do to protect personal data? Store less than you have to. Consider the following questions:</p>
<ul>
<li><p>Do I really need to store this data?</p>
</li>
<li><p>How long do I really need to keep it for?</p>
</li>
<li><p>Can I anonymise it?</p>
</li>
</ul>
<p>For example, if you are going to need analytics, consider deleting personal identifiers prior to storing the data:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> anonymizeData = <span class="hljs-function">(<span class="hljs-params">user</span>) =&gt;</span> {
    <span class="hljs-keyword">return</span> {
        <span class="hljs-attr">sessionId</span>: generateRandomId(),
        <span class="hljs-attr">event</span>: user.event,
        <span class="hljs-attr">timestamp</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toISOString()
    };
};
</code></pre>
<p>This JavaScript function removes identifying information (like name or email) and replaces it with a random session ID, keeping only the data necessary for analytics.</p>
<p>For instance, if you manage email lists, avoid storing unnecessary subscriber data beyond what is required for communication.</p>
<p>Regularly clean and scrub email lists to remove outdated or inactive addresses. Sending emails to outdated/inactive addresses can damage your domain reputation, leading to blacklisting and email deliverability issues. If you only need email addresses for temporary campaigns, consider <a target="_blank" href="https://support.google.com/a/answer/151128?hl=en">automated deletion policies</a> to remove old data.</p>
<h3 id="heading-4-secure-your-apis">4. Secure Your APIs</h3>
<p>If your application is consuming other services, protect your API endpoints. You can do this by:</p>
<ul>
<li><p><strong>Require tokens or API keys</strong>: These act as credentials to access the API and prevent unauthorized use.</p>
</li>
<li><p><strong>Implement rate limiting to deter abuse</strong>: This prevents attackers from flooding your server with too many requests.</p>
</li>
<li><p><strong>Validate and sanitize all input data</strong>: This protects against injection attacks and malformed inputs.</p>
</li>
</ul>
<p>Here's how you can validate API input in Node.js:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">'express'</span>);
<span class="hljs-keyword">const</span> app = express();

app.post(<span class="hljs-string">'/api/data'</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> { name, email } = req.body;
    <span class="hljs-keyword">if</span> (!name || !email.includes(<span class="hljs-string">'@'</span>)) {
        <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).send(<span class="hljs-string">'Invalid input'</span>);
    }
    res.send(<span class="hljs-string">'Data received'</span>);
});
</code></pre>
<p>This ensures the API receives valid data and returns an error for incorrect input, which is a basic form of input sanitization.</p>
<h3 id="heading-5-lock-down-your-database">5. Lock Down Your Database</h3>
<p>Your database is an attack treasure trove, so lock it down:</p>
<ul>
<li><p><strong>Use parameterized queries</strong> to prevent SQL injection. These queries separate data from code.</p>
</li>
<li><p><strong>Limit database access using role-based permissions</strong>: Only give each user or service the access it needs—no more.</p>
</li>
<li><p><strong>Back up and test restoration procedures</strong>: Regular backups ensure you can recover data in the event of a breach or corruption.</p>
</li>
</ul>
<p>Here's a safe way to query a database in Python:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> sqlite3

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_user</span>(<span class="hljs-params">email</span>):</span>
    conn = sqlite3.connect(<span class="hljs-string">'database.db'</span>)
    cursor = conn.cursor()
    cursor.execute(<span class="hljs-string">"SELECT * FROM users WHERE email = ?"</span>, (email,))
    <span class="hljs-keyword">return</span> cursor.fetchone()
</code></pre>
<p>This example uses a parameterized query (the ? placeholder) to safely insert the email into the SQL command, protecting against injection.</p>
<p>Also, never overlook how databases and internal systems might be accessed remotely. Remote access, whether for IT admins, support teams, or mobile workers, often involves logging in from unfamiliar devices—which introduces new security challenges. Tools that allow for secure, contactless logins without typing passwords or installing software on the remote machine reduce the risk of credential theft.</p>
<p>You can also ensure that remote database connections, SSH access, and admin panels are protected with strong authentication, IP restrictions, and, ideally, VPN access to avoid exposing sensitive entry points to the internet.</p>
<p>And remember, you don’t have to reinvent the wheel—there are <a target="_blank" href="http://blog.scalefusion.com/best-data-protection-software/">powerful data protection tools</a> available to keep your data safe from breaches and downtime. Want to know which ones stand out? Check out this guide for a breakdown of some of the best solutions.</p>
<h3 id="heading-6-periodically-audit-and-update-your-code">6. Periodically Audit and Update Your Code</h3>
<p>Unpatched software and outdated dependencies are essentially an open invitation to the attackers. Update your software and conduct security audits regularly.</p>
<p>Perform security scans for your project:</p>
<pre><code class="lang-javascript">npm audit fix --force  # For Node.js projects
</code></pre>
<pre><code class="lang-python">pip install --upgrade package_name  <span class="hljs-comment"># For Python projects</span>
</code></pre>
<p>These commands help find and fix known vulnerabilities in your project dependencies.</p>
<h3 id="heading-7-train-your-employees">7. Train Your Employees</h3>
<p>Your security is just as strong as your weakest link. If one employee handles sensitive data irresponsibly, everything else may have been for naught.</p>
<ul>
<li><p><strong>Standard security training</strong>: Regular sessions on topics like phishing, password security, and data handling.</p>
</li>
<li><p><strong>Implement solid policies on user data handling</strong>: For instance, never download sensitive data to personal devices.</p>
</li>
<li><p><strong>Establish a security-oriented culture</strong>: Encourage reporting of suspicious activity, regular internal audits, and open communication about threats.</p>
</li>
</ul>
<h3 id="heading-8-give-users-control-over-their-data">8. Give Users Control Over Their Data</h3>
<p>Transparency breeds trust. Give users control to:</p>
<ul>
<li><p>View and download their data.</p>
</li>
<li><p>Terminate their account easily.</p>
</li>
<li><p>Make adjustments in privacy settings.</p>
</li>
</ul>
<p>If you are collecting data, provide an opt-out. Users must be able to protect sensitive data and be in control of what becomes of their information. This is why it is important to have a privacy policy: users need to know what data you are collecting and for what purpose. Check out this <a target="_blank" href="https://www.iubenda.com/en/help/36387-privacy-policy-template">privacy policy template</a> if you need to create one for your site.</p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>Data protection isn't just about coding well—it's about attitude. Get in the head of an attacker for a day, minimize vulnerabilities, and put user privacy at the top of your mind.</p>
<p>So the next time you're scanning the headlines for news of the latest ginormous data breach, you can be confident that your apps are bulletproof. Be smart, continue to learn, and let's make the internet safe—one line of secure code at a time.</p>
</ul></dl> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ What is Backend as a Service (BaaS)? A Beginner's Guide ]]>
                </title>
                <description>
                    <![CDATA[ Building an authentication system can be complex, often requiring a server to store user data. Sometimes, you need a faster, easier solution. For those new to development or without technical expertise, managing servers, databases, and user logins ca... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/backend-as-a-service-beginners-guide/</link>
                <guid isPermaLink="false">67b30da662ec9a593dfeb4a7</guid>
                
                    <category>
                        <![CDATA[ backend ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Databases ]]>
                    </category>
                
                    <category>
                        <![CDATA[ authentication ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Ijeoma Igboagu ]]>
                </dc:creator>
                <pubDate>Mon, 17 Feb 2025 10:21:26 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1739291731037/169ad924-9bcb-4af2-9281-fad2488a868d.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Building an authentication system can be complex, often requiring a server to store user data. Sometimes, you need a faster, easier solution.</p>
<p>For those new to development or without technical expertise, managing servers, databases, and user logins can be overwhelming. This is where Backend as a Service (BaaS) helps.</p>
<p>BaaS platforms provide ready-made backend solutions, making app development simpler. Whether you're a developer or someone with no coding experience, BaaS allows you to focus on your app’s features instead of handling backend complexities.</p>
<p>​​This article will explore BaaS, its features, pricing, and popular BaaS​ tools.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-backend-as-a-service-baas">​​​What is Backend as a Service (BaaS)?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-key-features-of-baas">Key Features of Baas</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-use-backend-as-a-service-baas">Why use Backend as a Service (BaaS)?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-when-to-use-backend-as-a-service-baas">When to Use Backend as a Service (BaaS)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-are-the-popular-backend-as-a-service-baas-tools">What are the Popular Backend as a Service (BaaS) Tools?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-get-started-with-baas-quick-example">How to Get Started with BaaS (Quick Example)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-backend-as-a-service-baas">​​​What is Backend as a Service (BaaS)?</h2>
<p>BaaS is a cloud platform that provides pre-built backend infrastructure and services. It eliminates the need for developers to manage servers, databases, and other backend tasks.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739537937739/4ae3bb03-1196-4298-9299-a0a09c4bd41d.png" alt="Graphical interface of BaaS" width="810" height="583" loading="lazy"></p>
<p>​​<strong>Source:</strong> <a target="_blank" href="https://www.cloudflare.com/fr-fr/learning/serverless/glossary/backend-as-a-service-baas/">https://www.cloudflare.com</a></p>
<h2 id="heading-key-features-of-baas">​​Key Features of BaaS</h2>
<p>Here are some features of BaaS:</p>
<ul>
<li><p>BaaS makes it easy to create and manage user accounts and logins without much coding.</p>
</li>
<li><p>It lets you store and manage data, eliminating the need to set up a database from scratch.</p>
</li>
<li><p>BaaS comes with tools (APIs and SDKs) that help connect your application to the backend easily.</p>
</li>
<li><p>Many BaaS platforms let you see updates in real time, so your application can show live data to users.</p>
</li>
<li><p>BaaS offers space in the cloud to store files and images, making it easy to handle user uploads.</p>
</li>
<li><p>You don’t need to worry about managing servers—BaaS takes care of that for you, so you can focus on building your application.</p>
</li>
<li><p>Some BaaS platforms allow you to send notifications to users about updates or messages.</p>
</li>
<li><p>BaaS often provides tools to track user interactions, helping you understand what works and what doesn’t.</p>
</li>
<li><p>It also makes it easy to integrate with other services like payment systems and social media with minimal effort.</p>
</li>
<li><p>As your app grows, BaaS scales with it, handling more users and data seamlessly.</p>
</li>
</ul>
<h2 id="heading-why-use-backend-as-a-service-baas">Why use Backend as a Service (BaaS)?</h2>
<p>There are several key reasons why BaaS is an excellent choice for developers:</p>
<ul>
<li><p>Pre-built features reduce development time, allowing you to focus on design and functionality instead of backend issues.</p>
</li>
<li><p>With BaaS, you don’t have to worry about servers, scaling, or security updates—the provider takes care of it all.</p>
</li>
<li><p>Most BaaS platforms offer essential features like user authentication, data storage, and real-time updates, helping you build your app without starting from scratch.</p>
</li>
<li><p>As your application gets more users, BaaS can handle it! These services adjust to support more users and data, so you can focus on growing your app.</p>
</li>
<li><p>BaaS handles the infrastructure so you don’t need to spend time or money on the backend. This allows you to focus on design and creating user experiences that add value to your users.</p>
</li>
</ul>
<h2 id="heading-when-to-use-backend-as-a-service-baas">When to Use Backend as a Service (BaaS)</h2>
<p>BaaS is perfect for building an app in a short amount of time without managing the backend. Here are the scenarios when BaaS makes sense:</p>
<ul>
<li><p>BaaS handles your app’s backend, letting you focus on its features. <strong>For example,</strong> when building a to-do list app, BaaS makes it easy to manage user logins and task data without setting up servers from scratch.</p>
</li>
<li><p>For small teams or solo devs, BaaS handles the backend. You do not need extra resources.</p>
</li>
<li><p>If you're launching a startup, Baas lets you release a Minimum Viable Product (MVP) without delay. It helps you speed up development and cut costs. </p>
</li>
<li><p>If your app needs features like user authentication, data storage, or push notifications, BaaS provides them out of the box. For example, when building a social media app, BaaS simplifies user logins and file uploads, saving you from starting from scratch.</p>
</li>
<li><p>BaaS automatically scales to support more users, allowing you to focus on improving your app. For example, a small multiplayer game can start with a few players, and as it grows, BaaS will seamlessly handle thousands without extra backend effort.</p>
</li>
</ul>
<h2 id="heading-what-are-the-popular-backend-as-a-service-baas-tools">What are the Popular Backend as a Service (BaaS) Tools?</h2>
<p>If you're looking to explore BaaS, here are popular platforms you can use:</p>
<h3 id="heading-clerk"><strong>Clerk</strong></h3>
<p>Clerk software focuses on user management. It offers tools for authentication, user profiles, and permissions management. It’s great for developers who need simple user management in their apps.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_C0064052A71C5CFDDDBA59A6AE53132401EA70FC25ACA9B576D0C25C8E9EB8BE_1730034843051_FireShot+Capture+598+-+Clerk+-+Authentication+and+User+Management+-+clerk.com.png" alt="The Graphical Interface of Clerk" width="1920" height="970" loading="lazy"></p>
<h3 id="heading-features-of-clerk"><strong>Features of clerk</strong></h3>
<p>Clerk provides:</p>
<ul>
<li><p>Multi-factor authentication (MFA)</p>
</li>
<li><p>Passwordless login (magic links, OTPs)</p>
</li>
<li><p>Social &amp; OAuth login (Google, GitHub, and so on)</p>
</li>
<li><p>Enterprise SSO (SAML, OAuth)</p>
</li>
<li><p>Biometric login (Face ID, Touch ID)</p>
</li>
</ul>
<p>It also handles:</p>
<ul>
<li><p>User profiles &amp; custom attributes</p>
</li>
<li><p>Roles &amp; permissions</p>
</li>
<li><p>Teams &amp; organizations</p>
</li>
<li><p>Session management</p>
</li>
</ul>
<p>For security, it offers:</p>
<ul>
<li><p>Token-based authentication (JWT)</p>
</li>
<li><p>Rate limiting</p>
</li>
<li><p>Audit logs</p>
</li>
<li><p>GDPR &amp; SOC 2 compliance</p>
</li>
</ul>
<p>For developers, it comes with:</p>
<ul>
<li><p>Prebuilt UI components</p>
</li>
<li><p>SDKs for React, Next.js, Vue, and so on</p>
</li>
<li><p>Custom email &amp; SMS templates</p>
</li>
</ul>
<p>To learn more, click here: <a target="_blank" href="https://clerk.com/">Clerk</a></p>
<h3 id="heading-pricing"><strong>Pricing</strong></h3>
<p>Clerk offers a <strong>Free Plan</strong> that includes up to 10,000 Monthly Active Users (MAUs) at no cost. For more advanced features, the <strong>Pro Plan</strong> is available at $25 per month, which also includes the first 10,000 MAUs.</p>
<p>For detailed and up-to-date information on Clerk's pricing plans, please visit their <a target="_blank" href="https://clerk.com/pricing">official pricing page</a>:</p>
<h3 id="heading-firebase"><strong>Firebase</strong></h3>
<p>Firebase is a Google-backed BaaS platform. It is known for its real-time databases, authentication, and cloud storage. It also has easy-to-use tools for web and mobile apps.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_C0064052A71C5CFDDDBA59A6AE53132401EA70FC25ACA9B576D0C25C8E9EB8BE_1730035263750_FireShot+Capture+599+-+Firebase+-+Googles+Mobile+and+Web+App+Development+Platform_+-+firebase.google.com.png" alt="The Graphical Interface of Firebase" width="1920" height="970" loading="lazy"></p>
<h3 id="heading-features-of-firebase">Features of Firebase</h3>
<p>Firebase provides:</p>
<h3 id="heading-backend-services"><strong>Backend Services</strong></h3>
<ul>
<li><p>Firestore &amp; Realtime Database</p>
</li>
<li><p>Cloud Storage</p>
</li>
<li><p>Serverless Functions</p>
</li>
<li><p>Web Hosting</p>
</li>
</ul>
<h3 id="heading-authentication"><strong>Authentication</strong></h3>
<ul>
<li><p>Email &amp; password login</p>
</li>
<li><p>Social logins (Google, Facebook, and so on)</p>
</li>
<li><p>Phone authentication</p>
</li>
<li><p>Anonymous sign-in</p>
</li>
</ul>
<h3 id="heading-analytics-amp-monitoring"><strong>Analytics &amp; Monitoring</strong></h3>
<ul>
<li><p>Google Analytics</p>
</li>
<li><p>Crash tracking (Crashlytics)</p>
</li>
<li><p>Performance monitoring</p>
</li>
<li><p>A/B testing</p>
</li>
</ul>
<h3 id="heading-engagement-tools"><strong>Engagement Tools</strong></h3>
<ul>
<li><p>Push notifications</p>
</li>
<li><p>Remote app updates</p>
</li>
<li><p>In-app messaging</p>
</li>
</ul>
<h3 id="heading-machine-learning"><strong>Machine Learning</strong></h3>
<ul>
<li><p>Text recognition</p>
</li>
<li><p>Image labelling</p>
</li>
</ul>
<p>To learn more, click here: <a target="_blank" href="https://firebase.google.com/">Firebase</a></p>
<h3 id="heading-pricing-plan">Pricing plan</h3>
<p>Firebase offers a <strong>Spark Plan</strong> (free tier) and a <strong>Blaze Plan</strong> (pay-as-you-go). The Spark Plan provides limited free usage, while the Blaze Plan charges based on your actual usage. For detailed and up-to-date information on Firebase's pricing plans, please visit their <a target="_blank" href="https://firebase.google.com/pricing">official pricing page</a>.</p>
<h3 id="heading-convex"><strong>Convex</strong></h3>
<p>Convex is a serverless BaaS platform. It provides real-time data sync and scalable backend services. The design simplifies serverless computing for developers.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_C0064052A71C5CFDDDBA59A6AE53132401EA70FC25ACA9B576D0C25C8E9EB8BE_1730035688864_FireShot+Capture+600+-+Convex+-+The+fullstack+TypeScript+development+platform+-+www.convex.dev.png" alt="The Graphical Interface of Convex" width="1920" height="970" loading="lazy"></p>
<h3 id="heading-convex-features"><strong>Convex Features</strong></h3>
<ul>
<li><p><strong>Database</strong> – Real-time data storage</p>
</li>
<li><p><strong>Serverless Functions</strong> – Run backend logic without managing servers</p>
</li>
<li><p><strong>Authentication</strong> – Built-in user auth &amp; access control</p>
</li>
<li><p><strong>Caching</strong> – Faster data retrieval</p>
</li>
<li><p><strong>Webhooks &amp; Crons</strong> – Automate tasks &amp; trigger events</p>
</li>
</ul>
<p>To learn more, click here: <a target="_blank" href="https://www.convex.dev/">Convex</a></p>
<h3 id="heading-pricing-1"><strong>Pricing</strong></h3>
<ul>
<li><p><strong>Free Plan</strong> – Limited resources for small projects</p>
</li>
<li><p><strong>Pro Plan</strong> – Pay-as-you-go based on usage</p>
</li>
</ul>
<p>Check out full details for <a target="_blank" href="https://convex.dev/pricing">convex pricing</a></p>
<h3 id="heading-8base"><strong>8base</strong></h3>
<p>A low-code platform that allows developers to build serverless apps with minimal setup. It provides database management, authentication, and API development tools.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_C0064052A71C5CFDDDBA59A6AE53132401EA70FC25ACA9B576D0C25C8E9EB8BE_1730036229410_gui+8base.png" alt="The Graphical Interface of 8base" width="1920" height="970" loading="lazy"></p>
<h3 id="heading-8base-features"><strong>8base Features</strong></h3>
<ul>
<li><p><strong>Backend Builder</strong> – Manage your database easily.</p>
</li>
<li><p><strong>Serverless Functions</strong> – Run custom backend logic.</p>
</li>
<li><p><strong>GraphQL API</strong> – Auto-generated API for your data.</p>
</li>
<li><p><strong>Authentication</strong> – Built-in user login &amp; access control.</p>
</li>
<li><p><strong>File Management</strong> – Store and manage files.</p>
</li>
</ul>
<p>To learn more, click here: <a target="_blank" href="https://www.8base.com/">8base</a></p>
<h3 id="heading-pricing-2"><strong>Pricing</strong></h3>
<ul>
<li><p><strong>Free Plan</strong> – $0/month (1 developer, basic features).</p>
</li>
<li><p><strong>Developer Plan</strong> – $25/month per developer.</p>
</li>
<li><p><strong>Professional Plan</strong> – $150/month (5 developers).</p>
</li>
<li><p><strong>Custom Plan</strong> – Contact 8base for enterprise solutions.</p>
</li>
</ul>
<p>Check out full pricing details here: <a target="_blank" href="https://www.8base.com/pricing">8base Pricing</a></p>
<h3 id="heading-backendless"><strong>Backendless</strong></h3>
<p>Backendless is a no-code platform that makes app development easy. It provides APIs, data storage, user management, and real-time updates in one place.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_C0064052A71C5CFDDDBA59A6AE53132401EA70FC25ACA9B576D0C25C8E9EB8BE_1730036359851_FireShot+Capture+584+-+Backendless+Visual+App+Development+Platform+-+UI+Backend++Database_+-+backendless.com.png" alt="The Graphical Interface of Backendless" width="1920" height="970" loading="lazy"></p>
<h3 id="heading-features">Features</h3>
<ul>
<li><p><strong>UI Builder</strong>: Design your app's front end visually without coding.</p>
</li>
<li><p><strong>Real-Time Database</strong>: Store and sync data in real-time across clients.</p>
</li>
<li><p><strong>User Authentication</strong>: Manage user sign-ups, logins, and roles.</p>
</li>
<li><p><strong>Cloud Code</strong>: Implement custom server-side logic without managing servers.</p>
</li>
<li><p><strong>Push Notifications</strong>: Send real-time alerts to users on various devices.</p>
</li>
</ul>
<p>To learn more, click here: <a target="_blank" href="https://backendless.com/">Backendless</a></p>
<h3 id="heading-pricing-3">Pricing</h3>
<p>Backendless offers several plans to suit different needs:</p>
<ul>
<li><p><strong>Free Plan</strong>: Ideal for small projects or learning purposes.</p>
</li>
<li><p><strong>Scale Fixed Plan</strong>: Provides predictable monthly billing with set resource limits.</p>
</li>
<li><p><strong>Scale Variable Plan</strong>: Offers flexibility with usage-based billing, scaling as your app grows.</p>
</li>
<li><p><strong>Backendless Pro</strong>: A self-hosted solution for enterprises requiring unlimited scalability and control.</p>
</li>
</ul>
<p>For more details on Backendless's pricing plans, please visit their <a target="_blank" href="https://backendless.com/pricing/">official pricing plan page</a>.</p>
<h3 id="heading-appwrite"><strong>Appwrite</strong></h3>
<p>Appwrite is an open-source BaaS that provides databases, authentication, file storage, real-time updates, serverless functions, and API management. It supports multiple platforms and offers built-in security and scalability for modern apps.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_C0064052A71C5CFDDDBA59A6AE53132401EA70FC25ACA9B576D0C25C8E9EB8BE_1730036473890_FireShot+Capture+583+-+Appwrite+-+Build+like+a+team+of+hundreds+-+appwrite.io.png" alt="The Graphical Interface of Appwrite" width="1920" height="970" loading="lazy"></p>
<h3 id="heading-features-1">Features</h3>
<ul>
<li><p><strong>Authentication</strong>: Secure user login with over 30 methods, including email/password, OAuth, and magic URLs.</p>
</li>
<li><p><strong>Database</strong>: Scalable storage with advanced permissions, custom data validation, and support for relationships.</p>
</li>
<li><p><strong>Functions</strong>: Deploy serverless functions in over 13 languages, with automatic GitHub deployment and custom domain support.</p>
</li>
<li><p><strong>Storage</strong>: Manage and serve files with built-in security and privacy features.</p>
</li>
<li><p><strong>Real-Time</strong>: Subscribe to database events for instant updates.</p>
</li>
</ul>
<p>To learn more, click here: <a target="_blank" href="https://appwrite.io/">Appwrite</a></p>
<h3 id="heading-pricing-4"><strong>Pricing</strong></h3>
<ul>
<li><p><strong>Free</strong> – $0/month (5GB bandwidth, 2GB storage, 750K function runs).</p>
</li>
<li><p><strong>Pro</strong> – Starts at $15/month (more storage, bandwidth, &amp; features).</p>
</li>
<li><p><strong>Scale</strong> – Starts at $599/month (for large-scale projects).</p>
</li>
</ul>
<p>For more details on the pricing plan check their <a target="_blank" href="https://appwrite.io/pricing">official pricing page</a>.</p>
<h3 id="heading-nhost"><strong>Nhost</strong></h3>
<p>Nhost is a full backend platform with a GraphQL API, database, authentication, and storage. It’s easy to set up and great for modern app development.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_C0064052A71C5CFDDDBA59A6AE53132401EA70FC25ACA9B576D0C25C8E9EB8BE_1730036732414_FireShot+Capture+585+-+Nhost_+The+Open+Source+Firebase+Alternative+with+GraphQL+-+nhost.io.png" alt="s_C0064052A71C5CFDDDBA59A6AE53132401EA70FC25ACA9B576D0C25C8E9EB8BE_1730036732414_FireShot+Capture+585+-+Nhost_+The+Open+Source+Firebase+Alternative+with+GraphQL+-+nhost.io" width="1920" height="970" loading="lazy"></p>
<h3 id="heading-nhost-features"><strong>Nhost Features</strong></h3>
<ul>
<li><p><strong>Authentication</strong> – Secure login with email, OAuth, and so on.</p>
</li>
<li><p><strong>Database</strong> – Scalable storage with permissions.</p>
</li>
<li><p><strong>Serverless Functions</strong> – Run backend code without servers.</p>
</li>
<li><p><strong>Storage</strong> – Secure file hosting.</p>
</li>
<li><p><strong>Real-Time</strong> – Instant updates on data changes.</p>
</li>
</ul>
<p>To learn more, click here: <a target="_blank" href="https://nhost.io/">Nhost</a>.</p>
<h3 id="heading-pricing-5"><strong>Pricing</strong></h3>
<ul>
<li><p><strong>Free</strong> – $0/month (basic features for small projects).</p>
</li>
<li><p><strong>Pro</strong> – $25/month (more resources &amp; support).</p>
</li>
<li><p><strong>Dedicated Compute</strong> – $50/month per vCPU/2GB RAM (for scaling apps).</p>
</li>
</ul>
<p>Check out full details here: <a target="_blank" href="https://nhost.io/pricing">Nhost Pricing</a></p>
<h3 id="heading-back4apps"><strong>Back4apps</strong></h3>
<p>Back4App is an open-source BaaS that simplifies backend development. It provides a complete infrastructure for building, hosting, and managing scalable apps. With built-in server-side features, developers can focus on coding without managing servers or databases.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_C0064052A71C5CFDDDBA59A6AE53132401EA70FC25ACA9B576D0C25C8E9EB8BE_1730036859543_FireShot+Capture+587+-+Build+launch+and+scale+applications+faster+than+ever+with+the+power_+-+www.back4app.com.png" alt="The Graphical Interface of Back4apps" width="1920" height="970" loading="lazy"></p>
<h3 id="heading-back4app-features"><strong>Back4App Features</strong></h3>
<ul>
<li><p><strong>Database</strong> – Manage data with APIs &amp; a visual editor</p>
</li>
<li><p><strong>Authentication</strong> – Secure user login &amp; roles</p>
</li>
<li><p><strong>Real-Time</strong> – Instant data updates</p>
</li>
<li><p><strong>Push Notifications</strong> – Send alerts to users.</p>
</li>
<li><p><strong>Cloud Functions</strong> – Run custom backend code.</p>
</li>
</ul>
<p>To learn more, click here: <a target="_blank" href="https://www.back4app.com/">Back4apps</a>.</p>
<h3 id="heading-pricing-6"><strong>Pricing</strong></h3>
<ul>
<li><p><strong>Free</strong> – 25K requests, 250MB storage, 1GB transfer/month.</p>
</li>
<li><p><strong>MVP Plan</strong> – For launching small apps.</p>
</li>
<li><p><strong>Dedicated Plan</strong> – For production apps with more resources.</p>
</li>
</ul>
<p>The <strong>MVP Plan</strong> in Back4App refers to a <strong>Minimum Viable Product (MVP) Plan</strong>. It is designed for startups and developers who are launching a small app with essential backend services. This plan provides enough resources to test and validate an idea before scaling up.</p>
<p>While <strong>Dedicated Plan</strong> in Back4App provides a <strong>private server with dedicated resources</strong> for apps that need better performance, security, and scalability. It is ideal for production apps with high traffic or specific infrastructure requirements.</p>
<p>Check out full details here: <a target="_blank" href="https://www.back4app.com/pricing">Back4App Pricing</a>.</p>
<h3 id="heading-aws-amplify"><strong>AWS Amplify</strong></h3>
<p>AWS Amplify is a development platform from Amazon Web Services (AWS). It simplifies building and deploying web and mobile apps. It offers tools and services for developers. They can integrate scalable backends, manage frontends, and add features like authentication, storage, and APIs.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_C0064052A71C5CFDDDBA59A6AE53132401EA70FC25ACA9B576D0C25C8E9EB8BE_1730036938873_FireShot+Capture+588+-+Full+Stack+Development+-+Web+and+Mobile+Apps+-+AWS+Amplify+-+aws.amazon.com.png" alt="The Graphical Interface of Aws Amplify" width="1920" height="970" loading="lazy"></p>
<h3 id="heading-aws-amplify-features"><strong>AWS Amplify Features</strong></h3>
<ul>
<li><p><strong>Authentication</strong> – Secure login with email, social sign-in, and multi-factor authentication</p>
</li>
<li><p><strong>Database &amp; API</strong> – Build real-time APIs with AWS databases</p>
</li>
<li><p><strong>Storage</strong> – Manage files and media with Amazon S3</p>
</li>
<li><p><strong>Hosting</strong> – Deploy full-stack apps with continuous deployment</p>
</li>
</ul>
<p>To learn more, click here: <a target="_blank" href="https://aws.amazon.com/amplify/?gclid=Cj0KCQjwpP63BhDYARIsAOQkATZlSP8VJyO8gGZMtrSp7JE6hMJjFPh1Am4F2eQv5Yex_okPLLvWjlUaAgDQEALw_wcB&amp;trk=e37f908f-322e-4ebc-9def-9eafa78141b8&amp;sc_channel=ps&amp;ef_id=Cj0KCQjwpP63BhDYARIsAOQkATZlSP8VJyO8gGZMtrSp7JE6hMJjFPh1Am4F2eQv5Yex_okPLLvWjlUaAgDQEALw_wcB:G:s&amp;s_kwcid=AL!4422!3!647301987559!p!!g!!amplify%20framework!19613610159!148358959649">Aws Amplify</a></p>
<h3 id="heading-pricing-7"><strong>Pricing</strong></h3>
<ul>
<li><p><strong>Free Tier (First 12 months)</strong></p>
<ul>
<li><p>1,000 build minutes/month</p>
</li>
<li><p>5GB storage</p>
</li>
<li><p>15GB bandwidth</p>
</li>
<li><p>500K API requests</p>
</li>
</ul>
</li>
<li><p><strong>Pay-As-You-Go (After Free Tier)</strong></p>
<ul>
<li><p><strong>Build &amp; Deploy</strong> – $0.01 per build minute</p>
</li>
<li><p><strong>Storage</strong> – $0.023 per GB/month</p>
</li>
<li><p><strong>Bandwidth</strong> – $0.15 per GB served</p>
</li>
<li><p><strong>API Requests</strong> – $0.30 per 1M requests</p>
</li>
</ul>
</li>
</ul>
<p>Full details here: <a target="_blank" href="https://aws.amazon.com/amplify/pricing/">AWS Amplify Pricing</a></p>
<h3 id="heading-supabase"><strong>Supabase</strong></h3>
<p>Supabase is an open-source alternative to Firebase. It uses PostgreSQL for its database. It has built-in features like authentication, APIs, and real-time subscriptions.</p>
<p><img src="https://paper-attachments.dropboxusercontent.com/s_C0064052A71C5CFDDDBA59A6AE53132401EA70FC25ACA9B576D0C25C8E9EB8BE_1730037060219_FireShot+Capture+581+-+Supabase+-+The+Open+Source+Firebase+Alternative+-+supabase.com.png" alt="The Graphical Interface of Supabase" width="1920" height="970" loading="lazy"></p>
<h3 id="heading-supabase-features"><strong>Supabase Features</strong></h3>
<ul>
<li><p><strong>Database</strong> – PostgreSQL with full SQL support.</p>
</li>
<li><p><strong>Authentication</strong> – Secure login with email, password, and social logins.</p>
</li>
<li><p><strong>Storage</strong> – Store and serve files easily.</p>
</li>
<li><p><strong>Real-Time</strong> – Get instant updates when data changes.</p>
</li>
<li><p><strong>Edge Functions</strong> – Run serverless backend logic.</p>
</li>
</ul>
<p>To learn more, click here: <a target="_blank" href="https://supabase.com/">Supabase</a>.</p>
<h3 id="heading-pricing-8"><strong>Pricing</strong></h3>
<ul>
<li><p><strong>Free</strong> – Great for small projects i.e. projects for learning, and experimentation.</p>
</li>
<li><p><strong>Pro</strong> – Starts at $25/month (includes $10 compute credits).</p>
</li>
<li><p><strong>Team</strong> – Starts at $599/month (for advanced features &amp; support).</p>
</li>
</ul>
<p>Full details here: <a target="_blank" href="https://supabase.com/pricing">Supabase Pricing</a></p>
<h2 id="heading-how-to-get-started-with-baas-quick-example">How to Get Started with BaaS (Quick Example)</h2>
<p>Let’s go through a quick example to get started. In this tutorial, I’ll use Firebase as an example.</p>
<ul>
<li><p>Go to the <a target="_blank" href="https://firebase.google.com/">Firebase website</a> and sign up using your Google account.</p>
</li>
<li><p>After signing in, create a new Firebase project by following the on-screen instructions.</p>
</li>
<li><p>Go to "Authentication" and enable a sign-in method, like email/password or Google login</p>
</li>
<li><p>In "Firestore Database," create a new database for your app's data.</p>
</li>
<li><p>Install Firebase SDK in your project and integrate authentication, databases, and other Firebase services into your app.</p>
</li>
</ul>
<p>For more detailed instructions on setting up Firebase, check out this article: <a target="_blank" href="https://www.freecodecamp.org/news/authenticate-react-app-using-firebase/">How to Authenticate Your React App Using Firebase</a> where I explain each step in depth.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Backend as a Service (BaaS) is ideal for developers. It provides an efficient and cost-effective way to handle backend development tasks. BaaS can speed up your development. It lets you avoid server management. You can then focus on building better apps.</p>
<p>If you're new to backend development, check out the BaaS tools in this article. They can simplify your workflow. Try out BaaS today and take your development to the next level!</p>
<p>Have you tried using BaaS for your applications? Share your experiences!</p>
<p>If you found this article helpful, share it with others who may find it interesting.</p>
<p>Stay updated with my projects by following me on <a target="_blank" href="https://twitter.com/ijaydimples">Twitter</a>, <a target="_blank" href="https://twitter.com/ijaydimples">LinkedIn</a> and <a target="_blank" href="https://github.com/ijayhub">GitHub</a>.</p>
<p>Thank you for reading.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
