<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Akash Devdhar</title>
    <description>The latest articles on DEV Community by Akash Devdhar (@neospeed83).</description>
    <link>https://dev.to/neospeed83</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4059095%2Fdb1c8fab-81c1-4bb3-a8da-da71cd7abc12.jpg</url>
      <title>DEV Community: Akash Devdhar</title>
      <link>https://dev.to/neospeed83</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/neospeed83"/>
    <language>en</language>
    <item>
      <title>Designing Multi-Tenant Identity Systems</title>
      <dc:creator>Akash Devdhar</dc:creator>
      <pubDate>Wed, 09 Sep 2026 17:52:07 +0000</pubDate>
      <link>https://dev.to/neospeed83/designing-multi-tenant-identity-systems-1702</link>
      <guid>https://dev.to/neospeed83/designing-multi-tenant-identity-systems-1702</guid>
      <description>&lt;p&gt;Most multi-tenant applications are one missing filter away from becoming somebody else's data breach. That sounds dramatic, but the architecture is often exactly this: accept a &lt;code&gt;tenant_id&lt;/code&gt; from the request, add it to a SQL query, and call the system isolated. It is not isolated. It is trusting the caller to tell you which security boundary should apply, which is basically asking the request to authorize itself.&lt;/p&gt;

&lt;p&gt;A tenant is not just a row partition. It is an identity and authorization boundary, full stop. If that boundary is not established from a verified identity and enforced at every layer that touches protected data, then the application is multi-tenant only in the product brochure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dangerous version looks reasonable
&lt;/h2&gt;

&lt;p&gt;The most common design is attractive because it is simple. A frontend knows the current tenant, so it sends &lt;code&gt;X-Tenant-ID&lt;/code&gt;. The API reads the header and uses it in every repository query. Engineers review the query and see &lt;code&gt;WHERE tenant_id = ?&lt;/code&gt;, so everybody feels safe.&lt;/p&gt;

&lt;p&gt;The issue is not the filter. The issue is who supplied its value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/invoices&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tenantId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-Tenant-ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invoices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tenantId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An authenticated user can change a header only. If the API does not independently prove that the user belongs to that tenant, the filter is doing exactly what the attacker requested. Authentication succeeded, the database query is scoped, and the data is still wrong. This is why authentication and tenant isolation cannot be separate conversations.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.akashdevdhar.com%2Fdiagrams%2Fmulti-tenant-caller-controlled.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.akashdevdhar.com%2Fdiagrams%2Fmulti-tenant-caller-controlled.png" alt="Caller-controlled tenant context creates a false security boundary" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What I am noticing is that teams are putting tenant selection inside application state, when it belongs inside verified security context. The browser can request a tenant switch, of course, but the identity layer must decide whether that switch is permitted and mint or establish the resulting context. The browser does not get to declare it as fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Derive tenant context from identity
&lt;/h2&gt;

&lt;p&gt;The safer model begins with a verified session or access token. That identity carries a subject, an audience, and either a tenant claim or enough membership information for the authorization service to resolve one. The API validates the token, confirms membership, then creates an internal request context that downstream code cannot replace with a header or request-body field.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TenantContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;subjectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildTenantContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TenantContext&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verifyAccessToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestedTenant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-tenant-id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;requestedTenant&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tenant context is required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;membership&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;memberships&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findActive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;requestedTenant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;membership&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;subject is not a member of this tenant&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;subjectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;membership&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The header still exists here, but it is a request to select from the user's verified memberships, not proof of membership by itself. That difference is the whole model actually.&lt;/p&gt;

&lt;p&gt;For an AI agent, the context needs one more piece: delegation. The agent's own identity tells you which workload is calling. It does not automatically prove which customer or human authorized this task. A useful agent token or token-exchange result should preserve both the agent identity and the delegated subject, with a narrow tenant and audience. Otherwise one agent credential can quietly become a bridge between every customer the service supports.&lt;/p&gt;

&lt;p&gt;OAuth 2.0 Token Exchange, defined in RFC 8693, is useful here because a service can exchange an upstream token for a narrower downstream token. The new token can target one resource server and one tenant context rather than forwarding a broad credential through the full service chain. OpenID Connect claims establish identity, but your authorization policy still has to decide how those claims map to tenant membership.&lt;/p&gt;

&lt;h2&gt;
  
  
  Carry the boundary through every hop
&lt;/h2&gt;

&lt;p&gt;Verifying tenant membership at the first API is necessary, but it is not enough only. Multi-tenant systems usually have queues, background jobs, internal APIs, caches, and agents calling tools. Tenant context gets lost at these boundaries because developers serialize the business payload and forget the security context that made the action valid.&lt;/p&gt;

&lt;p&gt;A background job should never wake up with only an &lt;code&gt;invoiceId&lt;/code&gt; and then search globally. It should carry the tenant identifier, initiating subject, and authorization purpose, and the worker should verify those values before acting. Internal services should receive a signed, audience-bound token rather than a forwarded browser header. Cache keys must include the tenant boundary also, otherwise perfectly authorized reads can leak through a shared cache.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.akashdevdhar.com%2Fdiagrams%2Fmulti-tenant-verified-context.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.akashdevdhar.com%2Fdiagrams%2Fmulti-tenant-verified-context.png" alt="Verified tenant context is carried and enforced through every service hop" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The important part in this design is that each service treats tenant context as security input. It is not ordinary metadata that can be dropped, overwritten, or filled with a default when missing. Missing tenant context should fail closed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enforce isolation again at the data layer
&lt;/h2&gt;

&lt;p&gt;Application checks are valuable, but relying on every engineer to remember every filter forever is a weak last line of defense. The repository or database layer should make cross-tenant access difficult by construction.&lt;/p&gt;

&lt;p&gt;PostgreSQL Row-Level Security is one option. The application sets verified tenant context on the database session, and a policy restricts rows regardless of which query the application writes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;invoices&lt;/span&gt; &lt;span class="n"&gt;ENABLE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;LEVEL&lt;/span&gt; &lt;span class="k"&gt;SECURITY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;tenant_isolation&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;invoices&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app.tenant_id'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app.tenant_id'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not magic. Connection pooling has to reset session state correctly, privileged database roles must not bypass the policy casually, and migrations need testing. But it changes the failure mode. A forgotten application filter becomes a denied query instead of a customer-data incident.&lt;/p&gt;

&lt;p&gt;Some systems choose separate schemas or separate databases for stronger isolation. That can be the correct decision for regulatory or high-value tenants, but it adds operational cost. The point is not that one storage model wins everywhere. The point is that the isolation strength should match the consequence of crossing the boundary, and it should not depend on a single controller remembering a &lt;code&gt;WHERE&lt;/code&gt; clause.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tenant membership is lifecycle data
&lt;/h2&gt;

&lt;p&gt;Identity architecture tends to focus on login, while tenant membership keeps changing after login. Users join organizations, leave them, switch roles, and sometimes belong to several tenants at once. Agents get reassigned or disabled. A token issued yesterday can carry authorization that is no longer true today.&lt;/p&gt;

&lt;p&gt;Keep access tokens short-lived. Re-evaluate membership when selecting a tenant or performing sensitive work. Use SCIM where it fits for enterprise provisioning and deprovisioning, but do not assume that provisioning alone gives you runtime authorization. SCIM can tell your system that a user was removed. Your session and token design determines how quickly that removal actually stops access.&lt;/p&gt;

&lt;p&gt;Audit records should capture at least the effective tenant, the human or workload subject, delegated identity when present, the action, and the authorization decision. Logging only &lt;code&gt;user_id&lt;/code&gt; is not enough in a system where the same subject can act inside multiple customer boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The tenant boundary must come from identity, travel with the request, and be enforced where the data lives. If any layer can guess it, default it, or accept it without verification, then that layer can cross it also.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;tenant_id&lt;/code&gt; column is useful data modeling. A verified tenant context is security architecture. Multi-tenant systems need both, and confusing one for the other is how an application ends up showing the right query in code review and the wrong customer's data in production.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Akash Devdhar is a Senior Software Engineer specializing in enterprise identity, authentication, authorization, and AI infrastructure. He writes about building secure AI systems using OAuth, OIDC, RBAC, and modern identity architectures.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>authentication</category>
      <category>authorization</category>
      <category>architecture</category>
      <category>security</category>
    </item>
    <item>
      <title>RBAC vs ABAC: Choosing the Right Authorization Model</title>
      <dc:creator>Akash Devdhar</dc:creator>
      <pubDate>Wed, 02 Sep 2026 17:56:32 +0000</pubDate>
      <link>https://dev.to/neospeed83/rbac-vs-abac-choosing-the-right-authorization-model-5h4i</link>
      <guid>https://dev.to/neospeed83/rbac-vs-abac-choosing-the-right-authorization-model-5h4i</guid>
      <description>&lt;p&gt;Every time an AI agent project hits its first "wait, this agent should not have been able to do that" moment, someone proposes the same fix: add more roles. Split "support_engineer" into "support_engineer_tier1" and "support_engineer_tier2." Add a "read_only_agent" role. This works for about two more incidents, and then you have forty roles, half of them overlapping, and nobody actually knows what any of them mean anymore. That is the point where the conversation should shift from RBAC to ABAC, and it is also the point where most teams do it wrong, because they treat ABAC as "RBAC but with more config" instead of an actually different way of thinking about the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What RBAC is actually good at, and where it stops working
&lt;/h2&gt;

&lt;p&gt;Role Based Access Control is simple, and simple is a feature, not a weakness. You define a role, "support_engineer," you attach a fixed bundle of permissions to it, "read tickets, read customer profile," and you assign the role to a subject, human or agent. This works great when permissions genuinely cluster into a small number of stable job functions. It is also exactly why it breaks for agents.&lt;/p&gt;

&lt;p&gt;An agent's required permissions are not a fixed job function, they are a function of context. The same support agent should be able to read a ticket in its own team's queue but not in a different team's queue. It should be able to act during business hours on a live incident but maybe not at 3am unattended. It should be able to act on behalf of the specific user who invoked it, not on behalf of any user who happens to share its role. RBAC has no native concept of "in its own team's queue" or "on behalf of this specific user," so people fake it with more and more roles, and the role explosion is the actual symptom, not the disease.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.akashdevdhar.com%2Fdiagrams%2Fdiagram-1-rbac-model.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.akashdevdhar.com%2Fdiagrams%2Fdiagram-1-rbac-model.png" alt="RBAC: a role is a fixed bundle of permissions" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What ABAC actually changes
&lt;/h2&gt;

&lt;p&gt;Attribute Based Access Control does not ask "what role does this subject have." It asks "given everything we know about the subject, the resource, and the environment right now, does this specific action pass the policy." Subject attributes: which team, which clearance level, who delegated to it. Resource attributes: which team owns this ticket, how sensitive is it. Environment attributes: what time is it, is this an emergency escalation. The decision is computed at request time, not looked up from a static table.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.akashdevdhar.com%2Fdiagrams%2Fdiagram-2-abac-model.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.akashdevdhar.com%2Fdiagrams%2Fdiagram-2-abac-model.png" alt="ABAC: permission is a live decision computed from attributes" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is genuinely more powerful for the "agent acting on behalf of a specific person, in a specific context" problem that RBAC struggles with. It is also more work, and this is the part people skip. ABAC without a real, maintained policy set is not more secure than RBAC, it is just a different place to hide the same mess, and honestly a worse one, because a bad ABAC policy is harder to audit at a glance than a list of forty overlapping roles.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest tradeoff
&lt;/h2&gt;

&lt;p&gt;I am not telling you ABAC is strictly better, that would be dishonest actually. RBAC is easier to reason about, easier to audit ("show me everyone with the admin role" is a one line query), and perfectly fine for systems where permissions genuinely map to stable job functions. ABAC is the right call once your access decisions depend on context that changes per request, which for agent systems is most of the time, but only if someone actually owns writing and testing the policies, the same way someone has to actually own the role definitions in an RBAC system that is not a mess. Neither model saves you from doing the thinking, they just organize where the thinking lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick code example
&lt;/h2&gt;

&lt;p&gt;RBAC, a straightforward role lookup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;ROLE_PERMISSIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support_engineer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tickets:read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_profile:read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_allowed_rbac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ROLE_PERMISSIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ABAC, a policy evaluated against subject, resource, and environment attributes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_allowed_abac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tickets:read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;same_team&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;team&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;team&lt;/span&gt;
        &lt;span class="n"&gt;within_hours&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;acting_for_owner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delegated_by&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;owner_id&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;same_team&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;within_hours&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;acting_for_owner&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the ABAC version is not longer because ABAC is inherently more complex, it is longer because it is actually encoding the real rule, "same team, and either business hours or acting for the resource's owner," instead of pretending that rule can be flattened into a role name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Standards worth reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;INCITS 359-2012&lt;/strong&gt;, the NIST RBAC model, for the formal definition of roles, permissions, and role hierarchies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NIST SP 800-162&lt;/strong&gt;, Guide to Attribute Based Access Control, for how to actually define subject, resource, and environment attributes and structure policies around them, this is the one to read before building an ABAC system, not after.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;RBAC fails for agents not because roles are a bad idea, but because agent permissions are contextual and roles are not. ABAC fixes that by making the decision live instead of a lookup, but it only works if you put in the same discipline you would have needed for RBAC anyway, just applied to policies instead of role definitions. Pick RBAC when your access model genuinely maps to stable jobs. Pick ABAC when it depends on who, what, and when, and budget the time to actually own the policies, because an unmaintained ABAC system is not a safer RBAC system, it is a harder to audit one.&lt;/p&gt;

</description>
      <category>authorization</category>
      <category>rbac</category>
      <category>abac</category>
      <category>ai</category>
    </item>
    <item>
      <title>Building Identity into AI Applications from Day One</title>
      <dc:creator>Akash Devdhar</dc:creator>
      <pubDate>Wed, 26 Aug 2026 15:56:01 +0000</pubDate>
      <link>https://dev.to/neospeed83/building-identity-into-ai-applications-from-day-one-2bne</link>
      <guid>https://dev.to/neospeed83/building-identity-into-ai-applications-from-day-one-2bne</guid>
      <description>&lt;p&gt;Almost every team I am talking to treats identity as a security review item, something you bolt on right before launch once legal and infosec start asking questions. For a normal CRUD app, you can mostly get away with that, badly, but you get away with it. For an AI application, this is backwards in a way that actually costs you real engineering time later, because agent identity is not a checklist item you add at the end, it is wiring that has to run through the whole thing from the first commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why retrofitting identity is so much more expensive here
&lt;/h2&gt;

&lt;p&gt;With a traditional app, bolting on proper authorization later mostly means adding middleware and updating some route handlers. Painful, but contained.&lt;/p&gt;

&lt;p&gt;With an AI application, by the time someone notices the identity gap, you usually have: a growing list of tools the agent can call, each one written without a scope in mind because nobody was thinking about scopes when they wrote &lt;code&gt;def send_email(to, subject, body):&lt;/code&gt;. A shared service account or a single API key wired into every integration, because that was the fastest way to get a demo working. No per-agent, per-session distinction in your logs, because logging was "add a print statement" not "log against an identity." Retrofitting at that point means touching every tool, reissuing every credential, and rebuilding your audit trail from nothing, all while the thing is already in production and agents are already calling those tools. That is a much bigger job than adding middleware, and it is also exactly the kind of migration that gets deprioritized forever because nothing is technically broken, it is just quietly unsafe.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four things worth wiring in from day one
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs7wih3j99lnkecuq3qml.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs7wih3j99lnkecuq3qml.png" alt="Four building blocks to wire in from day one" width="760" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Give every agent instance its own identity.&lt;/strong&gt; Not a shared service account "for now." Even in a prototype, issue a distinct client identity per agent or per deployment. It costs almost nothing at day one and it is the one piece that is genuinely painful to add after the fact, because everything downstream (tokens, scopes, audit logs) hangs off of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Map every tool to a scope before you write the tool, not after.&lt;/strong&gt; When you are defining &lt;code&gt;send_email&lt;/code&gt; or &lt;code&gt;run_sql_query&lt;/code&gt;, decide right then what scope should gate it. This is a five minute conversation at design time and a multi-week migration once forty tools exist without one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design token issuance and expiry into the architecture, not into a later "security hardening" sprint.&lt;/strong&gt; Short lived, scoped tokens (OAuth 2.0 client credentials or token exchange per RFC 8693) should be how the agent gets access from the start, even if the actual identity provider behind it is a stub in your prototype.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build the audit trail against identity, not against generic app logs.&lt;/strong&gt; Every tool call should be attributable to a specific agent identity and the scope it used to make that call. Bolting this on later means you have a gap in your history exactly during the period you probably most need to explain, which is early production, when things are still being figured out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens if you skip this and retrofit later
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq25wfv6947ujpmlliyia.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq25wfv6947ujpmlliyia.png" alt="The cost of retrofitting identity after the fact" width="760" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I am not saying this to be dramatic, I am saying it because I am watching it happen basically every quarter with a different team. The MVP ships fast with one shared key because that is genuinely the right tradeoff for a two week prototype. Then the prototype works, more tools get added, more agents get spun up, all against the same shared credential because changing it now would mean touching working code. Eventually someone asks "which agent did this" during an incident review and the honest answer is "we cannot tell," and now the retrofit is not a nice-to-have, it is an incident follow-up item with a deadline.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick code example
&lt;/h2&gt;

&lt;p&gt;Here is roughly what "no scope was designed in" looks like, which is the natural result of moving fast without thinking about identity first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# works fine, called by whichever agent, however often, no scope check
&lt;/span&gt;    &lt;span class="n"&gt;smtp_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is what it looks like when the scope is part of the tool's definition from the moment it is written, not added later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;required_scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;callable&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;smtp_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;TOOL_REGISTRY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;send_email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;send_email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;required_scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email:send&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;send_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TOOL_REGISTRY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;required_scope&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;PermissionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token missing scope: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;required_scope&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing complicated about the second version, that is actually the point. It is barely more code. The difference is entirely about when you decided to think about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Standards worth reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RFC 6749&lt;/strong&gt;, The OAuth 2.0 Authorization Framework, for the base client identity and scope model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RFC 8693&lt;/strong&gt;, OAuth 2.0 Token Exchange, for how a per-agent identity gets a narrower, short lived token for a specific downstream call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NIST SP 800-207&lt;/strong&gt;, Zero Trust Architecture, for the general principle that identity and verification should be part of the architecture, not a perimeter you add once and trust forever after.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;You do not need a fully built out identity provider on day one, a stub is fine for a prototype. What you actually need on day one is the shape: distinct identity per agent, a scope decided before the tool is written, tokens instead of shared secrets, and logs tied to identity instead of generic app output. Fill in the real infrastructure later if you must, but design the shape now, because the shape is the part that is expensive to change once real agents are running on top of it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Akash Devdhar is a Senior Software Engineer specializing in enterprise identity, authentication, authorization, and AI infrastructure. He writes about building secure AI systems using OAuth, OIDC, RBAC, and modern identity architectures.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>authentication</category>
      <category>authorization</category>
      <category>ai</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Authentication vs Authorization: Why Most AI Apps Get It Wrong</title>
      <dc:creator>Akash Devdhar</dc:creator>
      <pubDate>Sat, 22 Aug 2026 15:14:02 +0000</pubDate>
      <link>https://dev.to/neospeed83/authentication-vs-authorization-why-most-ai-apps-get-it-wrong-3kmg</link>
      <guid>https://dev.to/neospeed83/authentication-vs-authorization-why-most-ai-apps-get-it-wrong-3kmg</guid>
      <description>&lt;p&gt;Here is the failure mode I am seeing over and over in AI app reviews. The app checks who you are exactly once, at login, and after that it treats the agent as if it has a blank check for the rest of the session. Read a file, sure. Send an email, sure. Delete a record, why not, you logged in twenty minutes ago only. That is not an authorization model, that is a login screen pretending to be one.&lt;/p&gt;

&lt;p&gt;Authentication and authorization are not the same question asked twice, they are two entirely different questions, and most AI apps are answering the first one and quietly skipping the second, forever, for the whole session.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one question that gets asked, and the one that doesn't
&lt;/h2&gt;

&lt;p&gt;Authentication happens once, at the door: who is this. The app checks a password, a session cookie, an SSO token, whatever, and it is satisfied. Fine so far, this part most teams actually get right.&lt;/p&gt;

&lt;p&gt;Authorization is supposed to happen continuously, at every door inside the building: now that we know who this is, should this specific action, on this specific resource, right now, be allowed. This is the part that AI apps are skipping basically. Traditional web apps got away with checking authorization loosely because a human was the one clicking buttons, slowly, with natural friction built into every step. An AI agent has no such friction. It can chain fifteen tool calls in three seconds, and if your authorization model is "well, they logged in," every single one of those fifteen calls just inherited the same blank check.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually looks like when it breaks
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd94ykti4tsu9dtgrettj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd94ykti4tsu9dtgrettj.png" alt="The one-time gate model most AI apps are actually running" width="799" height="311"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Say your AI assistant is authenticated as a support engineer who can read customer tickets. Somewhere in its tool list, there is also a "run_sql_query" tool, because someone needed it for a debugging session six months ago and nobody removed it. The authentication layer sees a valid, logged-in support engineer. It has no concept of "this specific tool call, on this specific table, for this specific reason, right now." So the agent runs the query. Not because anyone decided the support engineer should have that access for that task, but because nobody built the layer that would have stopped it.&lt;/p&gt;

&lt;p&gt;This is exactly the gap that RBAC and, better, scoped OAuth tokens are supposed to close, and it is also exactly the gap that gets silently reintroduced when a team wires an agent up with one long-lived service account "to keep things simple."&lt;/p&gt;

&lt;h2&gt;
  
  
  What continuous authorization actually looks like
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsydkq5qf69z59nbmb0lo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsydkq5qf69z59nbmb0lo.png" alt="Continuous, per-action authorization for an agent" width="799" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The difference is not exotic. Every tool call the agent makes should carry a scope, and something on the other end should actually check that scope before doing anything, every single time, not just once at session start. This is what OAuth 2.0 scopes were built for, and it is what the newer Rich Authorization Requests spec (RFC 9396) extends further, letting you express authorization at the level of "read tickets in this one project" rather than a flat "read tickets, all of them, everywhere."&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick code example
&lt;/h2&gt;

&lt;p&gt;Here is roughly what the one-time-gate version looks like, which is unfortunately close to what a lot of agent frameworks default to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Checked once, at session start, then trusted for everything after
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_tool_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_authenticated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;execute_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;PermissionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not logged in&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is the per-action version, where the token's scope is actually checked against what the specific tool call is trying to do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_tool_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;access_token&lt;/span&gt;  &lt;span class="c1"&gt;# short lived, scoped, from the OAuth flow
&lt;/span&gt;    &lt;span class="n"&gt;required_scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TOOL_SCOPE_MAP&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# e.g. "tickets:read"
&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;required_scope&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;PermissionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token lacks scope: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;required_scope&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;resource_matches_scope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# e.g. token is scoped to project_id=42, but the call targets project_id=17
&lt;/span&gt;        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;PermissionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resource outside token&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s authorized scope&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_expired&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;PermissionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token expired, re-authorize&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;execute_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second version has more code, I know, but every one of those checks is answering a question the first version never even asked. Which tool, on which resource, under which still-valid grant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Standards worth reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RFC 6749&lt;/strong&gt;, The OAuth 2.0 Authorization Framework, for the base scope model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RFC 9396&lt;/strong&gt;, OAuth 2.0 Rich Authorization Requests, for expressing authorization at a finer grain than a flat scope string.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NIST SP 800-207&lt;/strong&gt;, Zero Trust Architecture, for the broader principle this post is really just a specific case of: never trust a request just because it came from an already-authenticated session, verify it again for the specific action being taken.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Logging someone in, or logging an agent in, answers exactly one question and that question is not "should this happen." If your AI app's entire authorization model is "well, the session is valid," you do not have authorization at all, you have authentication wearing a bigger hat. Check the action, not just the actor, and check it every time, not just at the door.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Akash Devdhar is a Senior Software Engineer specializing in enterprise identity, authentication, authorization, and AI infrastructure. He writes about building secure AI systems using OAuth, OIDC, RBAC, and modern identity architectures.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>authentication</category>
      <category>authorization</category>
      <category>ai</category>
      <category>oauth</category>
    </item>
    <item>
      <title>OAuth, OIDC, SAML, JWT: A Practical Guide for Modern Engineers</title>
      <dc:creator>Akash Devdhar</dc:creator>
      <pubDate>Sat, 15 Aug 2026 18:19:56 +0000</pubDate>
      <link>https://dev.to/neospeed83/oauth-oidc-saml-jwt-a-practical-guide-for-modern-engineers-4ofl</link>
      <guid>https://dev.to/neospeed83/oauth-oidc-saml-jwt-a-practical-guide-for-modern-engineers-4ofl</guid>
      <description>&lt;p&gt;I am asking engineers in interviews to explain the difference between OAuth and OIDC, and half the time I am getting the same answer basically: "they are both for login." That answer is wrong, and it is not a small wrong, it is the exact kind of wrong that is now showing up in how people are wiring up AI agents. Four acronyms, four different jobs, and treating them as one blob is where the actual security bugs come from.&lt;/p&gt;

&lt;p&gt;Let me put it plainly. SAML and OIDC answer "who are you." OAuth answers "what are you allowed to do." JWT is not a protocol at all, it is just a format, a container that any of the above can choose to put its output in. If you remember only one sentence from this post, remember that one, full stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication versus authorization, actually explained
&lt;/h2&gt;

&lt;p&gt;Every confused conversation I have had on this topic traces back to mixing up two different questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication&lt;/strong&gt; is proving identity. You log into your company's SSO portal, it checks your credentials (maybe with an MFA prompt), and it tells the application "yes, this is genuinely Priya, verified." SAML and OIDC both live here. They are competing standards for the same job, not complements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authorization&lt;/strong&gt; is a separate question that comes after: now that we know it is Priya, what is she actually permitted to do. Can she read this calendar. Can she call this API. Can her AI agent, acting for her, delete a record. OAuth 2.0 lives here, and only here. OAuth was never designed to answer "who is this," which is exactly why bolting OIDC on top of it (which is literally what OIDC is, an identity layer added on top of OAuth 2.0) was necessary in the first place.&lt;/p&gt;

&lt;p&gt;JWT (RFC 7519) sits underneath both, as a token format. An OIDC ID token can be a JWT. An OAuth access token can be a JWT, or it can be an opaque reference string, both are valid, people forget this. SAML, being older, uses XML assertions instead of JWT, which is one of the practical reasons SAML feels clunky to work with compared to the newer JSON-based standards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where each one actually shows up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SAML&lt;/strong&gt;: enterprise SSO, the classic case being logging into Salesforce or Workday through your company's identity provider. Heavy XML, browser redirect based, still everywhere in large enterprises even though nobody is excited about it anymore.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OIDC&lt;/strong&gt;: modern consumer and enterprise login, "Sign in with Google" being the most familiar example. Built on OAuth 2.0, uses JSON and JWTs, much lighter than SAML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OAuth 2.0&lt;/strong&gt;: delegated authorization, letting an app or an agent act with limited permission on your behalf, without ever handing over your password. This is the one that matters most once agents enter the picture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWT&lt;/strong&gt;: the token format that carries the actual claims (who, what scope, when it expires) in a compact, signable, verifiable way. Not a standard for login or authorization by itself, just the envelope.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this confusion gets expensive with AI agents
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbrgg7ofzqo2x05vg69jy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbrgg7ofzqo2x05vg69jy.png" alt="Four protocols, four different jobs" width="720" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is the pattern I am actually seeing go wrong. A team builds an AI agent, the agent needs to call three internal services, and someone reaches for "let's just use SAML" or worse, "let's just give it a JWT" as if JWT alone was a security model. JWT is not a security model, it is a shipping container, what matters is who signed it, what claims are inside it, and who is checking those claims before trusting the box. An unsigned or unverified JWT is basically a sticky note that says "trust me."&lt;/p&gt;

&lt;p&gt;The pattern that actually works: OIDC (or SAML, if you are stuck in an enterprise IdP that has not modernized) authenticates the human who is delegating work to the agent. OAuth 2.0, specifically client credentials or token exchange (RFC 8693) grants, authorizes the agent for a narrow, expiring scope. The resulting access token happens to be encoded as a JWT, so the resource server can verify it without a network round trip back to the issuer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdwxqn4cvcu3fqgvaxo15.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdwxqn4cvcu3fqgvaxo15.png" alt="How the four pieces chain together for an agent request" width="760" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice that JWT is not doing any of the actual security work in that second diagram. It is the format the token happens to travel in. The security work is being done by OIDC establishing who the human is, and OAuth deciding what the agent gets to do on that human's behalf, for how long.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick code example
&lt;/h2&gt;

&lt;p&gt;Decoding (not verifying, an important distinction) a JWT to see what is actually inside one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;  &lt;span class="c1"&gt;# PyJWT
&lt;/span&gt;
&lt;span class="c1"&gt;# NEVER trust a token just because you can decode it.
# This only shows you the claims, it proves nothing by itself.
&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verify_signature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# {'sub': 'agent-invoice-reader', 'scope': 'invoices:read',
#  'aud': 'https://api.internal-service.com', 'exp': 1735689600,
#  'iss': 'https://idp.example.com'}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is the part people skip, actually verifying it before trusting anything in it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PyJWKClient&lt;/span&gt;

&lt;span class="n"&gt;jwks_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PyJWKClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://idp.example.com/.well-known/jwks.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;signing_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwks_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_signing_key_from_jwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;signing_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;algorithms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RS256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;audience&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.internal-service.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;issuer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://idp.example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Now the claims are actually trustworthy, because the signature,
# audience, and issuer were all checked, not just the payload read.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second snippet is the whole point of this post basically. Decoding is free and proves nothing. Verifying against a trusted issuer's public key is the actual work, and it is where most homegrown "just use JWT" implementations quietly skip a step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Standards worth reading, not just skimming
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SAML 2.0&lt;/strong&gt; (OASIS standard) for the XML assertion based SSO model, still relevant in large enterprises.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenID Connect Core 1.0&lt;/strong&gt; for how identity got layered on top of OAuth 2.0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RFC 6749&lt;/strong&gt;, The OAuth 2.0 Authorization Framework, for the base delegation model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RFC 7519&lt;/strong&gt;, JSON Web Token (JWT), for the token format itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RFC 8693&lt;/strong&gt;, OAuth 2.0 Token Exchange, for the agent-to-agent delegation case specifically.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Stop treating these four as synonyms with different logos. SAML and OIDC prove who someone is. OAuth decides what they, or their agent, can do. JWT is just the envelope the decision travels in, not the decision itself. The moment you are clear on which of these three jobs you are actually solving for, half the "how do we secure our AI agents" confusion in your team's Slack channel basically disappears on its own.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Akash Devdhar is a Senior Software Engineer specializing in enterprise identity, authentication, authorization, and AI infrastructure. He writes about building secure AI systems using OAuth, OIDC, RBAC, and modern identity architectures.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>oauth</category>
      <category>oidc</category>
      <category>saml</category>
      <category>jwt</category>
    </item>
    <item>
      <title>Why API Keys Are Not an Identity Strategy</title>
      <dc:creator>Akash Devdhar</dc:creator>
      <pubDate>Mon, 10 Aug 2026 20:20:57 +0000</pubDate>
      <link>https://dev.to/neospeed83/why-api-keys-are-not-an-identity-strategy-4oa5</link>
      <guid>https://dev.to/neospeed83/why-api-keys-are-not-an-identity-strategy-4oa5</guid>
      <description>&lt;p&gt;Every team building AI agents right now is basically doing the same thing: they generate a long random string, put it in an environment variable, call it “authentication,” and move on. I am seeing this pattern in almost every AI infrastructure discussion I am part of, and I want to say clearly: an API key is a secret, not an identity. Treating it as the same thing is a disaster waiting to happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The confusion, explained simply
&lt;/h2&gt;

&lt;p&gt;A secret answers one question: do you know this string or not? An identity answers a completely different set of questions. Who are you, actually? What are you allowed to do? Who granted you that permission, and for how long? When an API key is your whole security model, you have answered the first question and quietly skipped the other three.&lt;/p&gt;

&lt;p&gt;This was tolerable when the caller was a predictable backend service that your own team wrote and deployed. It is not tolerable when the caller is an AI agent that is making autonomous decisions, chaining tool calls, and sometimes acting on behalf of a human user it has never directly authenticated. The agent is not “your service” anymore in the old sense. It is closer to a semi-independent actor that needs its own verifiable identity, its own scoped permissions, and its own audit trail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the API key model actually breaks
&lt;/h2&gt;

&lt;p&gt;Let me list where I am seeing the cracks, without pretending this is some neat universal law:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No delegation.&lt;/strong&gt; An API key cannot say “I am acting on behalf of user X, with X’s consent, for the next 15 minutes.” It is a flat, all-or-nothing credential.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No expiry that means anything.&lt;/strong&gt; Most API keys live forever until someone remembers to rotate them, which basically means never.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No audience or scope binding.&lt;/strong&gt; The same key that reads a customer record can usually also delete it. There is no built-in concept of “this token is only valid for this one resource server, for this one action.”&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terrible audit story.&lt;/strong&gt; When three different AI agents share the same key—and in practice, they often do—your logs cannot tell you which agent actually did what. You get a name in a log line, not a proof.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revocation is a blunt instrument.&lt;/strong&gt; You cannot revoke “this one agent’s access to this one tool” without breaking everything else that shares the key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is a new problem, actually. Enterprise software went through this exact argument fifteen years ago when everyone moved off shared static credentials toward OAuth-based delegated access. What is different now is that the caller on the other end is not a human clicking “allow” on a consent screen; it is an autonomous agent, and the whole flow needs to happen without a human sitting there approving each step.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an identity-based model looks like instead
&lt;/h2&gt;

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

&lt;p&gt;In the API key model above, there is nothing to reason about. The key either works or it does not, and once it is compromised, everything behind it is compromised also. Now compare this to what a proper identity flow looks like for an agent.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffzyqpf1bm49080nis214.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffzyqpf1bm49080nis214.png" alt="Identity based token flow for an AI agent" width="760" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The important shift here is that the agent is not holding a permanent secret that grants everything. It is holding a token, and that token is a claim about identity, scope, and time. The resource server does not trust the token because it is hard to guess; it trusts the token because it is signed by an identity provider it already trusts, and it can inspect exactly what the token is allowed to do before it does anything.&lt;/p&gt;

&lt;p&gt;This is where OAuth 2.0 client credentials grant, or better, OAuth 2.0 token exchange (RFC 8693), becomes relevant for agent-to-agent and agent-to-tool calls. Token exchange in particular is built for exactly this situation: an agent that holds one token needs to call a downstream service with a narrower, more specific token, possibly acting on behalf of the original user. That is delegation done properly, not a shared secret pretending to be delegation.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick code comparison
&lt;/h2&gt;

&lt;p&gt;Here is roughly what the API key version looks like. Simple, and also the whole problem in one snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The "identity strategy" most agents ship with today
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.internal-service.com/v1/customer-records&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-API-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk_live_9f8a7...never_rotated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# No subject, no scope, no expiry, no way to tell which agent called this.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now compare with a token acquired through client credentials and passed as a bearer token, where the token itself carries claims:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;token_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://idp.example.com/oauth/token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grant_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_credentials&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent-invoice-reader&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AGENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# short lived, rotated, scoped to this one agent
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scope&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoices:read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audience&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.internal-service.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;access_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;token_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.internal-service.com/v1/customer-records&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;access_token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# The token carries sub, scope, aud, and exp claims.
# The resource server can verify all four before doing anything.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second version has more moving parts; I am not denying that. But those moving parts are exactly what let you answer “which agent did this, was it allowed to, and for how long” without guessing. If you decode that access token (it is typically a JWT, RFC 7519), you get a &lt;code&gt;sub&lt;/code&gt; claim identifying the client, a &lt;code&gt;scope&lt;/code&gt; claim limiting what it can do, an &lt;code&gt;aud&lt;/code&gt; claim binding it to one resource server, and an &lt;code&gt;exp&lt;/code&gt; claim that makes the whole thing self-expiring. An API key gives you none of these.&lt;/p&gt;

&lt;h2&gt;
  
  
  Standards worth actually reading
&lt;/h2&gt;

&lt;p&gt;If you are designing this for real, three documents are worth your time and not just a skim:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RFC 6749&lt;/strong&gt;, The OAuth 2.0 Authorization Framework, for the base grant types including client credentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RFC 8693&lt;/strong&gt;, OAuth 2.0 Token Exchange, which is basically the spec that AI agent architectures keep re-inventing badly when they do not use it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RFC 7519&lt;/strong&gt;, JSON Web Token (JWT), for how claims-based tokens are structured and verified.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OpenID Connect is also worth knowing here, though it solves a slightly different problem: authenticating a human end user. Most agent-to-service calls are closer to pure OAuth client credentials or token exchange territory.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;An API key tells you that a caller knows a string. It does not tell you who the caller is, what it is allowed to do, or when that permission runs out. AI agents are autonomous enough now that this gap is not theoretical anymore; it is showing up as real incidents. If your agent’s entire security model is one static header value, you do not have an identity strategy—you have a shared password with extra steps.&lt;/p&gt;

&lt;p&gt;Treat your agents like the semi-independent actors they are: give them real, scoped, expiring identity, not a key copied into a config file three deployments ago.&lt;/p&gt;

</description>
      <category>authentication</category>
      <category>ai</category>
      <category>oauth</category>
      <category>security</category>
    </item>
    <item>
      <title>OAuth Was Designed for Humans. AI Agents Change the Rules.</title>
      <dc:creator>Akash Devdhar</dc:creator>
      <pubDate>Tue, 04 Aug 2026 18:18:52 +0000</pubDate>
      <link>https://dev.to/neospeed83/oauth-was-designed-for-humans-ai-agents-change-the-rules-2d7m</link>
      <guid>https://dev.to/neospeed83/oauth-was-designed-for-humans-ai-agents-change-the-rules-2d7m</guid>
      <description>&lt;p&gt;I will say it plainly. OAuth 2.0, as good as it is, was never built with the idea that the “user” on the other end could be a software agent taking fifty decisions a minute without anybody watching. We have been bending the spec to fit agents, when actually the spec was assuming a human sitting in front of a browser, clicking “Allow.” That assumption is now breaking, and most teams building AI agents have not fully understood how much.&lt;/p&gt;

&lt;p&gt;Let me explain what I mean.&lt;/p&gt;

&lt;h2&gt;
  
  
  The human assumption baked into OAuth
&lt;/h2&gt;

&lt;p&gt;Classic OAuth 2.0 authorization code flow goes something like this. A user clicks login, gets redirected to an identity provider, sees a consent screen, clicks “Allow,” and comes back with an authorization code that the app exchanges for a token. Every single step in this flow assumes a human is present to look at a screen and make a judgment call.&lt;/p&gt;

&lt;p&gt;Now put an AI agent in that loop. There is no browser session waiting on the agent’s side. There is no human eyeball reading “This app wants to access your calendar and email.” The agent is not clicking “Allow”; it is calling an API, and it needs to do this at 2 AM, unattended, sometimes chaining five or six calls to different services in a matter of seconds.&lt;/p&gt;

&lt;p&gt;The three-legged OAuth dance was built for exactly one thing: a human giving informed consent, in the moment, for a specific access grant. Agents break this in at least three ways.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it actually breaks
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. No human present for consent at request time.&lt;/strong&gt; Consent has to be captured upfront, or delegated in advance, because the agent cannot pause mid-task and wait for someone to click a button. This changes consent from an interactive, moment-of-request thing into a policy that has to be defined beforehand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Identity gets layered, not singular.&lt;/strong&gt; In classic OAuth, the token represents “this user, authorized this app, for this scope.” With agents, you actually have three identities stacked together: the human who initiated the task, the agent that is executing it, and sometimes a sub-agent that the first agent spawned to do a smaller piece of work. Standard OAuth tokens were not built to carry this chain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Session lifetime assumptions do not hold.&lt;/strong&gt; A human session might last an hour, a day at most, with natural boundaries like closing the laptop. An agent’s “session” could span a long-running workflow, spin off parallel sub-tasks, and outlive any reasonable human-session pattern, while still needing the same scoped, revocable access a human would get.&lt;/p&gt;

&lt;p&gt;Here is the classic flow first, stripped down to its four essential hops:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fswg29fzdjcxay2fx9a0n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fswg29fzdjcxay2fx9a0n.png" alt="Classic OAuth 2.0 flow: user signs in, identity provider issues a token, app holds the token, app calls the resource API" width="680" height="136"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One human decision point, right at the start, and one hop from app to resource. Simple.&lt;/p&gt;

&lt;p&gt;Now look at what an agent delegation chain actually needs. The human still decides only once, upfront, but everything downstream has to be carried and enforced through tokens instead of another click:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2x2nbkuul1gbfl4jw0pk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2x2nbkuul1gbfl4jw0pk.png" alt="Agent delegation chain: user delegates task, primary agent executes it, identity provider issues a short-lived token, a sub-agent is spawned, resource API receives the scoped token" width="680" height="530"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same one human decision at the top. But underneath it, two extra hops that a human never sees or approves in real time, and each one has to answer for itself through the token it is carrying, not through a person watching a screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually helps here
&lt;/h2&gt;

&lt;p&gt;The good news is, we are not starting from zero. RFC 8693, the OAuth 2.0 Token Exchange spec, already gives us a pattern for “on-behalf-of” tokens, where one party can exchange a token for a narrower, downstream token while preserving a chain of who is acting for whom. This maps almost perfectly onto agent delegation, if teams actually use it instead of just minting a fresh static key for every agent.&lt;/p&gt;

&lt;p&gt;A rough version of what this looks like in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Agent requests a narrower, short-lived token to call a downstream API,
# on behalf of the user who originally delegated the task.
&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;token_endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grant_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;urn:ietf:params:oauth:grant-type:token-exchange&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subject_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;agent_session_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subject_token_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;urn:ietf:params:oauth:token-type:access_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requested_token_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;urn:ietf:params:oauth:token-type:access_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scope&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calendar.read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# narrower than the agent's own scope
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audience&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calendar-service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# explicit downstream target
&lt;/span&gt;    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;downstream_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="c1"&gt;# This token should be short-lived, auditable, and traceable
# back to both the agent and the human who delegated the task.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combine this with JWT-based claims (RFC 7519) carrying an explicit “acting on behalf of” field, and you get tokens that actually answer the question an auditor will ask you six months later: which agent did this, on whose authority, and with what scope?&lt;/p&gt;

&lt;p&gt;There is also real movement happening around authentication patterns specifically for MCP servers, since MCP is quickly becoming the way agents talk to tools and data sources. Worth watching this space closely, because whatever pattern wins there will likely become the default for agent-to-tool auth industry-wide.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;OAuth is not broken; it is just incomplete for this new kind of actor. The fix is not to throw it out. It is to extend it: upfront consent instead of interactive consent, token exchange for layered identity instead of one flat token, and short-lived scoped grants instead of a static key living in an environment file for six months.&lt;/p&gt;

&lt;p&gt;If your agent architecture still treats the agent as “just another OAuth client” with a long-lived key, you are one incident away from finding out the hard way why the humans-only assumptions in OAuth mattered in the first place.&lt;/p&gt;

</description>
      <category>oauth</category>
      <category>ai</category>
      <category>security</category>
    </item>
    <item>
      <title>Authentication for AI Agents: Lessons We Can Borrow from Enterprise Identity</title>
      <dc:creator>Akash Devdhar</dc:creator>
      <pubDate>Sun, 02 Aug 2026 14:44:24 +0000</pubDate>
      <link>https://dev.to/neospeed83/authentication-for-ai-agents-lessons-we-can-borrow-from-enterprise-identity-4c2g</link>
      <guid>https://dev.to/neospeed83/authentication-for-ai-agents-lessons-we-can-borrow-from-enterprise-identity-4c2g</guid>
      <description>&lt;p&gt;Last week I was debugging an issue where our AI agent kept losing its session mid-task. And it struck me: we are basically solving the same problem enterprise IT teams solved 15 years back with SSO and IAM. The only difference is that this time the “user” logging in is not a human—it’s a piece of software making decisions on its own.&lt;/p&gt;

&lt;p&gt;Most teams building AI agents right now are treating auth as an afterthought. An API key hardcoded somewhere, one service account shared across ten agents, nobody knowing which agent did what action. I have seen this pattern more than once now, and honestly, it is a disaster waiting to happen.&lt;/p&gt;

&lt;p&gt;So let’s talk about what enterprise identity already figured out, and how we can borrow it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Every identity, human or not, needs its own credential
&lt;/h2&gt;

&lt;p&gt;In the enterprise world, we don’t let two employees share one login. The audit trail becomes useless, accountability goes out the window, basically. The same logic applies to agents. If Agent A and Agent B are sharing one API key, and something goes wrong, how will you even trace back which agent did the damage? Each agent should get its own machine identity, full stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Short-lived tokens over static keys
&lt;/h2&gt;

&lt;p&gt;This one is not new. OAuth2 and SAML taught us this ages ago. A static API key sitting in an environment file for months is a ticking bomb. Agents should be requesting short-lived tokens, refreshing them, and the moment an agent’s task is done, that token should expire. Enterprise IAM tools like Okta or Azure AD do this for humans automatically; we need the equivalent for agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Least privilege is non-negotiable
&lt;/h2&gt;

&lt;p&gt;I know it’s tempting to give your agent broad access “just in case it needs it later.” But this is exactly the mistake enterprises made in the early days, before RBAC became standard practice. An agent that only needs to read a calendar should not have write access to your entire email account. Scope it down, always.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Delegated authority needs to be explicit
&lt;/h2&gt;

&lt;p&gt;This is where it gets interesting, actually. When an agent acts “on behalf of” a user, enterprise identity already has a pattern for this. OAuth’s delegation model, or even the older Kerberos constrained delegation. The agent should carry proof of who it’s acting for, not just its own identity. Otherwise, you can’t even answer the basic question: was this action authorized by the user, or did the agent go rogue on its own?&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Continuous verification, not one-time login
&lt;/h2&gt;

&lt;p&gt;Enterprises moved from “log in once, trust forever” to Zero Trust. Verify every request, every time, based on context. Agents are even more unpredictable than humans in their behaviour, so this applies double. Don’t authenticate an agent once at session start and forget about it; keep checking.&lt;/p&gt;

&lt;p&gt;Honestly, none of this is very new thinking. The identity and access management folks have already fought this battle, made the mistakes, and written the standards for it. We are just now applying the same to a new kind of actor in the system.&lt;/p&gt;

&lt;p&gt;What I am noticing is that a lot of AI infra teams are reinventing auth from scratch, when they could simply be adapting what already exists in the IAM world, with some tweaks for the fact that agents can act faster, more autonomously, and sometimes unpredictably compared to a human clicking buttons. Worth thinking about before the AI agent version of a credential leak becomes the next big headline.&lt;/p&gt;

</description>
      <category>authentication</category>
      <category>ai</category>
      <category>security</category>
    </item>
  </channel>
</rss>
