<?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[ oauth - 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[ oauth - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 13 Sep 2026 05:01:59 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/oauth/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Connect to HMRC's Making Tax Digital API: A Beginner's Guide ]]>
                </title>
                <description>
                    <![CDATA[ If you write software for anyone who pays UK tax, sooner or later you'll need to talk to HMRC. Making Tax Digital (MTD) for Income Tax went live on 6 April 2026, and it's already mandatory for self-em ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-connect-to-hmrc-making-tax-digital-api/</link>
                <guid isPermaLink="false">6aa47c59141bdd8e9f05b331</guid>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ oauth ]]>
                    </category>
                
                    <category>
                        <![CDATA[ MTD ]]>
                    </category>
                
                    <category>
                        <![CDATA[ tax ]]>
                    </category>
                
                    <category>
                        <![CDATA[ tax return ]]>
                    </category>
                
                    <category>
                        <![CDATA[ HMRC Tax Filing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ HMRC ]]>
                    </category>
                
                    <category>
                        <![CDATA[ HMRC Tax Returns ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Solomon Amos ]]>
                </dc:creator>
                <pubDate>Fri, 11 Sep 2026 22:10:33 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/3dd244f7-30e5-484e-8552-3938d1555bf3.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you write software for anyone who pays UK tax, sooner or later you'll need to talk to <a href="https://www.gov.uk/government/organisations/hm-revenue-customs">HMRC</a>.</p>
<p>Making Tax Digital (MTD) for Income Tax went live on 6 April 2026, and it's already mandatory for self-employed people and landlords whose qualifying income is over £50,000. That threshold drops to £30,000 in April 2027 and to £20,000 in April 2028, so the population it covers roughly triples over the next two years.</p>
<p>In practice, that means a lot of small businesses now need software that can send their figures to HMRC, and someone has to build that software.</p>
<p>The first time you open HMRC's developer documentation, it's a wall of acronyms: MTD, ITSA, OAuth scopes, fraud prevention headers, and obligations. It looks intimidating, and a lot of it is genuinely fiddly. But the path from zero to your first authenticated API call is more approachable than it looks once someone walks you through it in order.</p>
<p>That's what this guide does. I built the HMRC integration for a Making Tax Digital app, and I'm going to take you through the same steps I took, in the order I took them: what MTD is, how to get a sandbox app, how the OAuth login flow works, what those fraud prevention headers are, and how to make a real call. The code is Node and TypeScript, but the ideas carry over to any language.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5ff319e8638f6a0ef52a6236/ce6e430b-0129-4b55-bb9c-7216036219e4.png" alt="HMRC campaign graphic reading Making Tax Digital is here" style="display: block;" width="600" height="400" loading="lazy">

<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-what-making-tax-digital-actually-is">What Making Tax Digital Actually is</a></p>
</li>
<li><p><a href="#heading-step-1-create-a-sandbox-application">Step 1: Create a Sandbox Application</a></p>
</li>
<li><p><a href="#heading-step-2-understand-the-oauth-scopes">Step 2: Understand the OAuth Scopes</a></p>
</li>
<li><p><a href="#heading-step-3-the-oauth-20-authorization-code-flow">Step 3: The OAuth 2.0 Authorization Code Flow</a></p>
</li>
<li><p><a href="#heading-step-4-fraud-prevention-headers-the-part-nobody-warns-you-about">Step 4: Fraud Prevention Headers (the Part Nobody Warns You About)</a></p>
</li>
<li><p><a href="#heading-step-5-make-your-first-authenticated-call">Step 5: Make Your First Authenticated Call</a></p>
</li>
<li><p><a href="#heading-common-gotchas-that-will-cost-you-an-afternoon">Common Gotchas that Will Cost You an Afternoon</a></p>
</li>
<li><p><a href="#heading-where-to-go-next">Where to Go Next</a></p>
</li>
</ul>
<h2 id="heading-what-making-tax-digital-actually-is">What Making Tax Digital Actually is</h2>
<p>Making Tax Digital is HMRC's programme to move tax record-keeping and filing into software.</p>
<p>Instead of one big annual Self Assessment return typed into a website, MTD for Income Tax asks people to keep digital records and send HMRC a cumulative update of their income and expenses every quarter, then a final declaration at the end of the year.</p>
<p>(For the official scope and timeline, see <a href="https://www.gov.uk/guidance/use-making-tax-digital-for-income-tax">GOV.UK's MTD for Income Tax guidance</a>.)</p>
<img src="https://cdn.hashnode.com/uploads/covers/5ff319e8638f6a0ef52a6236/485240c3-b58e-408c-913a-c9559e48c1ea.png" alt="HMRC campaign graphic reading Making Tax Digital for Income Tax, one year to go" style="display: block;" width="600" height="400" loading="lazy">

<p>For developers, the important part is that HMRC doesn't give you a single "file my taxes" button. It gives you a set of REST APIs on its Developer Hub: one to list a person's businesses, one to read their filing obligations (what's due and when), one to submit the quarterly figures, one to trigger a tax calculation, and so on.</p>
<p>You compose these into a journey. Every one of them sits behind the same two gates: an OAuth 2.0 access token, and a set of fraud prevention headers. Get those two things right and the rest is ordinary JSON over HTTPS.</p>
<h2 id="heading-step-1-create-a-sandbox-application">Step 1: Create a Sandbox Application</h2>
<p>You never start against live taxpayer data. HMRC runs a full sandbox at <code>test-api.service.hmrc.gov.uk</code> that mirrors the production host (<code>api.service.hmrc.gov.uk</code>) and lets you create fake taxpayers to test with.</p>
<p>Here is the practical sequence:</p>
<ol>
<li><p>Register for a free account on the <a href="https://developer.service.hmrc.gov.uk/">HMRC Developer Hub</a>.</p>
</li>
<li><p>Create an application. You get a <strong>client ID</strong> and a <strong>client secret</strong>. Treat the secret like a password: keep it in an environment variable, never in your source.</p>
</li>
<li><p>Set a <strong>redirect URI</strong>. This is the URL HMRC sends the user back to after they log in. For local work something like <code>http://localhost:3000/auth/hmrc/callback</code> is fine. It has to match exactly later, character for character.</p>
</li>
<li><p><strong>Subscribe</strong> your application to the APIs you need. This is the step beginners miss. Listing it isn't enough. You click into each API and subscribe. If you forget, calls come back <code>403 Forbidden</code> with no obvious reason.</p>
</li>
</ol>
<p>In the code, the only thing that differs between sandbox and production is the base URL, so it's worth keeping those in config rather than hard-coding them:</p>
<pre><code class="language-ts">const config = {
  sandbox: {
    baseUrl: 'https://test-api.service.hmrc.gov.uk',
    authUrl: 'https://test-api.service.hmrc.gov.uk/oauth/authorize',
    tokenUrl: 'https://test-api.service.hmrc.gov.uk/oauth/token',
  },
  production: {
    baseUrl: 'https://api.service.hmrc.gov.uk',
    authUrl: 'https://api.service.hmrc.gov.uk/oauth/authorize',
    tokenUrl: 'https://api.service.hmrc.gov.uk/oauth/token',
  },
};
</code></pre>
<p>One thing I learned the hard way: pick the environment from an explicit setting, not from <code>NODE_ENV</code>. You'll eventually want a production deployment that still points at the HMRC sandbox during a beta, and if your URLs are keyed off <code>NODE_ENV</code> alone, that combination forces production HMRC and rejects your sandbox credentials with <code>client_id is invalid</code>. A small <code>HMRC_BASE_URL</code> environment variable that always wins saves you that confusion.</p>
<h2 id="heading-step-2-understand-the-oauth-scopes">Step 2: Understand the OAuth Scopes</h2>
<p>When your app asks a user for access, it asks for specific <strong>scopes</strong>, which are named permissions. For MTD Income Tax the ones you want are <code>read:self-assessment</code> and <code>write:self-assessment</code> (HMRC also exposes <code>read:vat</code> and <code>write:vat</code> for the VAT APIs). HMRC documents these in its <a href="https://developer.service.hmrc.gov.uk/api-documentation/docs/authorisation">OAuth 2.0 authorisation guide</a>. You join them with spaces:</p>
<pre><code class="language-ts">const scopes = ['read:self-assessment', 'write:self-assessment'].join(' ');
</code></pre>
<p>The user sees these spelled out on HMRC's consent screen, so only ask for what you actually use.</p>
<h2 id="heading-step-3-the-oauth-20-authorization-code-flow">Step 3: The OAuth 2.0 Authorization Code Flow</h2>
<p>This is the heart of the integration, and it's the standard three-legged OAuth dance. Think of it as your app, the user, and HMRC passing a baton. There are four moments: send the user to HMRC, HMRC sends them back with a code, you swap the code for tokens, and later you refresh those tokens.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5ff319e8638f6a0ef52a6236/7cd5e83f-2e18-4b09-9c2d-e9029ed33fe6.png" alt="Sequence diagram of the OAuth flow between the user, your application and the HMRC API" style="display: block;" width="600" height="400" loading="lazy">

<h3 id="heading-3a-send-the-user-to-hmrc">3a. Send the User to HMRC</h3>
<p>You build an authorization URL and redirect the browser to it. The query string carries your client ID, the scopes, your redirect URI, <code>response_type=code</code>, and a <code>state</code> value:</p>
<pre><code class="language-ts">function getAuthorizationUrl(state: string): string {
  const params = new URLSearchParams({
    response_type: 'code',
    client_id: hmrcConfig.clientId,
    scope: hmrcConfig.scopes,
    state: state,
    redirect_uri: hmrcConfig.redirectUri,
  });
  return hmrcConfig.authUrl + '?' + params.toString();
}
</code></pre>
<p>That <code>state</code> value matters. It's your defense against cross-site request forgery (CSRF). You generate a random, unguessable string, store it server-side, attach it here, and check it again when the user comes back. If the value that returns doesn't match one you issued, you reject the callback. A common pattern is to generate a UUID and stash it with the user's ID and a timestamp:</p>
<pre><code class="language-ts">const state = uuidv4();
await setStateToken(state, { createdAt: Date.now(), userId });
res.redirect(getAuthorizationUrl(state));
</code></pre>
<p>The user now logs in on HMRC's own pages (with their Government Gateway credentials) and approves the scopes. Your app never sees their password, which is the whole point of OAuth.</p>
<h3 id="heading-3b-handle-the-callback">3b. Handle the Callback</h3>
<p>HMRC redirects back to your redirect URI with two query parameters: <code>code</code> and <code>state</code> (or an <code>error</code> if the user declined). First validate the state, then make it single-use by deleting it immediately so it can never be replayed:</p>
<pre><code class="language-ts">const stored = await getStateToken(state);
if (!state || !stored) {
  return redirectError('Invalid or expired authorization request');
}
await deleteStateToken(state); // single-use: delete the moment it validates

// Optional but sensible: expire stale requests (here, 10 minutes).
if (Date.now() - stored.createdAt &gt; 10 * 60 * 1000) {
  return redirectError('Authorization request has expired. Please try again.');
}
</code></pre>
<h3 id="heading-3c-exchange-the-code-for-tokens">3c. Exchange the Code for Tokens</h3>
<p>The <code>code</code> is short-lived and useless on its own. You exchange it, server to server, for an access token and a refresh token. This is a <code>POST</code> to the token endpoint with <code>grant_type=authorization_code</code>, form-encoded, including your client secret. The secret is why this happens on your backend and never in the browser:</p>
<pre><code class="language-ts">async function exchangeCodeForTokens(code: string) {
  const response = await axios.post(
    hmrcConfig.tokenUrl,
    new URLSearchParams({
      grant_type: 'authorization_code',
      code,
      client_id: hmrcConfig.clientId,
      client_secret: hmrcConfig.clientSecret,
      redirect_uri: hmrcConfig.redirectUri,
    }).toString(),
    { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } },
  );

  return {
    accessToken: response.data.access_token,
    refreshToken: response.data.refresh_token,
    expiresIn: response.data.expires_in, // seconds until the access token dies
  };
}
</code></pre>
<p>Store these tokens server-side, tied to the user, and never send them to the client. The access token is what authorises every later API call. Note <code>expires_in</code>: HMRC access tokens are short-lived (currently four hours), so you'll need step 3d.</p>
<h3 id="heading-3d-refresh-the-token">3d. Refresh the Token</h3>
<p>When the access token expires, you don't make the user log in again. You use the refresh token to get a fresh pair, with <code>grant_type=refresh_token</code>:</p>
<pre><code class="language-ts">async function refreshAccessToken(refreshToken: string) {
  const response = await axios.post(
    hmrcConfig.tokenUrl,
    new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: hmrcConfig.clientId,
      client_secret: hmrcConfig.clientSecret,
    }).toString(),
    { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } },
  );
  return {
    accessToken: response.data.access_token,
    refreshToken: response.data.refresh_token,
    expiresIn: response.data.expires_in,
  };
}
</code></pre>
<p>A good habit is to check the stored token's expiry before each call and refresh proactively if it's close to running out. HMRC's <a href="https://developer.service.hmrc.gov.uk/api-documentation/docs/authorisation">OAuth documentation</a> has the full lifecycle.</p>
<h2 id="heading-step-4-fraud-prevention-headers-the-part-nobody-warns-you-about">Step 4: Fraud Prevention Headers (the Part Nobody Warns You About)</h2>
<p>Here's the surprise. Most APIs are happy with a bearer token. HMRC is not. By law, it requires your software to send a set of <strong>fraud prevention headers</strong> on every MTD call: a couple of dozen <code>Gov-Client-*</code> and <code>Gov-Vendor-*</code> headers describing the device, the network path, and the software making the request. They help HMRC detect credential abuse across thousands of third-party vendors.</p>
<p>This guide is about getting connected, so I'll keep this high level: read <a href="https://developer.service.hmrc.gov.uk/guides/fraud-prevention/">HMRC's Fraud Prevention Headers specification</a> carefully, because the rules are precise and the failure modes are quiet.</p>
<p>The single most important decision is the connection method, sent in <code>Gov-Client-Connection-Method</code>. A server-mediated web app uses <code>WEB_APP_VIA_SERVER</code>; a server-mediated mobile app uses <code>MOBILE_APP_VIA_SERVER</code>. That choice decides which other headers are required and which are forbidden, so don't just emit everything.</p>
<p>The genuinely good news for beginners is that HMRC gives you a <a href="https://developer.service.hmrc.gov.uk/api-documentation/docs/api/service/txm-fph-validator-api">Test Fraud Prevention Headers API</a> that inspects your request and tells you exactly which headers are missing or malformed. Build against that validator from your first commit and you turn a guessing game into a checklist.</p>
<p>You don't need the full depth of that spec to get your first sandbox call working, but you will need it before you go anywhere near production data, so budget real time for it rather than leaving it to the end.</p>
<h2 id="heading-step-5-make-your-first-authenticated-call">Step 5: Make Your First Authenticated Call</h2>
<p>Now you have a token and your headers. Time for a real request. Every MTD call sets two things on top of the token: an <code>Accept</code> header that pins the API version, and the fraud headers:</p>
<pre><code class="language-ts">async function request(method, path, accessToken, req, data = null, apiVersion = '2.0') {
  const headers = {
    Authorization: `Bearer ${accessToken}`,
    Accept: `application/vnd.hmrc.${apiVersion}+json`,
    ...hmrcConfig.getFraudHeaders(req),
  };
  // Only set a JSON Content-Type when there is actually a body (see gotcha 3).
  if (data !== null &amp;&amp; data !== undefined) {
    headers['Content-Type'] = 'application/json';
  }
  const res = await axios({ baseURL: hmrcConfig.baseUrl, method, url: path, headers, data });
  return res.data;
}
</code></pre>
<p>A great first call is "list this person's businesses", which uses their National Insurance number (NINO) and the Business Details API. It's read-only, so it's a safe way to confirm your token and headers are accepted:</p>
<pre><code class="language-ts">// GET /individuals/business/details/{nino}/list  (Business Details API v2.0)
const businesses = await request(
  'GET',
  `/individuals/business/details/${nino}/list`,
  accessToken,
  req,
  null,
  '2.0',
);
</code></pre>
<p>Once that returns, the natural next call is obligations: what quarterly updates are due and when. This one is on a newer API version, which is the perfect lead-in to the gotchas:</p>
<pre><code class="language-ts">// GET /obligations/details/{nino}/income-and-expenditure  (Obligations API v3.0)
const obligations = await request(
  'GET',
  `/obligations/details/${nino}/income-and-expenditure`,
  accessToken,
  req,
  null,
  '3.0',
);
</code></pre>
<p>To test these in the sandbox, you create a fake taxpayer using HMRC's "Create Test User" API, which hands you a NINO and Government Gateway credentials to log in with during the OAuth flow.</p>
<h2 id="heading-common-gotchas-that-will-cost-you-an-afternoon">Common Gotchas that Will Cost You an Afternoon</h2>
<p>A few traps catch nearly everyone the first time:</p>
<h3 id="heading-1-pin-the-api-version-or-get-a-406">1. Pin the API Version, or Get a 406.</h3>
<p>Different HMRC APIs live at different versions: Business Details is on <code>2.0</code>, Obligations on <code>3.0</code>, the calculation APIs on later versions again. The version lives in the <code>Accept</code> header (<code>application/vnd.hmrc.3.0+json</code>). Send the wrong version, or forget the header, and HMRC replies <code>406 Not Acceptable</code>. When a call mysteriously fails, check the version first.</p>
<h3 id="heading-2-the-state-token-is-single-use">2. The <code>state</code> Token is Single-Use.</h3>
<p>Validate it, then delete it immediately, before you do anything else. If you leave it lying around, you've weakened the CSRF protection it exists to provide. Expire stale ones too.</p>
<h3 id="heading-3-dont-send-content-type-applicationjson-on-a-bodyless-get">3. Don't send <code>Content-Type: application/json</code> on a bodyless GET.</h3>
<p>This one is genuinely surprising. HMRC's edge (CloudFront) rejects a GET that carries a JSON <code>Content-Type</code> with no body as a <code>403 Bad request</code>, which can break every read endpoint at once. Only set <code>Content-Type</code> when there's an actual body to send, as in the snippet above.</p>
<h3 id="heading-4-subscribe-your-app-to-each-api">4. Subscribe Your App to Each API.</h3>
<p>As mentioned in step 1, an unsubscribed API returns <code>403</code>, which reads like an auth problem but is not.</p>
<h3 id="heading-5-the-redirect-uri-must-match-exactly">5. The Redirect URI Must Match Exactly.</h3>
<p>A trailing slash or <code>http</code> versus <code>https</code> mismatch produces an opaque error on the consent screen. Copy it, don't retype it.</p>
<h2 id="heading-where-to-go-next">Where to Go Next</h2>
<p>That's the whole spine: register a sandbox app and subscribe it, run the OAuth Authorization Code flow to get tokens, attach fraud prevention headers, and make versioned JSON calls.</p>
<p>From here, the rest of MTD is more of the same: submit the cumulative quarterly figures, trigger a tax calculation, read the result, and eventually submit the final declaration. Each is another endpoint with the same two gates you've already cleared.</p>
<p>The kindest thing I can tell a beginner is to lean on HMRC's own tooling: the sandbox, the test users, and especially the fraud-header validator. Drive each of those to a clean result and the integration stops feeling like a regulatory maze and starts behaving like an ordinary, if unusually careful, REST API.</p>
<p>I work on <a href="https://taptax.co.uk?utm_source=freecodecamp&amp;utm_medium=editorial&amp;utm_campaign=ebas_fcc_mtdapi">TapTax</a>, a Making Tax Digital app for UK sole traders, which is where I picked up the details above.</p>
<p><em>About the author: Solomon Amos is the founder of TapTax and built its HMRC Making Tax Digital integration. He has spent the last three years as a lead technical architect on HMRC digital modernisation programmes, and holds a PhD in engineering, with research in machine learning. You can find him on</em> <a href="https://www.linkedin.com/in/solomonudoh/"><em>LinkedIn</em></a><em>.</em></p>
 ]]>
                </content:encoded>
            </item>
        
            <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;" width="1014" height="1028" 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[ 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;" 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;" 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;" 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;" 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;" 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;" 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;" 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;" 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 Set Up GitHub OAuth in a Django App for User Authentication ]]>
                </title>
                <description>
                    <![CDATA[ Maintaining safe and frictionless user authentication is paramount in today's fast-changing web application landscape.  Among the many authentication methods available, GitHub OAuth has emerged as a useful tool for improving user login experience whi... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/set-up-github-oauth-on-django-for-user-authentication/</link>
                <guid isPermaLink="false">66c4c64d1b22d2d8d9040ec6</guid>
                
                    <category>
                        <![CDATA[ authentication ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Django ]]>
                    </category>
                
                    <category>
                        <![CDATA[ GitHub ]]>
                    </category>
                
                    <category>
                        <![CDATA[ oauth ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Sophia Iroegbu ]]>
                </dc:creator>
                <pubDate>Tue, 05 Dec 2023 21:19:14 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2023/11/Blog-Banner---Template.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Maintaining safe and frictionless user authentication is paramount in today's fast-changing web application landscape. </p>
<p>Among the many authentication methods available, GitHub OAuth has emerged as a useful tool for improving user login experience while strengthening security measures. </p>
<p><a target="_blank" href="https://docs.djangoproject.com/en/4.2/">Django</a>, a Python web framework, has recently gained popularity in web development due to its efficiency and versatility. Adding GitHub OAuth to your Django projects helps improve the authentication process. </p>
<p>Django developers can use GitHub OAuth to access a user's GitHub profile and (with permission) their repositories to personalize the user experience and tailor application services.</p>
<p>This article will walk you through how to implement GitHub OAuth. You'll see the benefits to your Django projects as we go. By embracing this technology, you can give users a seamless login experience while adhering to strict security standards.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>If you wish to follow this guide, you need to have a basic understanding of these tools or have them installed on your PC: </p>
<ul>
<li><a target="_blank" href="https://github.com/">GitHub</a></li>
<li><a target="_blank" href="https://docs.djangoproject.com/en/4.2/">Django</a></li>
<li><a target="_blank" href="https://www.django-rest-framework.org/">Django Rest Framework</a></li>
</ul>
<h2 id="heading-how-to-create-a-github-oauth-application">How to Create a GitHub OAuth Application</h2>
<p>You must sign into your GitHub account to create a GitHub OAuth application. </p>
<p>First, login to your GitHub account, click on your GitHub profile picture, and select Settings.</p>
<p><img src="https://lh7-us.googleusercontent.com/tLIRoSttp2_c3XAlLPzt_TbxCrGT70wcAubnY3ilywK9kxiGJ-z_5pzX3rDECRpTxKpXx61esK_NL5t1Jkg0kQNfMnvU6hhvfa7TRr9wVX0WyhQWhcvWivDbEQOqtehc87MPXzinHvY_da3IkORxFy8" alt="Image" width="532" height="1223" loading="lazy">
<em>Login to GitHub and select "Settings" from the sidebar menu</em></p>
<p>Then, once the new page comes up, scroll to the bottom and select Developer Settings.</p>
<p><img src="https://lh7-us.googleusercontent.com/jmWCI4fxgLc34a7tZhqXA1hvD6QnBTF1_ERfsq7VwleIuv21frXVxFyoeuIVPz-0SwAD3fJK8hTqIc8pTGaijVQrFUAUptYfGcUmljisdqjAlhQgElkXRb8iO4OeW9YyZ_DOYal-6bkDhL-5RYcvifY" alt="Image" width="1600" height="1149" loading="lazy">
<em>Scroll to the bottom and select "Develop Settings"</em></p>
<p>Select OAuth Apps and click on New OAuth App.</p>
<p><img src="https://lh7-us.googleusercontent.com/2loAs8jJILhusyITEk6v2XUher8kP5jBZWEWuUszfD0_C1vD56L6hlIsAwXL7gMV_8gR28T1Mthv_VrSZwqWo2MuIVKdH0SfGsFWBZcK1M3FbMD6JTdszf1v56sKQHcpDYDsu7VSbfg0DFeQCPI6Af8" alt="Image" width="1600" height="317" loading="lazy">
<em>Creating a new OAuth app</em></p>
<p>Define your OAuth application by naming it.</p>
<ul>
<li>The Homepage URL should be the URL that leads to the homepage of your website.</li>
<li>The Authorization callback URL should be a site, or a page users view after their GitHub account has been authenticated. </li>
</ul>
<p>Once you are done defining it, click Register application.</p>
<p><img src="https://lh7-us.googleusercontent.com/2jEfTNBil-Z5qCakfh8HkptyMm4Z8WOxsUfoN6T9nclv9soRmR4akgJJxuc52Xqzo2f3uBPZ6a_UMGJR8eukFdZk6HxSwPSdrPLG5m2n5NLRJXroCvr8_56DwWvHjtmi7KqZvga48RFbpry--FJq9zg" alt="Image" width="992" height="1101" loading="lazy">
<em>Register your application once all the info has been filled in</em></p>
<p>Next, you'll need a client secret and a client ID key to access your GitHub OAuth app on your Django project.</p>
<p>The Client ID is already defined once you create an application. Click Generate a new client secret to create a client secret key.</p>
<p><img src="https://lh7-us.googleusercontent.com/Fl2B2iqfYUejqWlb04TRUgN6XNP3m4IswS2JptoS-cVkQ4ft3SElu8xV0cF04buhrLdl3zRo6OEtvpg7rGnJ0Yj22KbmONEz0HWbjRRRk6R0H-XIN-hoaBQUjyQl_XPzcCAPCBFPEhcet7WcDwTrBoU" alt="Image" width="1600" height="819" loading="lazy">
<em>Creating a client secret key</em></p>
<p>You might be prompted to sign in. Do so to continue. </p>
<p><img src="https://lh7-us.googleusercontent.com/ceMI0FXuKACvZeA_S-RWYs2qjlCgkPzK9DbJtA6vIH6Nh5GvVHA66_rb9bHmtdxrM5VIzA3S6rpWbsCXURbrRjPrs4yHCLPttCC_9g1vNfQV5qeUN-eKAueE4EqKAmcvSThhJcav-53Jz1PsC7z4JMI" alt="Image" width="677" height="835" loading="lazy">
<em>Sign in to continue</em></p>
<p>Once you log in, your client's secret key will be generated. Copy and save it on your .env file.</p>
<p><img src="https://lh7-us.googleusercontent.com/Y0trI-EIYeKVBT_s3TSA7A-5FEkONt4fNfdUKqHXdBqsfxyxfnl5E9_DL02eynpj87i-cBworbxusUIRdNaH_qU_2TaKRDM1afpuBjVZBsaq-2GZyf4dz4sE43hjx24hknJwkHwkaiZOTDLBZvjxbHk" alt="Image" width="1477" height="818" loading="lazy">
<em>Copy your secret key to your .env file</em></p>
<p>Now that you’ve set up your GitHub OAuth application, let’s connect it to your Django project.</p>
<h2 id="heading-how-to-integrate-github-oauth-with-django">How to Integrate GitHub OAuth with Django</h2>
<p>This part will link your Django project's GitHub OAuth application to it using the social-auth app package. </p>
<p>First, install the <a target="_blank" href="https://dj-rest-auth.readthedocs.io/en/latest/">dj-rest-auth</a> package and define it on your settings.py.</p>
<pre><code class="lang-python">pip install dj-rest-auth
</code></pre>
<p>Then configure dj-rest-auth package on your settings.py.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Application definition</span>
INSTALLED_APPS = [
    <span class="hljs-string">"django.contrib.admin"</span>,
    <span class="hljs-string">"django.contrib.auth"</span>,
    <span class="hljs-string">"django.contrib.contenttypes"</span>,
    <span class="hljs-string">"django.contrib.sessions"</span>,
    <span class="hljs-string">"django.contrib.messages"</span>,
    <span class="hljs-string">"django.contrib.staticfiles"</span>,
    <span class="hljs-string">"django.contrib.sites"</span>,
    <span class="hljs-string">"rest_framework"</span>,
    <span class="hljs-string">"rest_framework.authtoken"</span>,
    <span class="hljs-string">"dj_rest_auth"</span>,
    <span class="hljs-string">"allauth"</span>,
    <span class="hljs-string">"allauth.account"</span>,
    <span class="hljs-string">"allauth.socialaccount"</span>,
    <span class="hljs-string">"allauth.socialaccount.providers.github"</span>,
    <span class="hljs-string">"oauth2_provider"</span>,
    <span class="hljs-string">"users"</span>,
]
</code></pre>
<p>You'll need to enable the authentication classes for dj-rest-auth by updating REST_FRAMEWORK and AUTHENTICATION_BACKENDS on your settings.py.</p>
<p>Optionally, you can configure <code>allauth</code> if you intend to use templates. Do this on your settings.py file. </p>
<pre><code class="lang-python">AUTHENTICATION_BACKENDS = (<span class="hljs-string">"allauth.account.auth_backends.AuthenticationBackend"</span>,)

REST_USE_JWT = <span class="hljs-literal">True</span>  <span class="hljs-comment"># Use JWT for authentication with dj-rest-auth</span>
SITE_ID = <span class="hljs-number">1</span> <span class="hljs-comment">#Set site ID</span>

SITE_ID = <span class="hljs-number">1</span>  <span class="hljs-comment"># Set the site ID</span>

<span class="hljs-comment"># Disable email verification for simplicity</span>
ACCOUNT_EMAIL_VERIFICATION = <span class="hljs-string">"none"</span>
LOGIN_REDIRECT_URL = <span class="hljs-string">"/"</span>  <span class="hljs-comment"># Redirect URL after successful login</span>
LOGOUT_REDIRECT_URL = <span class="hljs-string">"/"</span>  <span class="hljs-comment"># Redirect URL after logout</span>

SOCIALACCOUNT_PROVIDERS = {
    <span class="hljs-string">"github"</span>: {
        <span class="hljs-string">"APP"</span>: {
            <span class="hljs-string">"client_id"</span>: <span class="hljs-string">"YOUR_GITHUB_CLIENT_ID"</span>,
            <span class="hljs-string">"secret"</span>: <span class="hljs-string">"YOUR_GITHUB_SECRET_KEY"</span>,
            <span class="hljs-string">"key"</span>: <span class="hljs-string">""</span>,
            <span class="hljs-string">"redirect_uri"</span>: <span class="hljs-string">"http://localhost:8000/accounts/github/login/callback/"</span>,
        }
    }
}
</code></pre>
<p>A Django app is required for this guide. Let's call it users. Head over to the views of the Django app and define the following code:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> allauth.socialaccount.providers.github.views <span class="hljs-keyword">import</span> GitHubOAuth2Adapter
<span class="hljs-keyword">from</span> allauth.socialaccount.providers.oauth2.client <span class="hljs-keyword">import</span> OAuth2Client
<span class="hljs-keyword">from</span> dj_rest_auth.registration.views <span class="hljs-keyword">import</span> SocialLoginView

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">GitHubLogin</span>(<span class="hljs-params">SocialLoginView</span>):</span>
    adapter_class = GitHubOAuth2Adapter
    callback_url = CALLBACK_URL_YOU_SET_ON_GITHUB
    client_class = OAuth2Client

<span class="hljs-comment"># Define the urls.py on the Django app</span>
urlpatterns += [
    path(<span class="hljs-string">'github/'</span>, GitHubLogin.as_view(), name=<span class="hljs-string">'github_login'</span>)
    ]
</code></pre>
<h2 id="heading-how-to-set-up-a-new-application">How to Set Up a New Application</h2>
<p>To specify the GitHub OAuth credentials, you'll need to log into the Django social application model. This will provide your Django project with an additional degree of protection. Because of this, changing the OAuth credentials will be simple and won't damage your existing code.</p>
<p>Start by logging into your Django admin, clicking Social Applications, and selecting Add Application. This will prompt you to create a new application.</p>
<p><img src="https://lh7-us.googleusercontent.com/9XQcRYZfLG8NOw-2ODor7Zgn-6x5Voq9F4ToVdp0eVLdnLnbWVkbB4PUEh68p3DJk9yjkKQf592_kDjipQQqHnpn7jeneWfu2X7Z4I2_n0wsltX5rGvbFSmyQteuDaXLUjWnNTBzDDJic6XQ8goBSd8" alt="Image" width="1600" height="812" loading="lazy">
<em>On Django admin, select "Social applications"</em></p>
<p>You will be prompted to enter information on the new page.</p>
<ul>
<li>Select GitHub as the provider.</li>
<li>Give your social app a name.</li>
<li>Enter the Client Secret and Client ID created from your GitHub OAuth app.</li>
<li>Select the site on Available sites and move it to Chosen sites. Once done, click Save. This will create a new application.</li>
</ul>
<p><img src="https://lh7-us.googleusercontent.com/pStq_1opKb7rkVNCqO2ouCfd2ZBLHFEwoxfWuHabFG12nT5v35NkXYSOH6Su2d_fISvwmO7LpCTfPsDK0EmmLUUvNYynoAgjuvsogP4Ee0xNBfIU_ai4TtXzzHZPFq2U0C3eFQfNCfXSuIoWup6PeCo" alt="Image" width="1600" height="1028" loading="lazy">
<em>Create/Add a Social application</em></p>
<p>Change your site domain to localhost since this is still the development phase.</p>
<p><img src="https://lh7-us.googleusercontent.com/pRKpPs06V2j5ciPCLZRCd5weyc3X5HOGgWXhen_GS9-DhItBkkVJFYe6jBd3QmWMRwfBPagYxh6r1PRXHVeM_M3X6xWeq0lRKYM0GbVKDMlZS7hIVz4oAF6M6lMxYUGF5ZuuPwQyUF-1lfidzJPw79E" alt="Image" width="1600" height="968" loading="lazy">
<em>Select, "Site" to change the existing the site domain</em></p>
<p>Select example.com and change it to http://127.0.0.1:8000/ then Save.</p>
<p><img src="https://lh7-us.googleusercontent.com/ikQ9_lhABi-avsKcoIqH98znI3aJKN4RkZqGfYQwio8nujR0M1kEewfBYdekhVkQMqYQi5APqsqxqpEkbX78wFS8dw76tGH11eEQ2qqTzCuabzgx5qD85SPBgkVtyJVEUui4RAKR_y2Dr65dEiyh7P4" alt="Image" width="1600" height="551" loading="lazy">
<em>Change your site domain from example.com to http://127.0.0.1:8000/</em></p>
<h2 id="heading-how-to-test-the-defined-social-app">How to Test the Defined Social App</h2>
<p>Once you are done defining and setting up the social Django app on your project, you will need to test it and make sure that it works. </p>
<p>If you open the route http://127.0.0.1:8000/auth/github/, you'll need to enter some information such as Access token, ID token, and code. </p>
<p>We'll manually get this information, as the front end is meant to get and parse this information.</p>
<p><img src="https://lh7-us.googleusercontent.com/eLkjj3nueZUf26fcORK9iJSvSyKNiO_ZgvfD9vFbF2momnDka6dVxngCQSKY9VwWcHJDduKDhGXhYsbimtSGZL5uzjrherU6bDXUFDfu5Bys1wylda6WZCOZsotH7ENkZAsHEYbhbImbx9JmbRKCtYM" alt="Image" width="1600" height="791" loading="lazy">
<em>Go to the GitHub registration page to test the defined social app</em></p>
<p>To do this, head over to https://github.com/settings/apps and select Personal access tokens then select Generate new token. Use the second option, Generate new token (classic), since this guide is focused on authenticating users just to get their GitHub user info.</p>
<p><img src="https://lh7-us.googleusercontent.com/E8bb0KrJ0IwoTqCf2f2WVMoycUNad3YuqQZnG6heWwpNh3euYesNjx_ipRAOxYZyGfT-DShM1OyIOznVByQCWqsFrllTXO-FQEUIYPKLbjcbCBrp6vsN_XLlZJhaB3ZaxyBmWGiTMfWD5vjq0VWEq5g" alt="Image" width="1600" height="434" loading="lazy">
<em>On GitHub settings, select "Personal access tokens" then select "Generate new token".</em></p>
<p>Give the token a name and select scopes. Ensure you check all the user scopes.  Then generate a new token.</p>
<p><img src="https://lh7-us.googleusercontent.com/ZLkgWxuIQ1y-jZ3Pere1I-cmDIlwS032kQ0i5bvYufflVfnjhezcgNRqY-UpnJMPbJZY1RcdKApbTz579_DqR-Cs2M6ba3gTcaS6H2utA9JVkW2KVVXqsDjGwItruyBKpktd8TvlIDzVvlgQqh-RqUE" alt="Image" width="1600" height="942" loading="lazy">
<em>Define your new token</em></p>
<p>Your new token should look like this. Ensure you store it somewhere safe. </p>
<p><img src="https://lh7-us.googleusercontent.com/ywiFWxHFRJQwVZpGS-ePV7qR6YNIVi7gh3OoL9HgJvHc7TWHiSevr_Hmc8TRXbNxv0VAwwdt71O3PVchsLuRIlM9nbvhzj8X4IWBtgAjx17M8yYGApqxgBlU1lKeQYg8xwdwCg1PchuqiLuyj8YRCAg" alt="Image" width="1484" height="723" loading="lazy">
<em>Copy the generated personal access token generated by GitHub.</em></p>
<p>Head over to http://127.0.0.1:8000/auth/github/. Enter the access token generated and voilà!  Your user access token and GitHub username will be sent as a response on the body. </p>
<p><img src="https://lh7-us.googleusercontent.com/jd736d5yvsSPhvXCNX21CGHEzTdMhUerN4HVst57iOVqisAejH_T35D7AwKGGHgkJCtkzfkn4ut0YP2vxpYZgSa7ITEqaR2Wqw0J4qxWeIug0ciCEFM4GnDK-DjfooYRzg1sbU1z8cyFMtwRmgMG_bs" alt="Image" width="1600" height="855" loading="lazy">
<em>Using your token and GitHub username ensure the backend system works</em></p>
<h2 id="heading-how-to-implement-oauth-authentication-flow">How to Implement OAuth Authentication Flow</h2>
<p>You have successfully installed and tested the dj-rest-auth package. Next, you will learn how to test the OAuth flow and how it obtains user data from GitHub.</p>
<p>To test the GitHub OAuth authentication flow, you'll need to send a request to https://github.com/login/oauth/authorize.</p>
<p>You can do so by either using curl:</p>
<pre><code class="lang-python"><span class="hljs-string">"https://github.com/login/oauth/authorize?client_id=YOUR_CLIENT_ID&amp;amp;redirect_uri=http://127.0.0.1:8000/auth/callback/&amp;amp;scope=user"</span>
</code></pre>
<p>(and making sure you use the same redirect URI defined in your GitHub OAuth application) or opening https://github.com/login/oauth/authorize?client_id=YOUR_CLIENT_ID&amp;redirect_uri=http://127.0.0.1:8000/auth/callback/&amp;scope=user on your browser. It will redirect you to an authorization page. </p>
<p><img src="https://lh7-us.googleusercontent.com/rJbNTlLFz8h-dMJNgeeMCX-kT-Y_Ofv-1Po0wNp2qZQVH_e6syyabIasdrjWzDDdtF6NQ-2o2oDxv_KX2wYmoUb7OiYcZGz66sbzNjfpfB0P3asAFh4oPV7OvybcQ4OYtiGKAUNYYvqAUt7H1-sn7mM" alt="Image" width="919" height="1164" loading="lazy">
<em>Send a request to GitHub OAuth authentication flow</em></p>
<p>Click Authorize YOUR USERNAME to authorize the user.</p>
<p><img src="https://lh7-us.googleusercontent.com/8GaW8izjQgcqd-9IbCeyDZcdnajXKBDETOIiZ5P2s3iziZQMUvROKmJQuJBDvmPdpAEEhSCCB_xdy1NSkgEEqU2o18lmwsbo8Eay8IYzKL-HJCKoB40ySLE9-vl3g5CLtMyuzmSwQy9u_fyI2iqfuwg" alt="Image" width="991" height="307" loading="lazy">
<em>Click "Authorize" to authorize your GitHub user</em></p>
<p>You will be redirected to a URL showing the code. With the code, you can generate the access token needed to authenticate the user.</p>
<p><img src="https://lh7-us.googleusercontent.com/_RO4IqpLY0-zg8SiW7SXKk1gTCkJq_bVqIrDzH4_tzqWSzHUArqsQlDYSqzFHiGfxdyPpSXW5psYKnZVyHPgnDbETBncgpIrWxZAWc1RjQvcGmi5QRN5XpOyOxuy5n5DqiJkSJO8c0VizOFh3h-tqy4" alt="Image" width="1523" height="159" loading="lazy">
<em>Use the code from GitHub to generate an access token for your auth</em></p>
<p>To get the access token, send a request to this URL: https://github.com/login/oauth/access_token?client_id=YOUR_CLIENT_ID&amp;client_secret=YOUR_CLIENT_SECRET&amp;code=CODE </p>
<p>Or you can choose to use curl to send the request. </p>
<pre><code class="lang-curl">"https://github.com/login/oauth/access_token?client_id=YOUR_CLIENT_ID&amp;amp;client_secret=YOUR_CLIENT_SECRET&amp;amp;code=CODE"
</code></pre>
<p>This should either download the access token for you or return it as a response body depending on how you defined it. The response should look like this:</p>
<p><img src="https://lh7-us.googleusercontent.com/ZIn4u5kqdW3P7o27ReEsAyc-X9R2O28Bm2qfjDh0saywx7vpso41OLoOldHzii4AbnQe-jfqT__4aELgchdXUSQPIR6I86-KdOyZL4hrcFI38YBOjX27IbH2NNtS7SWS7hAFNTroZfVF17s8xoI0lBk" alt="Image" width="1336" height="213" loading="lazy">
<em>Your response from your backend authentication system</em></p>
<p>Now, with this token, you can authenticate your user when they make any request. </p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In conclusion, setting up GitHub OAuth on Django is a helpful way to enable users to log in to your web applications using their GitHub credentials. </p>
<p>By following this guide, you can enhance your application's security and access your users' data which improves the user experience of your Django app.   </p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Set Up a GitHub OAuth Application ]]>
                </title>
                <description>
                    <![CDATA[ GitHub is an incredibly useful OAuth provider, especially if you are building an application targeted toward developers.  In this article, we will give you a quick rundown of how to set up a GitHub OAuth application. Create Your Application Begin by ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-set-up-a-github-oauth-application/</link>
                <guid isPermaLink="false">66ac7f3d23cc28a03a55e088</guid>
                
                    <category>
                        <![CDATA[ freeCodeCamp.org ]]>
                    </category>
                
                    <category>
                        <![CDATA[ GitHub ]]>
                    </category>
                
                    <category>
                        <![CDATA[ oauth ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Naomi Carrigan ]]>
                </dc:creator>
                <pubDate>Thu, 27 Oct 2022 21:31:36 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2022/10/pexels-george-becker-333837--1-.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>GitHub is an incredibly useful OAuth provider, especially if you are building an application targeted toward developers. </p>
<p>In this article, we will give you a quick rundown of how to set up a GitHub OAuth application.</p>
<h2 id="heading-create-your-application">Create Your Application</h2>
<p>Begin by navigating to your GitHub settings (make sure you are logged in!). Scroll down to the bottom of the sidebar and click "Developer Settings".</p>
<p>This will take you to the application page:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/image-230.png" alt="GitHub OAuth Apps view, showing a hacktoberfest and mattermost application that have been previously authorised." width="600" height="400" loading="lazy">
<em>You may see some applications you've previously authorised.</em></p>
<p>Click the "New OAuth App" button to create a new application.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/image-232.png" alt="The new OAuth application page, showing form fields for Application name, homepage URL, application description, and authorisation callback URL." width="600" height="400" loading="lazy"></p>
<p>Fill in the form and click "Register application". This will create your application and take you to the settings page.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/image-233.png" alt="The Application settings page, which shows the same form fields as the previous form, with additional options to transfer ownership, revoke user tokens, generate client secrets, and upload a logo." width="600" height="400" loading="lazy"></p>
<p>For OAuth applications, you will need the Client ID. You will also need to generate a client secret. Click the "Generate a new client secret" to do so.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/10/image-234.png" alt="The new client secret (obfuscated for security in this image)" width="600" height="400" loading="lazy"></p>
<p>Make sure to save this secret in a secure location as you will not be able to view it again.</p>
<h2 id="heading-using-your-new-application">Using Your New Application</h2>
<p>Now that you have a client ID and secret, you can use your OAuth application in your project. </p>
<p>If you want to learn how to do so, <a target="_blank" href="https://www.freecodecamp.org/learn/quality-assurance/#advanced-node-and-express">freeCodeCamp's curriculum can teach you</a>. </p>
<p>Happy coding!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Use Social Login with OAuth to Make Your Apps More Secure ]]>
                </title>
                <description>
                    <![CDATA[ Many developers have written a demo login application at some point in time. We all start with the simple user defined ID and password. We then try to implement something like a social login with, say, Google or Twitter. There is, of course, a more c... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-social-login-with-oauth-for-more-secure-apps/</link>
                <guid isPermaLink="false">66bb458361078de848a7e0e4</guid>
                
                    <category>
                        <![CDATA[ Application Security ]]>
                    </category>
                
                    <category>
                        <![CDATA[ oauth ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Rohit Jacob Mathew ]]>
                </dc:creator>
                <pubDate>Fri, 29 Oct 2021 14:50:00 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2021/10/MzBKTcnJA.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Many developers have written a demo login application at some point in time. We all start with the simple user defined ID and password. We then try to implement something like a social login with, say, Google or Twitter.</p>
<p>There is, of course, a more complex process involved in setting up social login, but for a user it's as simple as clicking a button to log in. </p>
<p>The ease of not having to remember an ID/password and just being able to signup/login through the click of a button is extremely beneficial to the user.</p>
<h2 id="heading-what-if-i-told-you-this-was-way-more-secure">What if I Told You This Was Way More Secure? 😉</h2>
<p>Social logins really help us achieve a few things:</p>
<ul>
<li>Support for multiple devices</li>
<li>Single Sign On</li>
<li>Simple to implement</li>
<li>The ability to share data for users without having to release personal information</li>
<li>Ability revoke an active session i.e not allow a third party access to the login and data</li>
<li>There are no long-lasting credentials being exchanged</li>
</ul>
<h2 id="heading-so-what-technology-drives-social-login">So What Technology Drives Social Login? 🤔</h2>
<p>The underlying protocol used is something called <a target="_blank" href="https://oauth.net/">OAuth</a>. It is defined as:</p>
<blockquote>
<p>An open protocol to allow secure authorization in a simple and standard method from web, mobile, and desktop applications.</p>
</blockquote>
<p>Now with a basic understanding of social login and the above definition you probably have some idea of how this works – but let me use a simple example to explain how to use OAuth.</p>
<p>I remember my friend <a target="_blank" href="https://twitter.com/lunatic_monk">Sumedh</a> describing it as an interaction between a Mother, Father, and their Son. Imagine that the mother wants some groceries from the market and she wants the son to buy them for her.</p>
<p>Before I go into the conversation let me set some context.</p>
<blockquote>
<p><strong>Mother:</strong> The user of the application</p>
<p><strong>Son:</strong> Third party client or in technical terms the OAuth Client</p>
<p><strong>Father:</strong> The Social Account or in technical terms the OAuth Provider</p>
</blockquote>
<p>The conversation could possibly go like this:</p>
<blockquote>
<p><strong>Mother:</strong> Hey son, go to the market and bring me some coffee powder. Take the required money from your father.</p>
<p><strong>Son:</strong> Okay.</p>
<p><em>Son (OAuth client) goes to father (OAuth provider)</em></p>
<p><strong>Son:</strong> Hey dad, mom told me to take money from you since she wants some things from the market.</p>
<p><em>Father (OAuth provider) asks mother (User) about the permission to give money to their son (OAuth client)</em></p>
<p><strong>Father:</strong> Hey, shall I give him the money and how much?</p>
<p><em>Authorization of your application takes place here.</em></p>
<p><strong>Mother:</strong> Yes, please give it to him.</p>
<p><em>Permission grant by mother (User)</em></p>
<p><em>Son (OAuth client) gets the required things from the market and returns them to mother (User). Here returning things to mother (User) can be thought of redirecting the user (or logging them) into the third party site.</em></p>
</blockquote>
<p>For a more technical understanding of how this works in code, <a target="_blank" href="https://twitter.com/schneems">Richard Schneeman</a> has this amazing video below:</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/tFYrq3d54Dc" 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>
<h2 id="heading-now-lets-put-all-of-this-in-context">Now Lets Put All of This in Context</h2>
<p>Let's take as an example <a target="_blank" href="https://dev.to/">the DEV Community</a>. If you wanted to create an account on the DEV Community using Twitter, what would happen?</p>
<p>Basically, if the "Sign up with Twitter" button exists, then the initial setup between the OAuth Client (Dev.to) and the OAuth Provider (Twitter) is already done.</p>
<p>The Client triggers a permission granting page for the OAuth Provider based on the credentials it receives from the initial setup. This looks something like below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1622980489496/IrLawupb6.png" alt="Permission Grant Page" width="1398" height="1106" loading="lazy"></p>
<p>Once you login and grant permission, the OAuth Provider redirects you back to the client and the client gets a token to access your information from the OAuth Provider. This access token enables the client to get specific data from the provider</p>
<p>Based on that data the client then creates an account and logs you in</p>
<h3 id="heading-what-happens-on-successive-login">What Happens on Successive Login?</h3>
<p>Thats a good question. Now OAuth has multiple grant types, and based on that we have different ways to get an access token from the OAuth Provider. </p>
<p>For all subsequent logins, the OAuth Client will hit the provider and generate a new access token to get access to the data and do the login.</p>
<p>Thus this enables us to achieve Single Sign On, the ability to share data for users without having to release personal information, the ability to revoke access, and the ability to not have long lasting credentials exchanged.</p>
<p>This all leads to a more secure experience.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>I hope this short blog post helps you understand why social logins are more secure than the traditional username/password option. I will be writing about the different OAuth Grant types in the future and will be providing code examples as well.</p>
<p>Thanks for reading! I really hope that you find this article useful. I'm always interested to know your thoughts and happy to answer any questions you might have in your mind. If you think this post was useful, please share it to help promote this piece to others.</p>
<p>Thanks for reading! :)</p>
<p>P.S Do feel free to connect with me on <a target="_blank" href="https://www.linkedin.com/in/rohitjmathew">LinkedIn</a> or <a target="_blank" href="https://twitter.com/iamrohitjmathew">Twitter</a></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Brilliant Add-on For Static Sites That Will Make You Dance ]]>
                </title>
                <description>
                    <![CDATA[ By Jared Wolff This post is originally from www.jaredwolff.com Privacy. Performance. Brilliant looks. Can you have all three? (Of course!) Having a statically generated blog is great. Many folks use services like Disqus and Google Analytics to make t... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-setup-worry-free-blog-comments-in-20-simple-steps/</link>
                <guid isPermaLink="false">66d8505639c4dccc43d4d4a7</guid>
                
                    <category>
                        <![CDATA[ blog ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker compose ]]>
                    </category>
                
                    <category>
                        <![CDATA[ nginx ]]>
                    </category>
                
                    <category>
                        <![CDATA[ oauth ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Static Site Generators ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Mon, 08 Jul 2019 12:30:00 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2019/07/Copy-of-Static-Site-Docker-Recipes-2.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Jared Wolff</p>
<p><strong>This post is originally from <a target="_blank" href="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/">www.jaredwolff.com</a></strong></p>
<p>Privacy.</p>
<p>Performance.</p>
<p>Brilliant looks.</p>
<p>Can you have all three?</p>
<p>(Of course!)</p>
<p>Having a statically generated blog is great. Many folks use services like Disqus and Google Analytics to make them even better. Not surprising if you were one of them!  Privacy concerns are are the forefront of everyone’s attention. So, rather than keeping the status quo, it’s time to do something about it!</p>
<p><strong>If you've been looking to protect your site visitor's privacy and improve performance this blog post is for you.</strong></p>
<p>In this article we'll be using DigitalOcean’s Docker droplet. It allows you to host several different applications/services on one (virtual) machine. By the end of it you'll know how to run your own comments server using Commento. Plus i’ll share a few tricks i’ve learned along the way to make it much easier for you.</p>
<p>Leeeets go!</p>
<h2 id="heading-reverse-proxy">Reverse Proxy</h2>
<p>One of the most important aspects of this setup is the reverse proxy. A reverse proxy acts like a router. Requests come in for a certain domain.  That request is then routed to the service associated with that domain.</p>
<p>Here’s a diagram from the Nginx Reverse Proxy + Let’s Encrypt Helper documentation. It'll help illustrate the idea.</p>
<p><img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/webproxy-1f1c7540-4b86-4478-bb3e-f05043d671a5.jpg" alt="Nginx Reverse Proxy with Let's Encrypt" width="730" height="334" loading="lazy"></p>
<p>Another benefit is that there’s an extra layer of protection to the outside world. Your websites run in a private network and the only access is through the Nginx reverse proxy. Point your DNS to the server and Nginx handles all the magic.</p>
<p>Here's how to get it setup:</p>
<ol>
<li>Go ahead and set up your Digital Ocean Droplet. <a target="_blank" href="https://marketplace.digitalocean.com/apps/docker">All the info you need is right here</a>. The $5 version is more than sufficient.</li>
<li><p><a target="_blank" href="https://github.com/evertramos/docker-compose-letsencrypt-nginx-proxy-companion">Go here to clone the repository.</a> You can also run this in your terminal. Make sure you SSH into your Digital Ocean droplet first!</p>
<p>     git clone git@github.com:evertramos/docker-compose-letsencrypt-nginx-proxy-companion.git</p>
</li>
<li><p>Change directories to the cloned repository.</p>
</li>
<li>Copy <code>.env.sample</code> to <code>.env</code> and update the values inside. I had to change the <code>IP</code> value to the IP of my Digital Ocean Droplet. I left all the other ones alone.</li>
<li>Run <code>docker-compose up -d</code> to start everything. (you can run without the <code>-d</code> option to make sure everything starts ok. Or you can attach the log output using <code>docker container logs -f &lt;container name</code></li>
</ol>
<p>When pointing your sub-domains to this server, make sure you use an A record. Here's an example of mine:</p>
<p><img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_6-9c0432cd-4d40-4c89-88f3-24037d915eaf.52.32_PM.png" alt="NS1 A Record Configuration" width="730" height="581" loading="lazy"></p>
<p>Depending on your DNS provider, you'll have to figure out how to set an A record. That is beyond the purpose of this article though!</p>
<h2 id="heading-setting-up-commento-with-docker-compose">Setting Up Commento with Docker Compose</h2>
<p><img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Compose-1c868832-6819-43e2-8696-ab698a10dbee.jpg" alt="Commento Logo with Docker Logo" width="730" height="486" loading="lazy"></p>
<p>Here is the current docker compose file i'm using for Commento. It includes a few more environment variables for configuring Github, Gitlab and Google. It also includes the environment variables for setting the SMTP settings. These parameters are important. Otherwise you can't receive password reset or moderation emails!</p>
<p>    version: '3'</p>
<p>    services:
      commento:
        image: registry.gitlab.com/commento/commento
        container_name: commento
        restart: always
        environment:
          COMMENTO_ORIGIN: https://${COMMENTS_URL}
          COMMENTO_PORT: 8080
          COMMENTO_POSTGRES: postgres://postgres:postgres@postgres:5432/commento?sslmode=disable
          COMMENTO_SMTP_HOST: ${SMTP_HOST}
          COMMENTO_SMTP_PORT: ${SMTP_PORT}
          COMMENTO_SMTP_USERNAME: ${SMTP_USERNAME}
          COMMENTO_SMTP_PASSWORD: ${SMTP_PASSWORD}
          COMMENTO_SMTP_FROM_ADDRESS: ${SMTP_FROM_ADDRESS}
          COMMENTO_GITHUB_KEY: ${COMMENTO_GITHUB_KEY}
          COMMENTO_GITHUB_SECRET: ${COMMENTO_GITHUB_SECRET}
          COMMENTO_GITLAB_KEY: ${COMMENTO_GITLAB_KEY}
          COMMENTO_GITLAB_SECRET: ${COMMENTO_GITLAB_SECRET}
          COMMENTO_GOOGLE_KEY: ${COMMENTO_GOOGLE_KEY}
          COMMENTO_GOOGLE_SECRET: ${COMMENTO_GOOGLE_SECRET}
          COMMENTO_TWITTER_KEY: ${COMMENTO_TWITTER_KEY}
          COMMENTO_TWITTER_SECRET: ${COMMENTO_TWITTER_SECRET}
          VIRTUAL_HOST: ${COMMENTS_URL}
          VIRTUAL_PORT: 8080
          LETSENCRYPT_HOST: ${COMMENTS_URL}
          LETSENCRYPT_EMAIL: ${EMAIL}
        depends_on:</p>
<ul>
<li>postgres
networks:</li>
<li>db_network</li>
<li><p>webproxy</p>
<p>postgres:
image: postgres
container_name: postgres
environment:
POSTGRES_DB: commento
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
networks:</p>
</li>
<li>db_network
volumes:</li>
<li><p>postgres_data_volume:/var/lib/postgresql/data</p>
<p>networks:
db_network:
webproxy:
external: true</p>
<p>volumes:
postgres_data_volume:</p>
</li>
</ul>
<p>To set the environment variables, put them inside an <code>.env</code> file. Make sure the <code>.env</code> file is in the same directory as <code>docker-compose.yml</code>. When you run <code>docker-compose up</code> it will apply the variables set in the <code>.env</code> file. Nothing gets set if they're left blank.</p>
<p>Set the required <code>COMMENTS_URL</code> and <code>EMAIL</code> or you may run into problems. The best way to set these is by pacing them in the <code>.env</code> file. Here is an example:</p>
<p>    COMMENTS_URL=comments.your.url
    EMAIL=you@your.url</p>
<h2 id="heading-getting-oauth-key-amp-secret">Getting OAuth Key &amp; Secret</h2>
<p>Commento works with most popular OAuth providers. Thus visitors can leave comments without making an account.</p>
<p>The instructions are similar for each. I've outlined the steps for all of them below.</p>
<h3 id="heading-twitter">Twitter</h3>
<ol>
<li><p>Login to <a target="_blank" href="http://twitter.com">Twitter.com</a> and apply for a developer account: <a target="_blank" href="https://developer.twitter.com/en/application/use-case">https://developer.twitter.com/en/application/use-case</a></p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_6-4171cdf7-6c2b-408b-bb64-57822ede91cb.26.08_PM.png" alt="Twitter API Access" width="730" height="581" loading="lazy"></p>
</li>
<li><p>Describe how you'll use the API. You can use what I wrote.</p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_6-4c0aecf2-c020-4005-bd5f-81e3b4ac6b8f.28.43_PM.png" alt="How will you use the API?" width="730" height="581" loading="lazy"></p>
</li>
<li><p>Double check your entry and click <strong>Looks Good!</strong></p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_6-ade63510-86d3-48a4-a121-221f6e14cd96.28.50_PM.png" alt="Is everything correct?" width="730" height="581" loading="lazy"></p>
</li>
<li><p>Agree to the terms of service.</p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_6-2e8e3089-bd51-4d27-8573-6987aafc663e.28.59_PM.png" alt="Agree to Developer Agreement" width="730" height="581" loading="lazy"></p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_6-145b1bfd-9fc7-4ea6-ba5f-032e59d7fe8d.41.47_PM.png" alt="You did it!" width="730" height="581" loading="lazy"></p>
</li>
<li><p>They'll tell you to check your email for a confirmation. Confirm your email and you should be able to create your first app!</p>
</li>
<li><p>Once approved to to <strong>Get started</strong> click <strong>Create an app</strong>.</p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_6-640686b8-15c6-4af0-b9df-65ce15ae0fe7.29.22_PM.png" alt="Create an app" width="730" height="581" loading="lazy"></p>
</li>
<li><p>Next screen, again click <strong>Create an app</strong></p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_6-de2b85d5-8bb7-428f-bfd1-2a23d0b7d4e0.29.26_PM.png" alt="Create an app" width="730" height="581" loading="lazy"></p>
</li>
<li><p>Enter all the appropriate details. For the callback URL, use <a target="_blank" href="https://comments.jaredwolff.com/api/oauth/google/callback"><code>https://&lt;your URL&gt;/api/oauth/github/callback</code></a> where <a target="_blank" href="https://comments.jaredwolff.com/api/oauth/google/callback"><code>&lt;your URL&gt;</code></a> is your Commento subdomain.</p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_6-91acb343-9dee-4917-be77-9704fe439722.32.44_PM.png" alt="App details" width="730" height="581" loading="lazy"></p>
</li>
<li><p>Finally, once you're done filling out the information to go the <strong>Keys and Token</strong>s area. Save both the key and token. Enter them into the <code>.env</code> file. You can use <code>COMMENTO_TWITTER_KEY</code> and <code>COMMENTO_TWITTER_SECRET</code></p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_6-b910e9ff-dc34-45e8-94df-affb06702617.33.07_PM.png" alt="Get oauth key and secret" width="730" height="581" loading="lazy"></p>
</li>
</ol>
<h3 id="heading-gitlab">Gitlab</h3>
<ol>
<li>Login to <a target="_blank" href="http://gitlab.com">Gitlab.com</a> and go to to top right and click <strong>Settings</strong></li>
<li><p>Then click on <strong>Applications</strong></p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_12-c6da9d02-2052-4fa4-89de-d5212b8f49ca.56.47_PM.png" alt="Gitlab profile" width="730" height="445" loading="lazy"></p>
</li>
<li><p>Enter a name for your app. I put <strong>Commento</strong>.</p>
</li>
<li>Set the Redirect URI to <a target="_blank" href="https://comments.jaredwolff.com/api/oauth/google/callback"><code>https://&lt;your URL&gt;/api/oauth/gitlab/callback</code></a></li>
<li><p>Select the <strong>read_user</strong> scope.</p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_12-e616c338-6144-4704-93c6-914db6fad5f6.59.15_PM.png" alt="Gitlab add application" width="730" height="500" loading="lazy"></p>
</li>
<li><p>Click the green <strong>Save Application</strong> button</p>
</li>
<li><p>Copy the <strong>Application ID</strong> and <strong>Secret</strong> and place them in your <code>.env</code> file using <code>COMMENTO_GITLAB_KEY</code> and <code>COMMENTO_GITLAB_SECRET</code></p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_1-a4f4ab4a-9fd6-423f-821c-6ff2f174e589.04.10_PM.png" alt="Application key and secret" width="730" height="689" loading="lazy"></p>
</li>
</ol>
<h3 id="heading-github">Github</h3>
<ol>
<li>To get your OAuth key and secret, you'll need to go to this URL: <a target="_blank" href="https://github.com/settings/developers">https://github.com/settings/developers</a></li>
<li><p>Once there, click on <strong>New OAuth App</strong></p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-04_at_9-18bf8f23-916f-476b-8c25-3377de931fe3.15.33_AM.png" alt="Add OAuth application" width="730" height="562" loading="lazy"></p>
</li>
<li><p>Enter your details. For the callback URL, use <a target="_blank" href="https://comments.jaredwolff.com/api/oauth/google/callback"><code>https://&lt;your URL&gt;/api/oauth/github/callback</code></a> where <a target="_blank" href="https://comments.jaredwolff.com/api/oauth/google/callback"><code>&lt;your URL&gt;</code></a> is your Commento subdomain.</p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-04_at_9-6e616334-7123-4de4-a4fd-f2fe319b1971.28.24_AM.png" alt="Register new OAuth application" width="730" height="585" loading="lazy"></p>
<p> <em>Note: Make sure you include <code>https</code> in your URLs.</em></p>
</li>
<li><p>Grab the <strong>Client ID</strong> and <strong>Client secret</strong> and put that into your <code>.env</code> file using <code>COMMENTO_GITHUB_KEY</code> and <code>COMMENTO_GITHUB_SECRET</code></p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-04_at_9-7505a3ef-386a-4b75-a7dc-1dd3e22d0baf.29.28_AM.png" alt="Application created successfully" width="730" height="585" loading="lazy"></p>
</li>
</ol>
<h3 id="heading-google">Google</h3>
<p>Setting up Google is just about as tedious to set up as Twitter. Despite how scary I just made it out to be, it's completely doable. Here are the steps.</p>
<ol>
<li>Go to this URL: <a target="_blank" href="https://console.developers.google.com/cloud-resource-manager?previousPage=%2Fapi">Google Developer Console</a></li>
<li><p>Create a new project</p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-04_at_8-f3793926-cc54-4345-b81c-5ec0f4631a35.42.48_AM.png" alt="Create a new project" width="730" height="588" loading="lazy"></p>
</li>
<li><p>Click the <strong>GoogleAPIs logo</strong> in the top left corner to go back once you have a project. (Make sure the dropdown next to the <strong>GoogleAPIs logo</strong> is the same as your new project!)</p>
</li>
<li>Then, click <strong>Credentials</strong> on the left side.</li>
<li><p>Update the <strong>Application Name</strong> and <strong>Authorized Domains</strong> in the <strong>OAuth consent screen</strong></p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-04_at_8-d839a5c9-3368-4f18-b674-73b6e4e7c17c.47.15_AM.png" alt="Setup application" width="730" height="499" loading="lazy"></p>
</li>
<li><p>Click <strong>Create credentials</strong> then <strong>OAuth client ID</strong></p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-04_at_8-201545f9-4d47-4e0c-ae9a-b40efdc35a4b.44.36_AM.png" alt="Setup credentials" width="730" height="545" loading="lazy"></p>
</li>
<li><p>On the <strong>Create OAuth client ID</strong> enter the subdomain associated with Commento to <strong>Authorized Javascript origins.</strong> Then, enter the full callback URL. For example <a target="_blank" href="https://comments.jaredwolff.com/api/oauth/google/callback"><code>https://comments.jaredwolff.com/api/oauth/google/callback</code></a>. Make it yours by replacing <code>comments.jaredwolff.com</code> with your URL.</p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-04_at_8-fdba3491-d562-41f3-acff-2857ea816cec.52.15_AM.png" alt="Create OAuth Client ID" width="730" height="706" loading="lazy"></p>
<p> Once entered, click the <strong>create</strong> button.</p>
</li>
<li><p>Grab the <strong>client ID</strong> and <strong>client secret</strong></p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-04_at_8-0c3f2895-0cb9-4b3a-a154-a3d80fd9716a.57.40_AM.png" alt="OAuth Credentials" width="730" height="706" loading="lazy"></p>
</li>
<li><p>Update your <code>.env</code> file using <code>COMMENTO_GOOGLE_KEY</code> and <code>COMMENTO_GOOGLE_SECRET</code></p>
</li>
</ol>
<h2 id="heading-install-your-application">Install your application</h2>
<p>You've entered your OAuth Credentials email, domain and SMTP credentials. It's time to wrap this show up!</p>
<ol>
<li>Once you're done editing your <code>.env</code> file. Run <code>docker-compose up</code> (For files not named <code>docker-compose.yml</code>, use the <code>-f</code> flag. Example: <code>docker-compose -f commento.yml up</code></li>
<li>Watch the output for errors. If it looks good you may want to kill it (<strong>CTRL+C</strong>) and run with the <code>-d</code> flag</li>
<li><p>On first start, Commento will prompt you with a login screen.</p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_12-d5a1ca53-93b3-49c5-a3a7-e8b728259e2d.11.29_PM.png" alt="Commento Login" width="730" height="545" loading="lazy"></p>
</li>
<li><p>Create a new account by clicking <strong>Don't have an account yet? Sign up.</strong></p>
</li>
<li>Enter your information and click <strong>Sign Up</strong></li>
<li><p>Check your email and click the included link:</p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_12-e263aa4f-201b-42ac-986c-b28c5f003f38.12.48_PM.png" alt="Validation email with link" width="730" height="733" loading="lazy"></p>
</li>
<li><p>Log in with your freshly made account.</p>
</li>
<li><p>Then, click <strong>Add a New Domain.</strong></p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_12-46acfe9c-f3f4-4d3e-b8fb-97fbff643a86.10.47_PM.png" alt="Add new domain" width="730" height="598" loading="lazy"></p>
</li>
<li><p>Once created go to <strong>Installation Guide.</strong>  Copy the snippet and place it where ever you want your comments to live. In my case, I put the snippet in an area just after my <code>&lt;article&gt;</code> tag.</p>
<p> <img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_12-f78f36c5-f3f7-45ec-971d-9bf0bf7b7d1f.36.35_PM.png" alt="Code snippet" width="730" height="589" loading="lazy"></p>
</li>
<li><p>Re-compile your site and check for success!</p>
<p><img src="https://www.jaredwolff.com/how-to-setup-worry-free-blog-comments-in-less-than-20-simple-steps/images/Screen_Shot_2019-07-05_at_12-8f7ffbdc-c49f-49bc-95bb-1f53a926f361.30.27_PM.png" alt="Blog comment section with checkmarks" width="730" height="589" loading="lazy"></p>
<p>Checkmark! Finally, I recommend you try logging in with each individual OAuth configuration. That way you know it working for your website visitors. ?</p>
</li>
</ol>
<h2 id="heading-alternatives">Alternatives</h2>
<p>I spent a good chunk playing around with some of the alternatives. This is by no means a definitive guide on what will work best for your site. Here are some of the top ones as of this writing:</p>
<p><a target="_blank" href="https://utteranc.es/#configuration">https://utteranc.es/#configuration</a></p>
<p><a target="_blank" href="https://github.com/netlify/gotell">https://github.com/netlify/gotell</a></p>
<p><a target="_blank" href="https://github.com/eduardoboucas/staticman">https://github.com/eduardoboucas/staticman</a></p>
<p><a target="_blank" href="https://posativ.org/isso/">https://posativ.org/isso/</a></p>
<p><a target="_blank" href="https://www.remarkbox.com/">https://www.remarkbox.com</a></p>
<p><a target="_blank" href="https://www.vis4.net/blog/2017/10/hello-schnack/">https://www.vis4.net/blog/2017/10/hello-schnack/</a></p>
<p><a target="_blank" href="https://github.com/gka/schnack">https://github.com/gka/schnack</a></p>
<p>There's also a huge thread over at the Hugo blog which has a ton more links and resources as well:</p>
<p><a target="_blank" href="https://discourse.gohugo.io/t/alternative-to-disqus-needed-more-than-ever/5516">https://discourse.gohugo.io/t/alternative-to-disqus-needed-more-than-ever/5516</a></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Congrats! You are now hosting your own comments server! ?</p>
<p>In this article you've learned how to harness the power of Docker and a Nginx Reverse Proxy. As an added bonus, you know how to  set up OAuth credentials! That way future setup will be easy peasy.</p>
<p>By the way, this is only the tip of the iceberg. You can set up the same server for analytics, data collection and more. <a target="_blank" href="https://www.jaredwolff.com/files/host-your-comments/">All the example code including code for other applications can be found here.</a></p>
<p>Finally, if you're looking pay for Commento head to <a target="_blank" href="http://www.commento.io">www.commento.io</a> and sign up for the service. You'll be supporting awesome open source software!</p>
<p>If you have comments and questions let's hear em'. Start the conversation down below. ???</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to dance the OAuth: a step-by-step lesson ]]>
                </title>
                <description>
                    <![CDATA[ By Anabella Spinelli Most of the times I try to learn something new and put it into practice, I quickly start to feel like I’m lost in a myriad of dance moves. I’m desperately trying to find the right way to do things, while not really understanding ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-dance-the-oauth-a-step-by-step-lesson-fd2364d89742/</link>
                <guid isPermaLink="false">66c3515b465d1b2f886ba42e</guid>
                
                    <category>
                        <![CDATA[ data ]]>
                    </category>
                
                    <category>
                        <![CDATA[ oauth ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                    <category>
                        <![CDATA[ technology ]]>
                    </category>
                
                    <category>
                        <![CDATA[ user experience ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Tue, 03 Jul 2018 19:08:15 +0000</pubDate>
                <media:content url="https://cdn-media-1.freecodecamp.org/images/1*eTlZtD7s7bWMGaa0P4anvA.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Anabella Spinelli</p>
<p>Most of the times I try to learn something new and put it into practice, I quickly start to feel like I’m lost in a myriad of dance moves. I’m desperately trying to find the right way to do things, while not really understanding what’s going on or how I ended up on the wrong side of the room…</p>
<p>Just trying things out until something works.</p>
<p>Maybe it’s because of the way my learning process works, or maybe guides and tutorials are targeted at more experienced or technical people. But, after I’m done wrapping my head around the subject, I always feel like there should be an easy guide for understanding the key concepts <strong>and</strong> making it easier to apply them in a project.</p>
<p>So this time, I’ve decided to stop wishing for it and make it myself, using the last thing that I learned.</p>
<p>And that thing was OAuth 2.0.</p>
<h3 id="heading-what-is-oauth">What is OAuth?</h3>
<p>Let’s start with the basics: OAuth stands for <strong>Open Authorization</strong>. It’s a process through which an application or website can access private user data from another website.</p>
<p>This other website usually works only as a trusted <strong>identity provider</strong>. It gives the requesting app some basic information about you so that the app can create a profile. This way, you don’t have to fill in a boring sign-up form and deal with yet another password ?</p>
<p>You’ve already used this at least a gazillion times, in fact you used it every time you clicked on “Log in with Facebook / Google / GitHub / …”. Next, you were shown a consent screen that displayed which information from your (let’s say) Facebook profile you’re allowing <strong>that-hot-new-app.com</strong> to read (and sometimes, write). After that, since <strong>that-hot-new-app.com</strong> trusts the identity provided by Facebook, they can create a profile for you on their database using the data that they received.</p>
<p>The communication between <strong>that-hot-new-app.com</strong> and Facebook usually ends here. This is why your profile picture won’t change all across the Internet if you change it on Facebook. They just never go back to Facebook and ask for updated data.</p>
<h3 id="heading-when-marimba-rhythms-start-to-play">When marimba rhythms start to play…</h3>
<p>There’s another purpose for building this kind of mechanism, one with way more potential: using the identity provider as a <strong>service provider</strong> (in an ongoing manner). This means communicating with it regularly to supply enhanced features for your users.</p>
<p>A nice example of this is <a target="_blank" href="https://www.relive.cc/"><strong>Relive</strong></a>, a service that connects with different sports tracking apps to create Earth view videos of your run or ride. Every time you finish an activity, Relive prompts you offering to create a video from it. If you say yes, they’ll process it, and notify you when it’s ready for social media bragging… I mean sharing ?</p>
<p>There’s really no technical difference between these two usages. That’s why <strong>you should be cautious</strong> about where you log in with your social media or Google/Gmail account.</p>
<p>It might sound scary, but there's really nothing to fear. Just bear in mind that you’re authorizing <strong>that-hot-new-app.com</strong> to access that information about you that’s detailed in the consent screen, potentially on a recurrent basis. Be aware of the permissions you grant, and make sure you know how to disable them whenever you don’t feel trusting anymore.</p>
<p>For instance, if you are using your Google account for accessing <strong>that-hot-new-app.com</strong> but don’t want to allow that anymore, just go to your <a target="_blank" href="https://myaccount.google.com/security#connectedapps">Google account settings</a> and disable their access.</p>
<p>All the main identity providers offer control over this.</p>
<h3 id="heading-all-right-but-how-do-you-dance-the-oauth">All right, but how do you dance the OAuth?</h3>
<p>Before you land on <strong>that-hot-new-app.com</strong> and even click on “Log in with <code>YourFavoriteIdentityProvider</code><em>”</em>, someone — probably a developer — has to create an application on the provider’s site.</p>
<p>This is a way of registering <strong>that-hot-new-app.com</strong> so that, later, the provider knows who’s asking for private data.</p>
<p>In this step, the developer will set up some information about the application, like the app's name or website and — most importantly — <strong>a redirect URI</strong>. The provider (like Google or Facebook) will use this to contact the requesting app and tell them that the user said <em>yes</em> ?</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/-ZNoydoRCuDXAntqiAdKG9MVmuTjLk7qpOtW" alt="Image" width="800" height="366" loading="lazy">
<em>I promise you won't have to write it by hand, we pride ourselves on our paperlessness.</em></p>
<p>Once the app is registered, the provider will give <strong>that-hot-new-app.com</strong> a <strong>clientId</strong> and a <strong>clientSecret</strong> which will be used in the communications between them. They work sort of like a username and password for the application.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/Nc8EUZy8o8w-5QddAogfWj2SifjW9Qh5OZWh" alt="Image" width="800" height="569" loading="lazy">
<em>You'll get the clientID and clientSecret right after you click on Save application</em></p>
<p>It's very important that you keep your clientSecret in a secure location and don't share it with strangers. If someone gets access to it, they could request private user data from the provider on your behalf, and then use it for evil!</p>
<p>We don't want that.</p>
<h4 id="heading-hands-on-waists-or-shoulders">Hands on waists or shoulders</h4>
<p>Apart from setting up all those things, the developer has to find out what kind of data the provider gives access to, and how it’s segmented.</p>
<p>These “segments” are known as <strong>scopes</strong> and they define access rights, usually separated in read/write categories. So, for example, <strong>that-hot-new-app.com</strong> can request for “<strong>profile:read</strong>” and “<strong>contacts:read</strong>” scopes. This means they can read whatever the provider assigns to the “profile” and “contacts” segments. Other things won’t be accessible, for example your posts or what content you like.</p>
<p>Well, just to make things simple for now on, let’s say that <strong>that-hot-new-app.com</strong> is a website that integrates with <a target="_blank" href="https://www.typeform.com/"><strong>Typeform</strong></a>, a service for creating beautiful and smart forms and also the company I work for. You definitely want in on the hottest thing right now, and quick, so on their website you click on “Log in with Typeform” to get right into the action. What’s next?</p>
<p>Here’s a home-made, organic, and cholesterol-free diagram to use as a map for the whole thing. It may look a bit complicated but don’t worry, we’ll examine each step up next.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/EieGAKMEfd4kumthtdBGcXE7rUq4xsvo8i15" alt="Image" width="800" height="1194" loading="lazy">
<em>Colorful notes bring joy to my heart</em></p>
<h3 id="heading-authorize-the-first-step-in-the-oauth-dance">Authorize: the first step in the OAuth dance</h3>
<p>So, you take the initiative and click on “Connect with Typeform”. Here, that-hot-new-app.com (<em>THNA</em> from now on, ’cause I’m getting tired of writing dash-separated words) will send you to Typeform’s authorize endpoint (<code>/oauth/authorize</code>) and provide:</p>
<ul>
<li>their clientId (remember, that’s <strong>THNA</strong>’s username)</li>
<li>their desired scopes (or access rights)</li>
<li>and their redirect URI again (Typeform already knows it from when we set up the whole thing, but we send it again as an extra layer of security)</li>
</ul>
<p>That URL will look something like this:</p>
<pre><code>https:<span class="hljs-comment">//api.typeform.com/oauth/authorize?client_id=yourClientId&amp;scope=accounts:read+forms:read+results:read</span>
</code></pre><p>Typeform will use this information to generate a consent screen where you can review what sort of things you’re authorizing <strong>THNA</strong> to see and do.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/istIyX0juNBgdwBXW2-y-sAzMhnOgkF1sY0R" alt="Image" width="800" height="251" loading="lazy"></p>
<p>Once you have <strong>thoroughly read what you’re consenting</strong> to and happily click on “Allow”, Typeform will send you to the redirect URI with a temporary, like so:</p>
<pre><code>https:<span class="hljs-comment">//that-hot-new-app.com/auth/redirect?code=xxxXXXxxxXXXxxx</span>
</code></pre><h3 id="heading-token-it-takes-2-to-tangoauth">Token: it takes 2 to tangOAuth ?</h3>
<p>All this back and forth feels like someone’s taking you for a tango spin, right?</p>
<p>The second step of the OAuth dance is when <strong>THNA</strong> receives that code, and exchanges it for an <strong>OAuth Token</strong>.</p>
<p>So <strong>THNA</strong> takes that code and sends it back again to Typeform, along with the redirect URI (yes, again!), and the client secret (that’s the app’s password!).</p>
<p>As reward for a dance well danced, <strong>THNA</strong> will get a shiny OAuth Token ✨ which it can use to interact with Typeform on behalf of the user, that is… you!</p>
<h4 id="heading-stay-with-me-sway-with-me">Stay with me, sway with me</h4>
<p>From now on, in every request <em>THNA</em> makes to Typeform on your behalf, they’ll have to include an <strong>Authorization</strong> header with that access token. With it, Typeform (or any other provider) can identify:</p>
<ul>
<li>who’s asking for the data (in this case, <strong>THNA</strong><em>)</em></li>
<li>who’s the data about (you!)</li>
<li>and also make sure they have the correct <strong>authorization</strong> to access that data (only what you consented to).</li>
</ul>
<h3 id="heading-ready-for-the-dance-floor">Ready for the dance floor ?</h3>
<p>So now that you know all the steps and spins of the OAuth dancing technique you should be ready to create your own choreographies, I mean, integrations, and make the Internet an even greater place.</p>
<p>Drawings by yours truly, cover photo by <a target="_blank" href="https://unsplash.com/photos/I_mkJxsx8kA?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Gez Xavier Mansfield</a> on <a target="_blank" href="https://unsplash.com/?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
