<?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>How Enterprise SSO Actually Works</title>
      <dc:creator>Akash Devdhar</dc:creator>
      <pubDate>Wed, 30 Sep 2026 19:28:23 +0000</pubDate>
      <link>https://dev.to/neospeed83/how-enterprise-sso-actually-works-5gh3</link>
      <guid>https://dev.to/neospeed83/how-enterprise-sso-actually-works-5gh3</guid>
      <description>&lt;p&gt;Enterprise SSO is usually explained as "log in once and access every application." That describes the user experience, but it hides the architecture so completely that teams start believing every application is sharing one giant session. They are not. Each application has its own session. What is shared is trust in the same identity provider, full stop.&lt;/p&gt;

&lt;p&gt;This difference matters because SSO failures are rarely about the login screen. They happen when an application trusts the wrong issuer, accepts an assertion intended for somebody else, maps groups directly into permanent privilege, or assumes that signing out from one place has magically ended every session everywhere.&lt;/p&gt;

&lt;p&gt;SSO is federation. One system proves that authentication happened, another system decides whether to accept that proof, and then the application creates local state of its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The identity provider does not log you into the application
&lt;/h2&gt;

&lt;p&gt;Take a normal employee opening a software-as-a-service application. The application does not have the employee's password, and it should not ask for it. Instead, it redirects the browser to an identity provider, usually called the IdP. The IdP authenticates the user using whatever controls the enterprise requires, perhaps password, phishing-resistant MFA, device posture, or an existing IdP session.&lt;/p&gt;

&lt;p&gt;After authentication, the IdP sends a signed response back through the browser. With SAML, that response contains a SAML assertion. With OpenID Connect, the application receives an authorization response, exchanges the authorization code at the token endpoint, and obtains an ID token.&lt;/p&gt;

&lt;p&gt;The browser is carrying the response, but the browser is not what makes it trustworthy. The signature, issuer, audience, time bounds, request correlation, and protocol validation make it trustworthy.&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%2Fsso-federated-login.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%2Fsso-federated-login.png" alt="Enterprise SSO is a trust exchange followed by a local application session" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the application validates the response, it maps the external identity to a local account and creates its own session cookie. On the next request, the application normally reads that local cookie. It does not redirect to the IdP every time only. This is why the application can remain signed in even when the IdP page is not open, and why logout becomes more complicated than login.&lt;/p&gt;

&lt;h2&gt;
  
  
  SAML and OIDC carry similar trust in different shapes
&lt;/h2&gt;

&lt;p&gt;SAML 2.0 is still common in enterprise software. It uses signed XML assertions and browser POST or redirect bindings. The application is the Service Provider, or SP. The IdP sends an assertion containing a subject, an audience restriction, validity times, and attributes.&lt;/p&gt;

&lt;p&gt;OpenID Connect is an identity layer built on OAuth 2.0. The application is an OIDC client, and the IdP is an OpenID Provider. Modern applications normally use the authorization code flow with PKCE. The ID token is usually a JWT, and the client can use the access token for a separate API when the design requires it.&lt;/p&gt;

&lt;p&gt;They solve a similar SSO problem, but they are not interchangeable payload formats. SAML has assertion consumer service URLs, entity IDs, XML signatures, and metadata. OIDC has redirect URIs, client IDs, discovery metadata, JSON Web Keys, authorization codes, and ID-token validation. Wrapping both behind one "SSO callback" abstraction can be useful, but the validation rules must stay protocol specific.&lt;/p&gt;

&lt;p&gt;The application should load trusted configuration from an administrative onboarding flow. It should not accept an arbitrary issuer or metadata URL from a login request. Otherwise the caller can choose which identity provider the application trusts, which is basically allowing the user to bring their own authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validation is the real SSO implementation
&lt;/h2&gt;

&lt;p&gt;Redirecting a browser is easy. Correctly deciding whether the returned identity proof belongs to this request and this application is the security work.&lt;/p&gt;

&lt;p&gt;For OIDC, validate at least the issuer, audience, signature algorithm, expiry, and &lt;code&gt;nonce&lt;/code&gt;. Correlate the &lt;code&gt;state&lt;/code&gt; value with the browser session to defend the authorization response flow. Use an exact registered redirect URI. If the token contains multiple audiences, apply the protocol's authorized-party rules rather than picking the first value that looks familiar.&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;const&lt;/span&gt; &lt;span class="nx"&gt;tokenSet&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;oidcClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://app.example.com/auth/callback&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;callbackParams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;loginAttempt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;loginAttempt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;code_verifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;loginAttempt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;codeVerifier&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tokenSet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;claims&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="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;iss&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;configuredIssuer&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;unexpected identity provider&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;account&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;accounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolveFederatedIdentity&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;iss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&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 stable external identifier is the pair of issuer and subject, not email address by itself. Email can change, be reused, or collide across identity providers. Linking accounts by an unverified email is convenient right until one tenant's identity becomes another tenant's local account.&lt;/p&gt;

&lt;p&gt;For SAML, validate the XML signature using the configured IdP certificate, the response destination, audience restriction, assertion validity, and request correlation such as &lt;code&gt;InResponseTo&lt;/code&gt; when the flow expects it. Protect against XML signature wrapping by using a maintained SAML library and consuming only the element that the library verified. Searching the XML tree manually for a convenient &lt;code&gt;NameID&lt;/code&gt; defeats the point of the signature.&lt;/p&gt;

&lt;p&gt;The OIDC Core specification and SAML 2.0 Profiles define these checks because each one closes a different substitution or replay path. They are not optional decoration around the signature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Account mapping is where tenants get crossed
&lt;/h2&gt;

&lt;p&gt;After validation, the application has an external identity. It still has to answer a local question: which account and tenant does this identity belong to.&lt;/p&gt;

&lt;p&gt;Enterprise applications often discover the tenant from the verified issuer or an explicit SSO connection selected before redirect. That connection maps one enterprise IdP to one application tenant. The returned email or domain may help onboarding, but it should not silently override the configured tenant boundary.&lt;/p&gt;

&lt;p&gt;Just-in-time provisioning can create a local user after the first successful SSO response. SCIM can provision users and groups before login. These are lifecycle choices, not authentication shortcuts. Whether the account was created just in time or through SCIM, the application still needs a durable mapping and a clear disabled state.&lt;/p&gt;

&lt;p&gt;Group and role claims need careful treatment also. An IdP group proves that the IdP placed the user in that group. It does not automatically mean the application should grant administrator access. Map external groups through tenant-owned policy, keep defaults narrow, and decide what happens when a claim disappears. Authorization remains an application responsibility.&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%2Fsso-identity-to-authorization.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%2Fsso-identity-to-authorization.png" alt="SSO establishes identity, then the application applies tenant and authorization policy" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This separation is useful during incidents. You can see whether the IdP authenticated the wrong person, the federation mapping selected the wrong local account, or the application granted the wrong permission. If everything is called "SSO," those three failures become one vague ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  SSO login is easier than SSO logout
&lt;/h2&gt;

&lt;p&gt;Login works well because the browser follows a chain and every participant can create new state. Logout asks multiple independent systems to destroy state they own, sometimes when one of them is unavailable.&lt;/p&gt;

&lt;p&gt;Signing out of the application should always end the application's local session. Redirecting to an IdP logout endpoint may end the IdP session, but it does not guarantee every other application session is gone. SAML Single Logout and OIDC logout specifications provide coordination mechanisms, but deployments vary and front-channel browser flows can fail.&lt;/p&gt;

&lt;p&gt;For high-risk environments, do not rely on coordinated logout as the only revocation strategy. Keep local session lifetimes appropriate, consume deprovisioning events, and re-check sensitive authorization. If an employee is disabled at the IdP, an application session with a month-long lifetime should not remain trusted simply because no logout message arrived.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI agents should not borrow the SSO session
&lt;/h2&gt;

&lt;p&gt;An AI agent acting for an employee needs the human identity context, but the agent is not the human. Passing the browser session cookie into an agent process collapses two actors into one credential and makes the audit trail useless basically.&lt;/p&gt;

&lt;p&gt;Use SSO to authenticate the human and establish the application's local session. When the user delegates a task, issue or exchange a separate short-lived credential for the agent. That credential should identify the workload, preserve the delegated subject, target one audience, and carry narrower scope. RFC 8693 token exchange is one useful pattern for this on-behalf-of relationship.&lt;/p&gt;

&lt;p&gt;The distinction becomes important when the user logs out, the task is cancelled, or the agent delegates to a tool. The application can revoke the agent task without pretending the browser and the agent are one session. The resource API can log both who authorized the work and which workload actually performed it.&lt;/p&gt;

&lt;p&gt;SSO answers how the human entered the trust system. It does not give every autonomous process permission to impersonate that human afterward.&lt;/p&gt;

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

&lt;p&gt;Enterprise SSO is a chain of explicit trust decisions. The IdP authenticates the user and signs a protocol response. The application validates that response, maps the external identity into the correct tenant, and creates a local session. Then the application applies its own authorization policy.&lt;/p&gt;

&lt;p&gt;SAML and OIDC make the exchange interoperable. They do not remove the need for strict validation, lifecycle management, or local access control. Once you see SSO as federation instead of a shared login, the architecture becomes much easier to reason about, and much harder to accidentally trust too broadly.&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>sso</category>
      <category>oidc</category>
      <category>saml</category>
    </item>
    <item>
      <title>Designing Secure Authentication APIs</title>
      <dc:creator>Akash Devdhar</dc:creator>
      <pubDate>Wed, 23 Sep 2026 18:26:28 +0000</pubDate>
      <link>https://dev.to/neospeed83/designing-secure-authentication-apis-2c4l</link>
      <guid>https://dev.to/neospeed83/designing-secure-authentication-apis-2c4l</guid>
      <description>&lt;p&gt;Many authentication APIs are basically one controller with very high confidence. Accept email and password, compare a hash, sign a JWT, return 200. Add HTTPS and rate limiting, then call it production ready. The endpoint may be only fifty lines, but those fifty lines are now issuing proof that every other service will trust. That is not a normal controller. It is a security protocol boundary, full stop.&lt;/p&gt;

&lt;p&gt;What I am seeing is that teams are reviewing authentication APIs like business APIs. Does the input validate. Does the database query work. Does the happy path return the right JSON. Those questions matter, but the dangerous behavior is usually between requests: what an attacker can learn from failures, whether an old refresh token still works, which audience accepts the token, and what happens after the account is disabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  The endpoint is smaller than the protocol
&lt;/h2&gt;

&lt;p&gt;Consider the naive shape:&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;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/login&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;user&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;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByEmail&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;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&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;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&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="na"&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;user not found&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="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verifyPassword&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;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;passwordHash&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&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="na"&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;wrong password&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;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signJwt&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&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;token&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;It works. It also reveals which email addresses exist, gives every resource server the same vaguely defined token, copies authorization into a credential with no freshness model, and has no visible session or refresh-token lifecycle. The bug is not one missing &lt;code&gt;if&lt;/code&gt; statement. The API has no protocol around it.&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%2Fauth-api-credential-factory.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%2Fauth-api-credential-factory.png" alt="A naive login endpoint becomes an uncontrolled credential factory" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Authentication design starts before password verification and continues after token issuance. Treat enrollment, recovery, MFA, login, refresh, logout, and revocation as one connected system. An account-recovery flow that bypasses MFA can defeat a perfectly implemented login endpoint. A refresh endpoint that accepts replay forever can undo a short access-token lifetime also.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure responses are part of the attack surface
&lt;/h2&gt;

&lt;p&gt;Different errors make debugging pleasant and account enumeration easy. "User not found" and "incorrect password" tell an attacker whether the first guess was correct. Different response times can reveal the same thing even when the JSON is identical.&lt;/p&gt;

&lt;p&gt;For public login and recovery endpoints, return a uniform external response for invalid credentials or unknown accounts. Internally, log the actual reason with appropriate access controls. When the account does not exist, perform a bounded password-hash operation using a fixed dummy hash so the timing shape stays reasonably close.&lt;/p&gt;

&lt;p&gt;Rate limiting needs more than an IP address. Attackers distribute traffic, and shared corporate networks put many legitimate users behind one IP. Combine controls by source, account identifier, device or client signal, and overall risk. Apply progressive delay or step-up checks where appropriate. Do not turn rate limiting into a denial-of-service button where anyone can lock a victim's account by sending bad passwords.&lt;/p&gt;

&lt;p&gt;The response should not explain your internal identity state. The audit trail should.&lt;/p&gt;

&lt;h2&gt;
  
  
  A token needs one meaning
&lt;/h2&gt;

&lt;p&gt;"It is a valid JWT" is not a security decision. A JWT is a container. The resource server must know who issued it, which audience it targets, which algorithms are allowed, what token type it represents, and whether its time bounds are acceptable.&lt;/p&gt;

&lt;p&gt;RFC 8725, the JSON Web Token Best Current Practices document, is clear about algorithm verification and cross-JWT confusion. An ID token should not accidentally work as an API access token. A token minted for the billing API should not work at the administration API just because both services know the same signing key.&lt;/p&gt;

&lt;p&gt;Validation should be explicit:&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;const&lt;/span&gt; &lt;span class="nx"&gt;claims&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;jwtVerify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signingKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://identity.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;audience&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.example.com/invoices&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;algorithms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ES256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;typ&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;at+jwt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;clockTolerance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&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="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token_use&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;access&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;unexpected token type&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;hasRequiredScope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scope&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:read&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;insufficient scope&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not let the token choose its own verification algorithm. Do not accept every audience your company owns. Do not make a resource server understand login-session cookies, ID tokens, access tokens, and internal agent tokens through one permissive parser. Separate token profiles make rejection easier to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Short access tokens need a real refresh design
&lt;/h2&gt;

&lt;p&gt;Short-lived access tokens reduce the useful window after theft, but only if the refresh token is handled as the more powerful credential it is. A refresh token that lives for months, can be replayed repeatedly, and is stored beside the access token has not reduced much risk actually.&lt;/p&gt;

&lt;p&gt;Rotate refresh tokens on every successful use. Store a hash of the current token, link tokens into a family, and treat reuse of an already-consumed token as a replay signal. Revoke the active family and require reauthentication rather than issuing one more access token to whichever party arrived second.&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;exchangeRefreshToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;presented&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hashToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;presented&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;tokenStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&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;tx&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;record&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;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findForUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;digest&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;record&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revokedAt&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expiresAt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;invalid refresh token&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;consumedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revokeFamily&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;familyId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;refresh token replay detected&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;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markConsumed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;issueSuccessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;familyId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subjectId&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 transaction is important. Two refresh requests racing each other should not both receive valid successors. One succeeds. The other becomes evidence that the credential may have been copied.&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%2Fauth-api-controlled-issuance.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%2Fauth-api-controlled-issuance.png" alt="A secure authentication API keeps token issuance inside explicit controls" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Agents are not unusual users
&lt;/h2&gt;

&lt;p&gt;When an AI agent needs an API, teams often send it through the human login endpoint using a service account. That creates a credential which looks like a person, has no delegation chain, and usually carries broad permissions because nobody can predict every task the agent may attempt.&lt;/p&gt;

&lt;p&gt;Give workloads their own client identity. Use an OAuth grant appropriate for the actor, such as client credentials for the agent's own authority or token exchange from RFC 8693 when it acts on behalf of a user. Bind the resulting access token to one audience and narrow scopes. Keep the human subject and agent identity distinguishable in logs.&lt;/p&gt;

&lt;p&gt;An agent session also needs a different lifetime. A browser may remain signed in for convenience, while an agent task should receive a credential bounded to the task and expire when the work should stop. Reusing the browser token is easy, but it makes logout, audit, and least privilege much harder only.&lt;/p&gt;

&lt;p&gt;For high-risk machine-to-machine calls, sender-constrained tokens can reduce bearer-token theft. Mutual TLS and proof-of-possession approaches add operational work, so use them where the threat justifies it, but understand the tradeoff. A bearer token belongs to whoever holds it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recovery and revocation are authentication APIs too
&lt;/h2&gt;

&lt;p&gt;Password reset, account recovery, device removal, and session revocation are sometimes built later by a different team. Attackers do not respect those ownership boundaries. They use whichever path produces trust most cheaply.&lt;/p&gt;

&lt;p&gt;Recovery tokens should be single use, short lived, stored hashed, and scoped to the exact recovery action. Completing recovery should invalidate relevant sessions and refresh-token families. Changing a password without touching already issued credentials leaves the attacker signed in.&lt;/p&gt;

&lt;p&gt;Revocation behavior needs a measurable target. If disabling an account must stop sensitive access within five minutes, access-token lifetimes, caches, long-lived connections, and background jobs must honor that window. Writing a disabled flag into the user table is not revocation until the services check it.&lt;/p&gt;

&lt;p&gt;Audit every credential transition: issuance, failed authentication, MFA challenge, refresh rotation, replay detection, recovery, and revocation. Avoid logging raw passwords, session IDs, access tokens, or recovery secrets. An audit system that captures the credential itself becomes another credential store.&lt;/p&gt;

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

&lt;p&gt;A secure authentication API does more than verify a password and sign a token. It controls what failures reveal, gives each token one precise meaning, detects refresh replay, and carries revocation through the rest of the architecture.&lt;/p&gt;

&lt;p&gt;Treat human sessions, service identities, and delegated agent access as different credential flows. They can share identity infrastructure, but they should not share one ambiguous token. The API is issuing trust that other systems will act on, and that trust needs a protocol around it, not just a controller.&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>oauth</category>
      <category>security</category>
      <category>api</category>
    </item>
    <item>
      <title>Session Management at Scale</title>
      <dc:creator>Akash Devdhar</dc:creator>
      <pubDate>Wed, 16 Sep 2026 18:14:18 +0000</pubDate>
      <link>https://dev.to/neospeed83/session-management-at-scale-3hl0</link>
      <guid>https://dev.to/neospeed83/session-management-at-scale-3hl0</guid>
      <description>&lt;p&gt;Teams usually discover they have a session-management problem when Redis gets slow. They add replicas, tune TTLs, maybe shard by user ID, and declare the system scaled. The cache is faster, yes. The security model is still the same fragile one only.&lt;/p&gt;

&lt;p&gt;A session is not just a cookie pointing at some server state. It is a live trust decision that has escaped the login service and started moving through your architecture. It reaches APIs, background workers, WebSocket connections, mobile devices, and now AI agents that can keep working after the user closes the browser. Scaling the lookup is the easy part. Scaling the meaning of that trust is where systems break.&lt;/p&gt;

&lt;h2&gt;
  
  
  Login creates a session, but the session keeps changing
&lt;/h2&gt;

&lt;p&gt;At login time the picture looks clean. A user proves identity, perhaps completes MFA, and receives a session identifier. The server stores a record with the user, expiry, and maybe a few claims. Every request presents the cookie and the application loads the record.&lt;/p&gt;

&lt;p&gt;Then reality arrives. The user changes organizations. An administrator removes a role. Risk signals require another MFA check. The user signs out one device but not another. A support team needs to revoke every active session after an account recovery. The original login was valid, but the authorization context around it is changing basically all the time.&lt;/p&gt;

&lt;p&gt;If the session is a signed blob with a 30-day expiry and no server-side control, those changes wait 30 days also. Cryptographic validity is not the same as current authorization.&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%2Fsession-stale-trust.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%2Fsession-stale-trust.png" alt="A long-lived session spreads stale trust across the system" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the first design decision that matters: what must be checked on every request, and what can safely remain fixed until expiry. User identity might be stable for the session. Tenant membership, account status, risk level, and sensitive permissions often are not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the browser credential small and meaningless
&lt;/h2&gt;

&lt;p&gt;The browser should hold an opaque, high-entropy session identifier in a cookie with &lt;code&gt;Secure&lt;/code&gt;, &lt;code&gt;HttpOnly&lt;/code&gt;, and an appropriate &lt;code&gt;SameSite&lt;/code&gt; policy. It should not hold the full authorization model in a value that every service learns to trust independently.&lt;/p&gt;

&lt;p&gt;RFC 6265 defines cookie behavior, but cookie flags are only transport controls. They help protect the identifier. They do not solve rotation, revocation, or authorization freshness.&lt;/p&gt;

&lt;p&gt;A server-side session record can stay compact:&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;SessionRecord&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&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;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;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;lastSeenAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;idleExpiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;absoluteExpiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;authLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;password&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mfa&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;revokedAt&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&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;Notice what is not there: a copied list of every permission the user had at login. Keep stable session facts in the record, then resolve authorization that can change from the current policy or membership source. Otherwise the session store becomes a warehouse of stale entitlements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rotation closes a window that expiry leaves open
&lt;/h2&gt;

&lt;p&gt;Session expiry answers how long a credential may live. Rotation answers whether the same credential should survive a security boundary such as login, MFA completion, password reset, tenant switch, or privilege elevation.&lt;/p&gt;

&lt;p&gt;Without rotation, an identifier captured before login can become an authenticated session after login. That is session fixation. Without rotation after privilege elevation, the same long-lived identifier moves from ordinary access to sensitive access with no new boundary around it.&lt;/p&gt;

&lt;p&gt;The safe pattern is to create a new identifier and invalidate the old one atomically:&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rotateSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oldId&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="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SessionRecord&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;return&lt;/span&gt; &lt;span class="nx"&gt;sessionStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&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;tx&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;current&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;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getForUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oldId&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;current&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revokedAt&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;session is no longer active&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;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;lastSeenAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oldId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;next&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;Atomicity matters. If both identifiers remain valid during a race, rotation has created a second session rather than replacing the first one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Revocation must reach every service
&lt;/h2&gt;

&lt;p&gt;Central session storage gives you a revocation point, but only if every path actually checks it. A normal HTTP request may load the session on every call while a WebSocket authenticates once and remains connected for hours. A queued job may have copied the subject and tenant into its payload. An internal service may cache a successful session check for longer than the revocation target.&lt;/p&gt;

&lt;p&gt;This is why a useful session design begins with a revocation objective. If disabling an account must stop sensitive actions within five minutes, every cache, connection, and worker must either revalidate inside five minutes or receive a reliable revocation event. Writing &lt;code&gt;revokedAt&lt;/code&gt; into Redis does nothing for a process that never looks again.&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%2Fsession-revocation-control.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%2Fsession-revocation-control.png" alt="Central session control keeps distributed work inside the revocation window" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For high-risk actions, check current session state immediately and require step-up authentication when the authentication level is not sufficient. For lower-risk reads, a short cache may be reasonable. The policy should be explicit. Accidental cache duration is not a security policy.&lt;/p&gt;

&lt;p&gt;NIST SP 800-63B is useful here because it separates overall session lifetime from inactivity timeout and reauthentication requirements. Those controls should not collapse into one TTL field. An active session can still hit its absolute maximum, and a session inside its maximum can still require fresh authentication before a sensitive action.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI agents create sessions inside sessions
&lt;/h2&gt;

&lt;p&gt;An agent task is often treated as a background continuation of the user's browser session. The user starts a task, the agent receives some credentials, and it keeps running. But the browser session may end while the task is still making decisions. If the agent simply copied the original bearer token, logout becomes mostly cosmetic.&lt;/p&gt;

&lt;p&gt;The agent needs its own bounded execution session. It should identify the workload, preserve who delegated the task, target one tenant, and carry only the scopes needed for that task. Its lifetime should reflect the work, not the user's browser-cookie lifetime. If it delegates to another agent or tool, the downstream credential should become narrower, not inherit everything.&lt;/p&gt;

&lt;p&gt;OAuth 2.0 Token Exchange from RFC 8693 gives a useful pattern for creating that downstream credential. The browser session establishes the human context, but the agent receives a separate short-lived token with its own audience and traceable delegation. Revoking the task can then stop the agent without destroying every user session, and revoking the user can still cascade to active tasks when policy requires it.&lt;/p&gt;

&lt;p&gt;This distinction also improves audit logs. "User 123 made API call" is incomplete when an autonomous workload performed the call twenty minutes after the user clicked a button. Record the session, agent identity, delegated subject, tenant, and task identifier. Otherwise all autonomous work becomes indistinguishable from direct human action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Availability cannot quietly disable security
&lt;/h2&gt;

&lt;p&gt;A centralized session store becomes critical infrastructure, so teams are tempted to fail open when it is unavailable. That turns an operational incident into an authorization bypass. If a service cannot determine whether a sensitive session is active, it should not assume active.&lt;/p&gt;

&lt;p&gt;You can reduce the availability risk with replicas, regional stores, carefully bounded local caches, and signed short-lived proofs. But every optimization needs a maximum stale-trust window. A five-minute cache is a decision that revoked access may continue for five minutes. Sometimes that is acceptable. Pretending it is only a performance setting is not.&lt;/p&gt;

&lt;p&gt;Monitor identity outcomes, not just cache latency. Track rejected expired sessions, revoked-session reuse, rotation failures, impossible tenant switches, and how long revocation takes to reach long-lived connections. A session system can have perfect uptime while authorization freshness is completely broken.&lt;/p&gt;

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

&lt;p&gt;Session management at scale is the work of keeping trust current while it spreads. Store as little as possible in the browser, rotate identifiers when trust changes, separate idle and absolute lifetime, and make revocation reach every execution path inside a known window.&lt;/p&gt;

&lt;p&gt;For AI agents, do not stretch a human browser session until it covers autonomous work. Create a smaller, traceable execution session with its own identity and expiry. The moment a session crosses a service boundary, it becomes identity architecture, full stop.&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>security</category>
      <category>architecture</category>
      <category>ai</category>
    </item>
    <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>
