<?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: Auth0</title>
    <description>The latest articles on DEV Community by Auth0 (@auth0).</description>
    <link>https://dev.to/auth0</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.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F634%2Fc6bfc78f-136d-456b-96dc-bcc4be1c88f9.jpg</url>
      <title>DEV Community: Auth0</title>
      <link>https://dev.to/auth0</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/auth0"/>
    <language>en</language>
    <item>
      <title>Implementing the Device Authorization Flow with Auth0 in a C# Console App</title>
      <dc:creator>Andrea Chiarelli</dc:creator>
      <pubDate>Mon, 15 Jun 2026 08:01:01 +0000</pubDate>
      <link>https://dev.to/auth0/implementing-the-device-authorization-flow-with-auth0-in-a-c-console-app-1npa</link>
      <guid>https://dev.to/auth0/implementing-the-device-authorization-flow-with-auth0-in-a-c-console-app-1npa</guid>
      <description>&lt;p&gt;The redirect-based OAuth 2.0 flows assume one thing: the user has a browser on the same device where your app runs. Smart TVs, IoT sensors, and CLI tools break that assumption. If your application runs somewhere a browser isn't practical or doesn't exist, you need a different mechanism.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://auth0.com/docs/get-started/authentication-and-authorization-flow/device-authorization-flow" rel="noopener noreferrer"&gt;Device Authorization Flow&lt;/a&gt; (standardized in &lt;a href="https://datatracker.ietf.org/doc/html/rfc8628" rel="noopener noreferrer"&gt;RFC 8628&lt;/a&gt;) was designed for exactly this scenario. It lets a constrained device initiate authentication and then wait while the user completes it on a separate device with a browser.&lt;/p&gt;

&lt;p&gt;By the end of this tutorial, you'll have a working .NET 10 console application that authenticates users through Auth0 without requiring a browser on the device itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Device Authorization Flow Works
&lt;/h2&gt;

&lt;p&gt;The flow introduces two parallel channels. The device handles polling; the user handles authorization in a browser. These two channels synchronize through a short-lived code.&lt;/p&gt;

&lt;p&gt;Here's how those two channels interact:&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.amazonaws.com%2Fuploads%2Farticles%2F9sa0o60407vknfufr848.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.amazonaws.com%2Fuploads%2Farticles%2F9sa0o60407vknfufr848.png" alt="Diagram of the OAuth 2.0 Device Authorization Flow" width="800" height="543"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's walk through what each step does:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user launches your app.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Device code request.&lt;/strong&gt; Your app sends its &lt;code&gt;client_id&lt;/code&gt; to Auth0's &lt;code&gt;/oauth/device/code&lt;/code&gt; endpoint.
&lt;/li&gt;
&lt;li&gt;Auth0 responds with a &lt;code&gt;device_code&lt;/code&gt; (used internally for polling), a short &lt;code&gt;user_code&lt;/code&gt; (shown to the user), a &lt;code&gt;verification_uri&lt;/code&gt; (where the user will navigate to authorize the device), and a polling &lt;code&gt;interval&lt;/code&gt; in seconds.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User activation.&lt;/strong&gt; Your app displays the &lt;code&gt;verification_uri&lt;/code&gt; and &lt;code&gt;user_code&lt;/code&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Polling.&lt;/strong&gt; While waiting for user authentication, your app polls &lt;code&gt;/oauth/token&lt;/code&gt; with the &lt;code&gt;device_code&lt;/code&gt;.
&lt;/li&gt;
&lt;li&gt;Until the user completes authorization, Auth0 returns &lt;code&gt;{"error": "authorization_pending"}&lt;/code&gt;. The &lt;code&gt;interval&lt;/code&gt; field in the initial response specifies the minimum seconds between polls.
&lt;/li&gt;
&lt;li&gt;The user opens a browser on any convenient device (a phone, laptop, anything with a browser) navigates to that URL.
&lt;/li&gt;
&lt;li&gt;The user enters the code.
&lt;/li&gt;
&lt;li&gt;The user is confirmed that the device has been authorized.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token delivery.&lt;/strong&gt; Once the user authenticates and approves the device, the next successful poll returns an access token, an ID token, and optionally a refresh token.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Polling faster than the specified interval triggers a &lt;code&gt;slow_down&lt;/code&gt; response. When that happens, increase your interval.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A key point to understand: Auth0 never issues tokens to the device until a real user explicitly approves the request in a browser. The user_code is the artifact that ties the device's session to the browser's session. If there is no code exchange, there’s no token.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Sample Project
&lt;/h2&gt;

&lt;p&gt;Now that you have a good understanding of the Device Authorization flow, let’s build a .NET 10 console application that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requests device authorization from Auth0
&lt;/li&gt;
&lt;li&gt;Displays the activation URL and user code to the operator
&lt;/li&gt;
&lt;li&gt;Polls Auth0 until the user completes authorization
&lt;/li&gt;
&lt;li&gt;Retrieves and displays the authenticated user's profile using the resulting access token&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The complete sample uses the &lt;a href="https://www.nuget.org/packages/Auth0.AuthenticationApi" rel="noopener noreferrer"&gt;Auth0 .NET Authentication API SDK&lt;/a&gt; (&lt;code&gt;Auth0.AuthenticationApi&lt;/code&gt;), which wraps the device authorization endpoints directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Register with Auth0
&lt;/h2&gt;

&lt;p&gt;Before writing any code, you need an Auth0 application configured for the Device Authorization flow.&lt;/p&gt;

&lt;p&gt;If you don’t have an Auth0 account, you can &lt;a href="https://a0.to/blog_signup" rel="noopener noreferrer"&gt;sign up for free&lt;/a&gt;! Then, follow these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log in to your &lt;a href="https://manage.auth0.com/" rel="noopener noreferrer"&gt;Auth0 Dashboard&lt;/a&gt; and go to &lt;strong&gt;Applications &amp;gt; Applications&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create Application&lt;/strong&gt;, name it (e.g., "Device Flow Demo"), and select &lt;strong&gt;Native&lt;/strong&gt; as the application type. Click &lt;strong&gt;Create&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;On the &lt;strong&gt;Settings&lt;/strong&gt; tab, copy your &lt;strong&gt;Domain&lt;/strong&gt; and &lt;strong&gt;Client ID&lt;/strong&gt;: you'll need both shortly.
&lt;/li&gt;
&lt;li&gt;Scroll down to &lt;strong&gt;Advanced Settings &amp;gt; Grant Types&lt;/strong&gt;. Make sure &lt;strong&gt;Device Code&lt;/strong&gt; is enabled (see the picture below). Save any changes.&lt;/li&gt;
&lt;/ol&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.amazonaws.com%2Fuploads%2Farticles%2Fe0o31jemiavlplkcom9k.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.amazonaws.com%2Fuploads%2Farticles%2Fe0o31jemiavlplkcom9k.png" alt="Auth0 Dashboard — Grant Types with Device Code enabled." width="799" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's all the configuration the Device Authorization flow requires. Unlike redirect-based flows, this one doesn't need callback URLs or allowed origins, because no browser redirect happens on the device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add the Device Authorization Flow to Your App
&lt;/h2&gt;

&lt;p&gt;Now you are ready to implement the Device Authorization flow in your console application. Create a new console project and add the &lt;a href="https://github.com/auth0/auth0.net#authentication-api" rel="noopener noreferrer"&gt;Auth0 .NET Authentication SDK&lt;/a&gt; by running the following commands in a terminal window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet new console &lt;span class="nt"&gt;-n&lt;/span&gt; DeviceFlowApp
&lt;span class="nb"&gt;cd &lt;/span&gt;DeviceFlowApp
dotnet add package Auth0.AuthenticationApi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;Program.cs&lt;/code&gt; and replace its contents with the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Program.cs&lt;/span&gt;

&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Auth0.AuthenticationApi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Auth0.AuthenticationApi.Models&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Auth0.Core.Exceptions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"YOUR_AUTH0_DOMAIN"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;clientId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"YOUR_CLIENT_ID"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AuthenticationApiClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;deviceCodeResponse&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartDeviceFlowAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DeviceCodeRequest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="n"&gt;ClientId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="n"&gt;Scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"openid profile email"&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Activate this device:"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"  Visit:      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;deviceCodeResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VerificationUri&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"  Enter code: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;deviceCodeResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserCode&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"  Or open: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;deviceCodeResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VerificationUriComplete&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This first block requests the device code and displays the activation instructions.&lt;/p&gt;

&lt;p&gt;Replace &lt;code&gt;YOUR_AUTH0_DOMAIN&lt;/code&gt; and &lt;code&gt;YOUR_CLIENT_ID&lt;/code&gt; placeholders with the values you got in the Auth0 dashboard.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;StartDeviceFlowAsync()&lt;/code&gt; method calls the &lt;code&gt;/oauth/device/code&lt;/code&gt; endpoint and returns a &lt;code&gt;DeviceCodeResponse&lt;/code&gt; containing the activation details. The &lt;code&gt;Scope = "openid profile email"&lt;/code&gt; requests the standard OpenID Connect claims needed to read user profile information. Adjust this based on what your app needs.&lt;/p&gt;

&lt;p&gt;After receiving the response from Auth0, the application shows the verification URI and the code that the user must provide to authorize your application running on this device. The &lt;code&gt;VerificationUriComplete&lt;/code&gt; embeds the user code in the URL directly, which makes it suitable for QR code generation on devices with a display.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get an Access Token
&lt;/h2&gt;

&lt;p&gt;With the device code in hand, the next step is to poll Auth0 until the user authorizes the device. Start with the basic polling structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Program.cs&lt;/span&gt;

&lt;span class="c1"&gt;//...existing code...&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pollingInterval&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deviceCodeResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Interval&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;AccessTokenResponse&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;tokenResponse&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\nWaiting for authorization"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokenResponse&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&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="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pollingInterval&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

   &lt;span class="n"&gt;tokenResponse&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTokenAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DeviceCodeTokenRequest&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="n"&gt;ClientId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;DeviceCode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deviceCodeResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeviceCode&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;This works for the happy path, but &lt;code&gt;GetTokenAsync()&lt;/code&gt; will throw an exception when Auth0 returns &lt;code&gt;authorization_pending&lt;/code&gt; or &lt;code&gt;slow_down&lt;/code&gt;. Replace the &lt;code&gt;while&lt;/code&gt; block with this version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Program.cs&lt;/span&gt;

&lt;span class="c1"&gt;//...existing code...&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokenResponse&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&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="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pollingInterval&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

   &lt;span class="c1"&gt;// 👇changed code&lt;/span&gt;
   &lt;span class="k"&gt;try&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="n"&gt;tokenResponse&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTokenAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DeviceCodeTokenRequest&lt;/span&gt;
       &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="n"&gt;ClientId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;DeviceCode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deviceCodeResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeviceCode&lt;/span&gt;
       &lt;span class="p"&gt;});&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
   &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ErrorApiException&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ApiError&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"authorization_pending"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&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;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ErrorApiException&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ApiError&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"slow_down"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="n"&gt;pollingInterval&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
       &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here are the main points of this code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;authorization_pending&lt;/code&gt; is the expected response while the user hasn't acted yet. It's not a terminal error; it means keep waiting.
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;slow_down&lt;/code&gt; means you exceeded the polling rate. The &lt;a href="https://datatracker.ietf.org/doc/html/rfc8628#section-3.5" rel="noopener noreferrer"&gt;OAuth spec&lt;/a&gt; requires adding at least 5 seconds to the interval when this occurs.
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;deviceCodeResponse.Interval&lt;/code&gt; gives the minimum polling interval Auth0 requires, typically 5 seconds. Starting from this value keeps you within rate limits from the first poll.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once you receive the access token, use it to get the user profile information. Add the following code after the loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Program.cs&lt;/span&gt;

&lt;span class="c1"&gt;//...existing code...&lt;/span&gt;

&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\n\nDevice authorized!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;userInfo&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetUserInfoAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokenResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AccessToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Signed in as: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;userInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FullName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;userInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Access token: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tokenResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AccessToken&lt;/span&gt;&lt;span class="p"&gt;[..&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s"&gt;..."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;GetUserInfoAsync()&lt;/code&gt; calls Auth0's &lt;code&gt;/userinfo&lt;/code&gt; endpoint using the access token and returns the profile claims you requested in &lt;code&gt;Scope&lt;/code&gt;. This confirms the token works and identifies the authenticated user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test Your Application
&lt;/h2&gt;

&lt;p&gt;Now, run your application with &lt;code&gt;dotnet run&lt;/code&gt;. The initial expected output will be similar to the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Activate this device:
  Visit:      https://YOUR_AUTH0_DOMAIN/activate
  Enter code: HBTC-NDWC

  Or open: https://YOUR_AUTH0_DOMAIN/activate?user_code&lt;span class="o"&gt;=&lt;/span&gt;HBTC-NDWC

Waiting &lt;span class="k"&gt;for &lt;/span&gt;authorization...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open a browser on another device, or click the link if you are on a machine with a browser. You will see the following screen:&lt;br&gt;&lt;br&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.amazonaws.com%2Fuploads%2Farticles%2Fnq6mt6rkbnmma6iswewu.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.amazonaws.com%2Fuploads%2Farticles%2Fnq6mt6rkbnmma6iswewu.png" alt="Auth0 device authorization confirmation screen in the browser." width="800" height="897"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You will be asked to log in to confirm the code. After authentication, the polling loop picks up the tokens on its next iteration and exits. The following message will be added to your application’s output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Device authorized!
Signed &lt;span class="k"&gt;in &lt;/span&gt;as: Jane Smith &lt;span class="o"&gt;(&lt;/span&gt;jane@example.com&lt;span class="o"&gt;)&lt;/span&gt;
Access token: eyJhbGciOiJkaXIiLCJl...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Get an Access Token for Calling a Protected API
&lt;/h2&gt;

&lt;p&gt;The access token obtained with the current code only allows you to query the Auth0 &lt;code&gt;/userInfo&lt;/code&gt; endpoint to retrieve user profile information. In reality, this call is not strictly necessary: you'd typically read user profile claims directly from the ID token rather than making a round trip to &lt;code&gt;/userInfo&lt;/code&gt;. The call above is useful for confirming the token is valid and seeing who authenticated. If you want to inspect the raw ID token, print &lt;code&gt;tokenResponse.IdToken&lt;/code&gt; and decode it at &lt;a href="https://www.jwt.io/" rel="noopener noreferrer"&gt;jwt.io&lt;/a&gt;. You can do this as an exercise.&lt;/p&gt;

&lt;p&gt;If your application needs to access a protected API, you will need an access token for that specific API. Getting an access token for this is pretty easy: once you have the API’s &lt;a href="https://auth0.com/docs/glossary?term=audience" rel="noopener noreferrer"&gt;audience&lt;/a&gt; identifier, simply add the setting highlighted below to your authorization request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Program.cs&lt;/span&gt;

&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Net.Http.Headers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;//👈 new using&lt;/span&gt;

&lt;span class="c1"&gt;//...existing code...&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;deviceCodeResponse&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartDeviceFlowAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DeviceCodeRequest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="n"&gt;ClientId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="n"&gt;Scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"openid profile email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="n"&gt;Audience&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"YOUR_API_AUDIENCE"&lt;/span&gt;  &lt;span class="c1"&gt;//👈 new setting&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;//...existing code...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should also add the scopes you need for accessing the API based on the business logic of your application.&lt;/p&gt;

&lt;p&gt;That’s it! Now your application can call the protected API like in the following example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Program.cs&lt;/span&gt;

&lt;span class="c1"&gt;//...existing code...&lt;/span&gt;

&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpRequestMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpMethod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"https://your-api.com/endpoint"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Authorization&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AuthenticationHeaderValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Bearer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokenResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AccessToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"\nAPI response status: &lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReasonPhrase&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where To Go Next?
&lt;/h2&gt;

&lt;p&gt;You now have a working foundation for Device Authorization Flow in .NET. A few directions from here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Refresh tokens.&lt;/strong&gt; If your app needs to stay authenticated across sessions, configure your Auth0 application to issue refresh tokens and use &lt;code&gt;GetTokenAsync(new RefreshTokenRequest { ... })&lt;/code&gt; to renew the access token without user interaction.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handling token expiry.&lt;/strong&gt; The device code itself expires (&lt;code&gt;deviceCodeResponse.ExpiresIn&lt;/code&gt; seconds from issuance). Handle the &lt;code&gt;expired_token&lt;/code&gt; error from the polling loop and restart the flow when needed.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth0 .NET SDK reference.&lt;/strong&gt; The &lt;a href="https://auth0.github.io/auth0.net/" rel="noopener noreferrer"&gt;Authentication API SDK&lt;/a&gt;  documentation lists &lt;a href="https://auth0.github.io/auth0.net/api/Auth0.AuthenticationApi.html" rel="noopener noreferrer"&gt;all available methods and models&lt;/a&gt;, including request options for &lt;code&gt;StartDeviceFlowAsync()&lt;/code&gt; and &lt;code&gt;GetTokenAsync()&lt;/code&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The spec.&lt;/strong&gt; &lt;a href="https://datatracker.ietf.org/doc/html/rfc8628" rel="noopener noreferrer"&gt;RFC 8628&lt;/a&gt; is concise and readable. If your production implementation needs to handle edge cases (concurrent sessions, revocation, device re-authorization) it's worth consulting directly.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>authentication</category>
      <category>security</category>
    </item>
    <item>
      <title>The Many Faces of OAuth 2.0 Token Exchange</title>
      <dc:creator>Andrea Chiarelli</dc:creator>
      <pubDate>Wed, 03 Jun 2026 16:09:41 +0000</pubDate>
      <link>https://dev.to/auth0/the-many-faces-of-oauth-20-token-exchange-4gib</link>
      <guid>https://dev.to/auth0/the-many-faces-of-oauth-20-token-exchange-4gib</guid>
      <description>&lt;p&gt;Most developers face the need for token exchange when their architecture outgrows its initial assumptions. You have a service that holds a token for one purpose, and now it needs a different token for a different purpose. While simply forwarding the existing token or creating a fresh one might seem like intuitive solutions, both methods introduce significant issues. The OAuth 2.0 Token Exchange flow, as specified in &lt;a href="https://www.rfc-editor.org/rfc/rfc8693.html" rel="noopener noreferrer"&gt;RFC 8693&lt;/a&gt;, was explicitly created to address these challenges.&lt;/p&gt;

&lt;p&gt;Token exchange gives you a standards-based mechanism for converting one security token into another. The concept is straightforward: a client sends a token to the authorization server and receives a different token back, scoped for a different context, audience, or purpose. But behind this simple operation lie several distinct scenarios, each with its own trade-offs and security implications.&lt;/p&gt;

&lt;p&gt;Let's walk through the use cases that make token exchange essential in modern architectures, and see how they connect to solutions available today.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Token Exchange
&lt;/h2&gt;

&lt;p&gt;At its core, OAuth 2.0 Token Exchange is a protocol extension that turns the authorization server into a &lt;a href="https://datatracker.ietf.org/doc/html/rfc8693#name-introduction" rel="noopener noreferrer"&gt;Security Token Service (STS)&lt;/a&gt;, i.e., &lt;em&gt;a service capable of validating security tokens provided to it and issuing new security tokens in response&lt;/em&gt;. The flow is straightforward: A client sends a &lt;code&gt;subject_token&lt;/code&gt;, optionally accompanied by an &lt;code&gt;actor_token&lt;/code&gt;, and receives a new token appropriate for the target context. The &lt;code&gt;subject_token&lt;/code&gt; is the token representing the user or entity on whose behalf the request is being made; the &lt;code&gt;actor_token&lt;/code&gt; identifies the party performing the exchange.&lt;/p&gt;

&lt;p&gt;The specification defines two fundamental modes of operation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Impersonation&lt;/strong&gt;: The requesting party receives a token that &lt;strong&gt;makes it indistinguishable from the original subject&lt;/strong&gt;. The downstream service has no way to tell whether the actual user or an impersonating party is making the call.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delegation&lt;/strong&gt;: The requesting party receives a token that &lt;strong&gt;preserves both identities&lt;/strong&gt;. The downstream service knows who the user is &lt;em&gt;and&lt;/em&gt; who is acting on their behalf, typically through an &lt;code&gt;act&lt;/code&gt; (actor) claim embedded in the new token.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This distinction matters more than it might seem at first glance. Impersonation is powerful but opaque; delegation is more constrained but auditable. The choice between them shapes your security posture and your ability to trace what happened when something goes wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Administrative Impersonation
&lt;/h2&gt;

&lt;p&gt;Let's start by exploring the first intuitive use case: a customer reports that their dashboard shows incorrect data, but only for their account. Your support engineer needs to see exactly what the customer sees, with the same permissions and data access, to diagnose the issue.&lt;/p&gt;

&lt;p&gt;This is the classic administrative impersonation scenario. An administrator exchanges their own token for one that represents the customer's identity. The resulting token carries the customer's &lt;code&gt;sub&lt;/code&gt; claim, their scopes, and their audience. From the application's perspective, the request is coming from the customer.&lt;/p&gt;

&lt;p&gt;The token exchange request in this case uses only a &lt;code&gt;subject_token&lt;/code&gt; (identifying the user to impersonate) and provides no &lt;code&gt;actor_token&lt;/code&gt;, because the intent is full impersonation. The authorization server validates that the requesting party has impersonation privileges and issues a token bound to the target user's identity.&lt;/p&gt;

&lt;p&gt;There's a problem, though: because this is true impersonation, you lose the audit trail of &lt;em&gt;who&lt;/em&gt; actually performed the actions. The downstream service cannot distinguish between the real user and the admin. This is why &lt;strong&gt;impersonation-based token exchange should be paired with external logging mechanisms that record who initiated the exchange, when, and for what reason&lt;/strong&gt;. Without that, you're creating a security gap in the name of troubleshooting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing Protocol Transition
&lt;/h2&gt;

&lt;p&gt;Organizations rarely get to start from scratch. Legacy systems persist, and with them persist older protocols. A common scenario is transitioning from SAML-based authentication to OpenID Connect (OIDC) while keeping both systems operational during the migration.&lt;/p&gt;

&lt;p&gt;Token exchange handles this gracefully. A service that authenticates users via SAML can exchange the SAML assertion for an OAuth 2.0 access token. The authorization server validates the incoming SAML artifact and issues a standard access token that downstream services understand.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;subject_token_type&lt;/code&gt; parameter in the exchange request identifies the format of the incoming token. RFC 8693 defines several token type identifiers, including &lt;code&gt;urn:ietf:params:oauth:token-type:saml2&lt;/code&gt; for SAML 2.0 assertions and &lt;code&gt;urn:ietf:params:oauth:token-type:jwt&lt;/code&gt; for JWT tokens. This flexibility means the authorization server can accept tokens from different protocol families and normalize them into a consistent format for your modern services.&lt;/p&gt;

&lt;p&gt;In practice, this pattern appears during company acquisitions and mergers where two organizations with different identity stacks need to interoperate before a full migration is complete. Rather than forcing all users to re-authenticate against a new system, token exchange bridges the gap: users authenticate as they always have, and the system translates their credentials into whatever the consuming service requires.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chaining Service Calls
&lt;/h2&gt;

&lt;p&gt;This is where token exchange becomes indispensable for most teams. You have a microservices architecture where Service A receives a user request, processes it, and needs to call Service B on the user's behalf. Service B might then need to call Service C.&lt;/p&gt;

&lt;p&gt;The following diagram summarizes this scenario:&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.amazonaws.com%2Fuploads%2Farticles%2Fdgnwt9ybo334o8iybeft.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.amazonaws.com%2Fuploads%2Farticles%2Fdgnwt9ybo334o8iybeft.png" alt="Chaining service calls diagram" width="795" height="158"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The question is: what credentials does each service use when calling the next one?&lt;/p&gt;

&lt;p&gt;There are three options, each with clear trade-offs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 1: The service uses its own credentials
&lt;/h3&gt;

&lt;p&gt;Service A calls Service B using its own client credentials, ignoring the user context entirely. This is the simplest approach and works well for service-to-service calls that don't need user context (batch processing, system health checks, data synchronization).&lt;/p&gt;

&lt;p&gt;However, this option has a problem when a user is involved or your services need the full context. If Service A uses its own credentials, Service B cannot enforce user-level authorization. It doesn't know which user triggered the request, so it can't check whether that user should have access to the requested resource. You've lost the security context that makes accurate authorization possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: The service impersonates the user
&lt;/h3&gt;

&lt;p&gt;Service A passes the user's original token directly to Service B, or exchanges it for a token that makes Service A indistinguishable from the user. Service B sees a request that appears to come from the user and applies user-level authorization.&lt;/p&gt;

&lt;p&gt;In this case, the problem is that Service B cannot distinguish the user's direct actions from actions taken by Service A on the user's behalf. If Service A is compromised, it can make any call the user is authorized to make with no way for downstream services to apply different trust levels to direct vs. proxied requests. You've preserved the user context but lost the service context.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 3: The service acts on behalf of the user (Delegation)
&lt;/h3&gt;

&lt;p&gt;Service A exchanges the user's token for a new token that identifies both the user (as subject) and Service A (as actor). The resulting token carries an &lt;code&gt;act&lt;/code&gt; claim that tells Service B: "This request is about User X, performed by Service A."&lt;/p&gt;

&lt;p&gt;This is the delegation model, and it's the one RFC 8693 was primarily designed to support. Service B can now make nuanced authorization decisions: "User X can read this data, and Service A is authorized to act on User X's behalf, so this request is allowed." If Service A tries to access something the user hasn't authorized it to do, the request fails.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;act&lt;/code&gt; claim is nestable, meaning that if Service B then calls Service C on behalf of the user, the delegation chain grows: Service C sees that Service B is acting on behalf of User X with Service A as the original actor. This chain provides a complete audit trail of how the request propagated through the system.&lt;/p&gt;

&lt;p&gt;The trade-off here is complexity. Each hop requires a token exchange, which adds latency and requires each service to be registered as a client with the authorization server. But for architectures where security and auditability matter (and they always should), delegation is the principled choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Token Exchange and Federated Identity
&lt;/h2&gt;

&lt;p&gt;The chain-of-calls scenario becomes significantly more complex when services span security domains, for example, when services are provided by third-party organizations. Consider the situation shown by the following diagram:&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.amazonaws.com%2Fuploads%2Farticles%2F8tqmpduzey858yr8yww5.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.amazonaws.com%2Fuploads%2Farticles%2F8tqmpduzey858yr8yww5.png" alt="Cross-domain chaining service calls diagram" width="799" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Service A has a token to access Service B on behalf of a user in  MyCompany’s security domain.
&lt;/li&gt;
&lt;li&gt;Service B needs to call Service C, which is protected in  the External Provider’s domain.
&lt;/li&gt;
&lt;li&gt;Service B needs an access token valid in MyCompany’s domain to access Service C on behalf of the user.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the federated identity challenge. The token that Service B received from MyCompany's authorization server means nothing to the External Provider’s domain. A token issued by one authorization server is not automatically accepted by another, and for good reason: trust boundaries exist to limit blast radius.&lt;/p&gt;

&lt;p&gt;In a standard token exchange, Service B would present its token to the External Provider's authorization server and request a new token. But the External Provider's authorization server has no reason to trust a token issued by MyCompany unless a trust relationship has been explicitly established.&lt;/p&gt;

&lt;p&gt;Solving this requires federation at the authorization server level. External Provider's authorization server must be configured to accept tokens from MyCompany domain as valid subject tokens in an exchange request. This involves pre-established trust (through metadata exchange, certificate validation, or direct configuration) and mapping between the identity representations in each domain.&lt;/p&gt;

&lt;p&gt;In practice, this is where token exchange alone starts showing its limitations. Each cross-domain hop requires explicit trust configuration. As the number of domains grows (enterprise integrations, SaaS ecosystems, multi-cloud deployments), the matrix of bilateral trust relationships becomes unmanageable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cross App Access and Identity Chaining
&lt;/h3&gt;

&lt;p&gt;This scaling problem is precisely what &lt;a href="https://oauth.net/cross-app-access/" rel="noopener noreferrer"&gt;Cross App Access&lt;/a&gt; (XAA) aims to solve. XAA implements the &lt;a href="https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-assertion-authz-grant/" rel="noopener noreferrer"&gt;Identity Assertion JWT Authorization Grant&lt;/a&gt;, an OAuth extension that introduces a mediator into the cross-domain exchange: the enterprise identity provider (IdP).&lt;/p&gt;

&lt;p&gt;The key insight is that the IdP already knows about both applications and the user's relationship to each. Rather than requiring every pair of domains to establish bilateral trust, XAA centralizes access decisions in the IdP. Let's see how this works concretely.&lt;/p&gt;

&lt;p&gt;The flow involves four parties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Requesting App&lt;/strong&gt;: The application (or AI agent) in MyCompany domain that needs to access a resource in another domain (External Provider).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise IdP&lt;/strong&gt;: The identity provider in MyCompany domain that authenticates employees and manages cross-app policies.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource App&lt;/strong&gt;: The application that owns the protected API in the External Provider domain.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource Authorization Server&lt;/strong&gt;: The authorization server that issues access tokens for the Resource App's protected API in the External Provider domain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following diagram shows how the exchange unfolds:&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.amazonaws.com%2Fuploads%2Farticles%2Fiqh6w1ri7r2cw8yd67xn.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.amazonaws.com%2Fuploads%2Farticles%2Fiqh6w1ri7r2cw8yd67xn.png" alt="Cross App Access - XAA authorization flow" width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is the description of each step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The employee logs in to the Requesting App via SSO and obtains an ID token from the IdP.
&lt;/li&gt;
&lt;li&gt;The Requesting App sends that ID token back to the IdP, asking for a cross-domain identity assertion (an &lt;strong&gt;ID-JAG&lt;/strong&gt;, a special JWT scoped for cross-app use).
&lt;/li&gt;
&lt;li&gt;The IdP checks its XAA policy: is this Requesting App allowed to access the Resource App on behalf of this user? If yes, it returns the ID-JAG.
&lt;/li&gt;
&lt;li&gt;The Requesting App presents the ID-JAG to the Resource App's authorization server.
&lt;/li&gt;
&lt;li&gt;The Resource App's authorization server validates the ID-JAG (using the IdP's public keys via OIDC discovery) and issues an access token.
&lt;/li&gt;
&lt;li&gt;The Requesting App calls the Resource App's API with that access token.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Notice the critical difference from plain token exchange: in step 3, the IdP enforces a policy decision. An administrator explicitly configures which apps can reach which resources, giving IT visibility and control over cross-app data sharing. Users don't face repetitive consent flows, and the organization maintains a centralized view of who can access what.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Identity Chaining&lt;/strong&gt; is the broader term for this pattern: the user's identity assertion flows from the original authentication through every downstream service in a standardized way, without requiring ad-hoc trust configuration at each boundary. XAA is one concrete implementation of identity chaining, built on OAuth primitives.&lt;/p&gt;

&lt;p&gt;This approach is particularly relevant in scenarios involving AI agents, where a single user request might trigger calls to services across multiple third-party providers, each with their own security domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Token Exchange and Auth0
&lt;/h2&gt;

&lt;p&gt;Auth0 implements token exchange through mechanisms that address different points in the spectrum we've discussed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom Token Exchange
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://auth0.com/docs/authenticate/custom-token-exchange" rel="noopener noreferrer"&gt;Custom Token Exchange&lt;/a&gt; implements RFC 8693 on Auth0's &lt;code&gt;/oauth/token&lt;/code&gt; endpoint with full developer control over the validation logic. You define a &lt;strong&gt;Token Exchange Profile&lt;/strong&gt; that maps a &lt;code&gt;subject_token_type&lt;/code&gt; URI to a custom Action. When a token exchange request arrives, Auth0 invokes your Action code to validate the incoming token, enforce authorization rules, and associate it with a user in your tenant.&lt;/p&gt;

&lt;p&gt;This is the mechanism for protocol transitions and custom federation scenarios. Auth0 treats the &lt;code&gt;subject_token&lt;/code&gt; as an opaque string, meaning you can accept any token format: JWTs from another identity provider, SAML assertions from a legacy system, or proprietary tokens from a partner's API. Your Action code owns the validation logic, and Auth0 handles the issuance of a standards-compliant token on the other side.&lt;/p&gt;

&lt;p&gt;Check out &lt;a href="https://auth0.com/blog/developers-guide-flexible-token-exchange-auth0/" rel="noopener noreferrer"&gt;this article for practical introduction to Custom Token Exchange in Auth0&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Token Vault
&lt;/h3&gt;

&lt;p&gt;For the AI agent scenario, where services need to call third-party APIs on behalf of users across multiple providers, Auth0 offers &lt;a href="https://auth0.com/ai/docs/intro/token-vault" rel="noopener noreferrer"&gt;Token Vault&lt;/a&gt;. This builds on token exchange but adds secure storage and automatic lifecycle management for third-party tokens.&lt;/p&gt;

&lt;p&gt;The flow works like this: a user authenticates and connects their accounts (Google, GitHub, Slack, Microsoft, and others). Token Vault stores the resulting tokens securely and handles refresh automatically. When an AI agent needs to call a third-party API on behalf of that user, it performs a token exchange to retrieve a valid access token from the vault.&lt;/p&gt;

&lt;p&gt;The resulting token includes an &lt;code&gt;act&lt;/code&gt; claim that identifies the AI agent, creating an audit trail of which agent accessed which service on behalf of which user. This is critical for enterprise deployments where compliance requires knowing not just what happened, but what automation triggered it.&lt;/p&gt;

&lt;p&gt;Take a look at &lt;a href="https://auth0.com/blog/auth0-token-vault-secure-token-exchange-for-ai-agents/" rel="noopener noreferrer"&gt;this article for a quick introduction to Token Vault&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  On-Behalf-Of Token Exchange
&lt;/h3&gt;

&lt;p&gt;For the service chain scenario, Auth0's &lt;a href="https://auth0.com/docs/secure/call-apis-on-users-behalf/on-behalf-of-token-exchange" rel="noopener noreferrer"&gt;On-Behalf-Of (OBO) token exchange&lt;/a&gt; implements the delegation pattern directly. A middle-tier service exchanges the incoming user token for a new token scoped to the downstream API, preserving the user's identity while adding itself to the delegation chain via the &lt;code&gt;act&lt;/code&gt; claim.&lt;/p&gt;

&lt;p&gt;Auth0 supports up to five levels of delegation chain nesting, providing visibility into the full path a request takes through your architecture. Each token in the chain carries the &lt;code&gt;sub&lt;/code&gt; claim (maintaining user identity), the &lt;code&gt;aud&lt;/code&gt; claim (scoped to the target service), and the nested &lt;code&gt;act&lt;/code&gt; claim (recording the chain of services involved).&lt;/p&gt;

&lt;h3&gt;
  
  
  Cross App Access
&lt;/h3&gt;

&lt;p&gt;For the federated identity scenario, where a requesting application needs to call a resource API protected by a different authorization server, Auth0 supports &lt;a href="https://auth0.com/docs/secure/call-apis-on-users-behalf/xaa" rel="noopener noreferrer"&gt;Cross App Access&lt;/a&gt; (XAA). This feature implements the Identity Assertion Authorization Grant OAuth extension, enabling applications and AI agents to obtain tokens for cross-domain API calls without requiring users to go through repetitive consent flows.&lt;/p&gt;

&lt;p&gt;The mechanism works through Auth0 acting as the Resource Authorization Server. When the Requesting App needs to reach a Resource App registered with Auth0, it sends the user's ID token to its IdP, such as Okta, and receives an ID-JAG (Identity Assertion JWT) in return. The IdP issues this assertion only if an administrator has configured the cross-app connection in the Admin Console. The Requesting App then presents the ID-JAG to the Resource App's authorization server, i.e., Auth0, which validates it via OIDC discovery and issues a scoped access token.&lt;/p&gt;

&lt;p&gt;This gives IT centralized visibility into cross-app data sharing. Administrators define which applications can reach which resources, and Auth0 enforces those policies at exchange time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the Right Approach
&lt;/h2&gt;

&lt;p&gt;Token exchange is not a single solution but a family of patterns. The right choice depends on what context you need to preserve and what trust boundaries you need to cross:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Administrative impersonation&lt;/strong&gt; when troubleshooting requires seeing exactly what the user sees
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protocol transition&lt;/strong&gt; when bridging legacy and modern identity systems during migrations
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delegation&lt;/strong&gt; when service chains need user context with full auditability
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross App Access / Identity Chaining&lt;/strong&gt; when delegation spans multiple security domains
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token Vault&lt;/strong&gt; when AI agents need managed access to third-party APIs on behalf of users&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The spread of AI agents has made token exchange more relevant than ever. An AI agent that orchestrates actions across multiple services and multiple providers on a user's behalf is, fundamentally, a delegation chain. The mechanisms we've discussed provide the security foundation for making that chain auditable, authorized, and scoped to what the user actually intended.&lt;/p&gt;

&lt;p&gt;The token exchange specification gives us the vocabulary. Implementations like Auth0's give us the tools. The choice of which pattern to apply at each boundary is yours.&lt;/p&gt;

</description>
      <category>oauth</category>
      <category>security</category>
      <category>identity</category>
      <category>microservices</category>
    </item>
    <item>
      <title>How to Decode, Encode, and Validate JWTs inside Claude Code</title>
      <dc:creator>Jessica Temporal</dc:creator>
      <pubDate>Fri, 29 May 2026 17:42:15 +0000</pubDate>
      <link>https://dev.to/auth0/how-to-decode-encode-and-validate-jwts-inside-claude-code-240b</link>
      <guid>https://dev.to/auth0/how-to-decode-encode-and-validate-jwts-inside-claude-code-240b</guid>
      <description>&lt;p&gt;&lt;em&gt;Stop context switching and start debugging.&lt;/em&gt; Let me show you how to supercharge your AI agent with JWT Skills. Learn how to decode, encode, and validate JSON Web Tokens (JWTs) directly within your terminal session using Claude Code. Whether you're troubleshooting an expired token or testing a custom claim, these tools bring security insights right into your development flow.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What You'll Learn:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to install the &lt;code&gt;jwt-skills&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Decoding tokens to inspect headers, payloads, and claims.&lt;/li&gt;
&lt;li&gt;Identifying security flags like expired tokens or PII in payloads.&lt;/li&gt;
&lt;li&gt;Generating test tokens with custom claims for edge-case testing.&lt;/li&gt;
&lt;li&gt;Validating tokens against JWKS endpoints for production-level checks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Resources:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🛠 &lt;em&gt;&lt;a href="https://github.com/jsonwebtoken/jwt-skills" rel="noopener noreferrer"&gt;Get the Skills&lt;/a&gt;&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;📖 &lt;em&gt;&lt;a href="https://jwt.io" rel="noopener noreferrer"&gt;JWT Deep Dive&lt;/a&gt;&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>productivity</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Your Team's Productivity Metric is Probably Useless</title>
      <dc:creator>Carla Urrea Stabile</dc:creator>
      <pubDate>Wed, 20 May 2026 13:38:00 +0000</pubDate>
      <link>https://dev.to/auth0/your-teams-productivity-metric-is-probably-useless-3i6</link>
      <guid>https://dev.to/auth0/your-teams-productivity-metric-is-probably-useless-3i6</guid>
      <description>&lt;p&gt;If your team is measuring developer productivity by lines of code or number of commits, you're measuring the wrong thing. So what should you be looking at instead?&lt;/p&gt;

&lt;p&gt;In Episode 5 of Making Software, I talked to &lt;strong&gt;Dennis Henry&lt;/strong&gt;, Productivity Architect at Okta. Dennis has a master's degree in human factors, and he explained to me how he applies it to software engineering every day.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The science of failure, applied to your codebase.&lt;/strong&gt; Human factors studies how people interact with systems and how things go wrong. Dennis explains why "human error" is never the real root cause in production incidents, and what you should be looking for instead.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A multi-model AI gateway for an entire company.&lt;/strong&gt; Dennis built an internal platform that gives every Okta employee secure access to any Anthropic, OpenAI, and more models, that lives behind SSO with PII guardrails and MCP call auditing. He walks through why locking into one vendor right now is a mistake and how to architect around that.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Improving Engineering Productivity&lt;/strong&gt; Dennis constantly asks engineers: "What pisses you off? What makes you want to throw your keyboard across the room?" He makes the case that happy people make the best software, and that measuring sentiment matters more than measuring output.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;What to actually track.&lt;/strong&gt; Lines of code? Terrible. Commits? Meaningless. Dennis argues for reliability, bug resolution time, customer experience, and whether your team feels like they have the tools to do their job. Qualitative over quantitative.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Productivity isn't about squeezing more output out of people. It's about removing the things that get in the way of meaningful work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the worst productivity metric you've seen used on a team?&lt;/strong&gt; Let me know in the comments!&lt;/p&gt;

&lt;h2&gt;
  
  
  Listen to the full episode
&lt;/h2&gt;

&lt;p&gt;Available on &lt;a href="https://www.youtube.com/playlist?list=PLZ14qQz3cfJKRDmX3yasmbwoC4kipeQfu" rel="noopener noreferrer"&gt;&lt;strong&gt;YouTube&lt;/strong&gt;&lt;/a&gt;, &lt;a href="https://podcasts.apple.com/us/podcast/making-software/id1872107131" rel="noopener noreferrer"&gt;&lt;strong&gt;Apple Podcasts&lt;/strong&gt;&lt;/a&gt;, and &lt;a href="https://open.spotify.com/show/6J856S2fijMvP3rzFkRnBi" rel="noopener noreferrer"&gt;&lt;strong&gt;Spotify&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thanks for reading! 👋&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>security</category>
    </item>
    <item>
      <title>What AI Tools, MCP Servers, and Skills Actually Do</title>
      <dc:creator>Andrea Chiarelli</dc:creator>
      <pubDate>Tue, 19 May 2026 13:39:13 +0000</pubDate>
      <link>https://dev.to/auth0/what-ai-tools-mcp-servers-and-skills-actually-do-7ep</link>
      <guid>https://dev.to/auth0/what-ai-tools-mcp-servers-and-skills-actually-do-7ep</guid>
      <description>&lt;p&gt;I remember being very confused when I first heard about an LLM's ability to request code execution. This feature has been called various names: tool, action, plugin, function. Now the terminology is settling on a single name: tool. However, talking to other developers and reading comments online, I see the confusion has shifted elsewhere. Some argue that, &lt;a href="https://www.linkedin.com/posts/jason-lopatecki-9509941_why-skills-are-exploding-and-mcp-is-already-activity-7443091557408219136-R617" rel="noopener noreferrer"&gt;with the introduction of skills by Anthropic, MCP no longer makes sense&lt;/a&gt;, and &lt;a href="https://www.reddit.com/r/mcp/comments/1kkyajj/could_you_explain_how_mcps_are_different_and/" rel="noopener noreferrer"&gt;others aren't convinced of the usefulness of MCP&lt;/a&gt; compared to direct tool calls.&lt;/p&gt;

&lt;p&gt;The confusion is architectural. Tools, MCP servers, and skills solve different problems at different layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Tools Are the Model's Swiss Army Knife
&lt;/h2&gt;

&lt;p&gt;At the most fundamental level, an AI tool is a function that a language model can decide to call. Not a metaphor for "capability" in general, but a specific, callable function with a defined input schema and a predictable output.&lt;/p&gt;

&lt;p&gt;When a model needs information or an action it can't produce from training alone, it generates a structured call to a tool: something like "call &lt;code&gt;get_customer_record&lt;/code&gt; with &lt;code&gt;customer_id: 12345&lt;/code&gt;." The host application intercepts that call, runs the actual function, and returns the result to the model. The model then incorporates it and continues.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;While the tool calling concept is the same across the different LLMs, each one has its own specifications to invoke tools. Check out &lt;a href="https://developers.openai.com/api/docs/guides/function-calling" rel="noopener noreferrer"&gt;OpenAI&lt;/a&gt;, &lt;a href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview" rel="noopener noreferrer"&gt;Claude&lt;/a&gt;, or &lt;a href="https://ai.google.dev/gemini-api/docs/function-calling" rel="noopener noreferrer"&gt;Gemini&lt;/a&gt; documentation for some specific examples.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Tools are what give AI its "hands." Without them, a model is impressively articulate but isolated from live data and the systems where real work happens. With them, it can search databases, call APIs, send messages, or trigger business logic in response to what a user needs.&lt;/p&gt;

&lt;p&gt;The key property to hold onto: &lt;strong&gt;a tool should know how to execute one thing&lt;/strong&gt;. It doesn't decide when to be called. That judgment belongs to the model, or in more complex systems, to something above it.&lt;/p&gt;

&lt;p&gt;Here's what that looks like in code. Using the &lt;a href="https://docs.anthropic.com/en/api/getting-started" rel="noopener noreferrer"&gt;Anthropic SDK&lt;/a&gt;, you define a tool as a JSON schema: its name, a description the model uses to decide when to invoke it, and the inputs it expects:&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;anthropic&lt;/span&gt;
&lt;span class="err"&gt;​&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;tools&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;name&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;get_customer_record&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;description&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;Retrieve a customer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s account details by 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;input_schema&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;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;object&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;properties&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;customer_id&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;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;string&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;description&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;The unique customer identifier&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;required&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;customer_id&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
   &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-4-6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="n"&gt;messages&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;role&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;user&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;content&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;Look up customer C-12345&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;# When the model decides to use the tool, it returns a structured tool_use block
&lt;/span&gt;&lt;span class="n"&gt;tool_use&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="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&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;tool_use&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="c1"&gt;# get_customer_record
&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;tool_use&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# {"customer_id": "C-12345"}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things to notice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;description&lt;/code&gt;:&lt;/strong&gt; This is what the model reads to decide whether to call the tool. A clear, accurate description matters more than the schema itself.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;input_schema&lt;/code&gt;:&lt;/strong&gt; The structured contract. The model generates arguments that conform to this schema when it decides to invoke the tool.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tool_use&lt;/code&gt; block:&lt;/strong&gt; What the model provides when it wants to call a tool. Your application handles the actual execution and passes the result back in the next turn to continue the conversation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model never runs the function itself. It produces a structured intent. Your application does the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP Servers Are the Universal Translator
&lt;/h2&gt;

&lt;p&gt;At first glance, an MCP server might look like a more elaborate way to define tools. In practice, it solves a different problem.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol (MCP)&lt;/a&gt; is an open standard that introduces a protocol layer between AI applications and the services they connect to. Think of it as USB-C for AI: a universal connector that lets any compliant client communicate with any compliant server, regardless of which model runs underneath.&lt;/p&gt;

&lt;p&gt;Without MCP, every AI application builds its own integrations: a custom function schema for one client, a different one for another, yet another for whichever agent framework a team has chosen. The same capability gets re-implemented for each consumer. MCP exists to eliminate that duplication.&lt;/p&gt;

&lt;p&gt;Here's how the architecture works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MCP clients&lt;/strong&gt; are the components that &lt;em&gt;hosts&lt;/em&gt; (i.e., AI applications such as Claude Desktop, a VS Code extension, a custom agent, etc.) instantiate to initiate connections to servers.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP servers&lt;/strong&gt; expose three types of primitives:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tools:&lt;/strong&gt; actions the AI can invoke.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resources:&lt;/strong&gt; data the AI can read.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompts:&lt;/strong&gt; pre-built interaction templates.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;The MCP transport layer is equally flexible. The protocol runs over standard I/O for local processes, or HTTP with Server-Sent Events for remote services, using OAuth 2.1 for authentication on remote connections.&lt;/p&gt;

&lt;p&gt;What makes MCP structurally different from direct tool definitions is &lt;strong&gt;decoupling&lt;/strong&gt;. A tool definition embedded in a model API call is tightly coupled to that application. An MCP server is portable: the same server works across any compliant client. Build a GitHub integration once, connect it everywhere.&lt;/p&gt;

&lt;p&gt;There's also the matter of bidirectionality: an MCP server can &lt;a href="https://modelcontextprotocol.io/specification/2025-11-25/client/sampling" rel="noopener noreferrer"&gt;request that the AI client sample from the language model&lt;/a&gt;, enabling interactions that go beyond a simple request-response pattern like an API. It's a more symmetric relationship than direct function calling, which flows strictly from model to function.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Skills Direct the Work
&lt;/h2&gt;

&lt;p&gt;Skills occupy a different layer from tools and MCP servers entirely.&lt;/p&gt;

&lt;p&gt;Where a tool is a capability and an MCP server is the infrastructure for exposing it, a skill is closer to a recipe: &lt;strong&gt;higher-level instructions that tell an AI when and how to use its available capabilities to accomplish a complex goal&lt;/strong&gt;. A skill doesn't replace tools. It orchestrates them.&lt;/p&gt;

&lt;p&gt;In practice, a skill combines three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;System prompts:&lt;/strong&gt; Instructions that define a persona and a set of constraints.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logic/Workflows:&lt;/strong&gt; A series of steps the model should follow.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool selection strategy:&lt;/strong&gt; Knowing which tool to pick in a specific sequence to achieve a complex goal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consider a customer support scenario. Handling a refund request might involve checking the customer record, looking up the original transaction, verifying return eligibility based on policy, and then either initiating the refund or escalating to a human, as shown in the following diagram: &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.amazonaws.com%2Fuploads%2Farticles%2Feqvpui78miuwwcho9vr5.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.amazonaws.com%2Fuploads%2Farticles%2Feqvpui78miuwwcho9vr5.png" alt="Diagram showing the flow for a purchase refund AI skill" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each step might invoke a different tool. The skill is the framework that defines the sequence, the conditions, and the decision points. Skills carry the domain knowledge and reasoning structure that raw tool access doesn't provide.&lt;/p&gt;

&lt;p&gt;Anthropic formalized the skills concept for Claude Code, and the format is now an &lt;a href="https://agentskills.io/" rel="noopener noreferrer"&gt;open standard&lt;/a&gt; gaining adoption across AI coding tools. The refund scenario above could be expressed as in the following example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;handle-refund-request&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Handle&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;refund&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;request&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;user."&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

You are a customer support agent for Acme Corp.
You have access to: &lt;span class="sb"&gt;`get_customer_record`&lt;/span&gt;, &lt;span class="sb"&gt;`get_transaction`&lt;/span&gt;,
&lt;span class="sb"&gt;`initiate_refund`&lt;/span&gt;, &lt;span class="sb"&gt;`escalate_to_human`&lt;/span&gt;.

When a user requests a refund:
&lt;span class="p"&gt;
1.&lt;/span&gt; Verify the account with &lt;span class="sb"&gt;`get_customer_record`&lt;/span&gt;
&lt;span class="p"&gt;2.&lt;/span&gt; Retrieve the purchase details with &lt;span class="sb"&gt;`get_transaction`&lt;/span&gt;
&lt;span class="p"&gt;3.&lt;/span&gt; If the purchase was within 30 days and its amout is less than $500, call &lt;span class="sb"&gt;`initiate_refund`&lt;/span&gt;
&lt;span class="p"&gt;4.&lt;/span&gt; For amounts over $500, always escalate regardless of purchase date
&lt;span class="p"&gt;5.&lt;/span&gt; If ineligible, call &lt;span class="sb"&gt;`escalate_to_human`&lt;/span&gt; and explain why
​
Never approve a refund without completing steps 1 and 2 first.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare this to the tool definition from the previous section. The tool schema said nothing about when to be called, what order to follow, or what to do when an amount exceeds a threshold. The skill encodes all of that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Available tools:&lt;/strong&gt; The skill names the specific tools relevant to this task, not everything the agent has access to.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sequence and conditions:&lt;/strong&gt; Steps 1–5 define the order and the branching logic.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guard rails:&lt;/strong&gt; "Never approve without completing steps 1 and 2" is judgment the tools themselves can't encode.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tools handle execution. The skill handles when, in what order, and under what conditions.&lt;/p&gt;

&lt;p&gt;The distinction from tools becomes clearest at the boundary between capability and judgment. A tool knows how to execute a database query. A skill knows when a query is the right mechanism versus, say, asking a user for clarification first. That's the bridge skills provide: from what an AI can do to what it should do in a given scenario.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Differences at a Glance
&lt;/h2&gt;

&lt;p&gt;The distinction between tools, MCP servers, and skills lies in their architectural layer, scope, and security considerations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Definition and scope:&lt;/strong&gt; A &lt;strong&gt;tool&lt;/strong&gt; is a fundamental capability, defined as a single, specific callable function that the model can invoke via its API schema. Above this is the &lt;strong&gt;MCP server&lt;/strong&gt;, which acts as a standardized protocol server. It exposes a collection of capabilities, including actions, data, and prompts, which are used by AI clients via the MCP protocol. The highest layer is the &lt;strong&gt;skill&lt;/strong&gt;, which is not a capability but a set of instructions guiding AI behavior toward a complex goal, managing a multi-step workflow or domain reasoning through the agent's orchestration layer.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coupling and portability:&lt;/strong&gt; Another key difference is portability. &lt;strong&gt;Tools&lt;/strong&gt; are tightly coupled to the application they are defined within. In contrast, &lt;strong&gt;MCP servers&lt;/strong&gt; are portable, designed to work across any compliant client, making them a universal connector. &lt;strong&gt;Skills&lt;/strong&gt; are specific to the particular task or domain they are designed for.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security surface:&lt;/strong&gt; Each layer presents a distinct security surface. Security for &lt;strong&gt;tools&lt;/strong&gt; focuses on function-level permissions. &lt;strong&gt;MCP servers&lt;/strong&gt; manage risk through protocol-level consent and capability negotiation. &lt;strong&gt;Skills&lt;/strong&gt; introduce behavioral guardrails and decision boundaries, controlling the overall logic and sequence of actions to ensure appropriate conduct.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following picture summarizes these distinctions:&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.amazonaws.com%2Fuploads%2Farticles%2Fe3rviwmmxo9xfa5sztfc.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.amazonaws.com%2Fuploads%2Farticles%2Fe3rviwmmxo9xfa5sztfc.png" alt="Table showing the architectural layer, scope, and security considerations for AI tools, MCP servers, and skills." width="800" height="786"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The relationship is hierarchical: skills direct what to accomplish, tools do the work, and MCP servers are the infrastructure that makes tools available consistently. None of the three replaces the others.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Security Perspective
&lt;/h2&gt;

&lt;p&gt;Each layer introduces distinct security considerations, and they compound when combined.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At the tool layer&lt;/strong&gt;, the core risk is over-permission. A tool that can read and write to a database, when the AI only needs to read, creates unnecessary exposure. &lt;a href="https://owasp.org/www-community/attacks/PromptInjection" rel="noopener noreferrer"&gt;Prompt injection attacks&lt;/a&gt; (where malicious content in retrieved data tricks a model into taking unintended actions) are most damaging when tools have broad permissions. The principle of least privilege applies here as directly as it does anywhere in security engineering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At the MCP server layer&lt;/strong&gt;, the attack surface expands because MCP servers are external processes, often network-accessible, and connected to by multiple clients. The risks here include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://owasp.org/www-community/attacks/MCP_Tool_Poisoning" rel="noopener noreferrer"&gt;&lt;strong&gt;Tool poisoning&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;:&lt;/strong&gt; A malicious or compromised server exposes functions that look legitimate but behave maliciously when called.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unauthorized access:&lt;/strong&gt; Without proper authentication, any client could invoke a server's capabilities, including sensitive ones.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope creep:&lt;/strong&gt; Servers that expose more than intended, by design or misconfiguration, give connected clients broader access than warranted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://modelcontextprotocol.io/specification" rel="noopener noreferrer"&gt;MCP specification&lt;/a&gt; addresses this through explicit consent requirements and capability negotiation, but correct implementation is the server author's responsibility. Remote MCP servers should require OAuth 2.1-based authentication, and permissions granted to any connection should follow the least-privilege principle.&lt;/p&gt;

&lt;p&gt;To mitigate risks in both tools and MCP servers, apply the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strict Scoping:&lt;/strong&gt; Never give an AI tool a "God Mode" API key. Use scoped tokens that only allow the specific actions required for that tool.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human-in-the-Loop:&lt;/strong&gt; For high-stakes tools (like making a payment or deleting data), always require an approval from the user before the application executes the model's tool call.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input Validation:&lt;/strong&gt; Treat every argument generated by an LLM as untrusted user input. Validate the types, ranges, and permissions before hitting your backend.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;At the skills layer&lt;/strong&gt;, the risks become behavioral. Skills that don't account for adversarial inputs, that allow irreversible actions without human confirmation, or that chain too many autonomous steps without a checkpoint are dangerous in a specific way. By the time someone can intervene, the damage is done.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://auth0.com/ai/docs" rel="noopener noreferrer"&gt;Auth0 for AI Agents&lt;/a&gt; addresses several of these challenges at the identity layer. Based on standards, it provides support for user authentication, delegation patterns for acting on a user's behalf, and asynchronous authorization for scenarios where sensitive operations need human sign-off before proceeding. Auth0's MCP support, &lt;a href="https://auth0.com/blog/auth0-auth-for-mcp-servers-generally-available" rel="noopener noreferrer"&gt;now generally available&lt;/a&gt;, provides OAuth 2.1-based authentication for MCP servers.&lt;/p&gt;

&lt;p&gt;The underlying principle is familiar from traditional IAM: identity infrastructure doesn't fundamentally change because the client is an AI, but the patterns for applying it need to account for how agentic systems behave differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Layers, One System
&lt;/h2&gt;

&lt;p&gt;The shift from AI as a generator to AI as an actor is real, and the architecture that supports it is becoming clearer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tools give AI a way to act on the world.
&lt;/li&gt;
&lt;li&gt;MCP servers give it a consistent, portable way to access those actions.
&lt;/li&gt;
&lt;li&gt;Skills give it the reasoning framework to act appropriately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each layer has its own design considerations, its own security surface, and its own place in a well-structured AI system. Understanding the distinctions is foundational to building systems that aren't just capable, but trustworthy. As the tooling matures, the teams that understand the layers will be the ones building systems they can actually reason about.&lt;/p&gt;

&lt;p&gt;For a closer look at the vulnerabilities that emerge in agentic AI, &lt;a href="https://auth0.com/blog/how-to-fix-five-critical-ai-agent-security-risks" rel="noopener noreferrer"&gt;five critical AI agent security risks&lt;/a&gt; is a practical next step. If you're working through identity for MCP servers specifically, the &lt;a href="https://auth0.com/ai/docs" rel="noopener noreferrer"&gt;Auth0 AI documentation&lt;/a&gt; covers authentication, delegation, and human-in-the-loop authorization patterns.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>llm</category>
    </item>
    <item>
      <title>AI Agents Have Two Souls. You Only Control One</title>
      <dc:creator>Andrea Chiarelli</dc:creator>
      <pubDate>Fri, 08 May 2026 16:04:16 +0000</pubDate>
      <link>https://dev.to/auth0/ai-agents-have-two-souls-you-only-control-one-3bm4</link>
      <guid>https://dev.to/auth0/ai-agents-have-two-souls-you-only-control-one-3bm4</guid>
      <description>&lt;p&gt;Everyone seems to be building AI agents now. But ask ten developers what an AI agent actually is, and you'll get ten different answers. Some say it is any LLM with tool access. Others define it by the ability to autonomously take actions in the world. A few will point at an existing chatbot and call it an agent.&lt;/p&gt;

&lt;p&gt;This definitional vagueness is not just an academic problem. It leads to a security problem. How can you protect a system you cannot describe precisely?&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking for an AI Agent Definition
&lt;/h2&gt;

&lt;p&gt;Beyond the generic definitions that emphasize the level of autonomy in making decisions, I'd like to point out a slightly more technical one that I prefer. It comes from &lt;a href="https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ai-agents/#what-is-an-ai-agent" rel="noopener noreferrer"&gt;Microsoft&lt;/a&gt; and seems to be quite consistent with the &lt;a href="https://genai.owasp.org/glossary/" rel="noopener noreferrer"&gt;OWASP definition&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;"&lt;em&gt;An AI agent is a flexible software program that uses generative AI models to interpret inputs, [...] reason through problems, and decide on the most appropriate actions. [...] Agents are built on five core components:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;Generative AI model&lt;/em&gt;&lt;/strong&gt; &lt;em&gt;serves as the agent's reasoning engine. It processes instructions, integrates tool calls, and generates outputs, either as messages to other agents or as actionable results.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;Instructions&lt;/em&gt;&lt;/strong&gt; &lt;em&gt;define the scope, boundaries, and behavioral guidelines for the agent. Clear instructions prevent scope creep and ensure the agent adheres to business rules.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;Retrieval&lt;/em&gt;&lt;/strong&gt; &lt;em&gt;provides the grounding data and context required for accurate responses. Access to relevant, high-quality data is critical for reducing hallucinations and ensuring relevance.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;Actions&lt;/em&gt;&lt;/strong&gt; &lt;em&gt;are the functions, APIs, or systems the agent uses to perform tasks. Tools transform the agent from a passive information retriever into an active participant in business processes.&lt;/em&gt; &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;Memory&lt;/em&gt;&lt;/strong&gt; &lt;em&gt;stores conversation history and state. Memory ensures continuity across interactions, allowing the agent to handle multi-turn conversations and long-running tasks effectively.&lt;/em&gt;"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agents differ from traditional applications, which are based on fixed rules. By dynamically orchestrating workflows according to real-time context, agents gain adaptability that allows them to manage ambiguity and complexity beyond the capability of traditional software.&lt;/p&gt;

&lt;p&gt;We can visualize this definition into the following diagram:&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.ctfassets.net%2F23aumh6u8s0i%2FuKLVsROcv4NHKZZ6QvuTA%2F9036537f8d4d4dca7deee1e7622eac84%2FAIAgentDefinitionDiagram.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%2Fimages.ctfassets.net%2F23aumh6u8s0i%2FuKLVsROcv4NHKZZ6QvuTA%2F9036537f8d4d4dca7deee1e7622eac84%2FAIAgentDefinitionDiagram.png" alt="Diagram of an AI agent architecture showing the deterministic Agent Core, Tools, and the probabilistic LLM reasoning engine." width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;br&gt;
Aside from input, output, and external resources, the heart of an AI agent is a collection of instructions, the LLM, and the Agent Core.&lt;/p&gt;

&lt;p&gt;The LLM is, as we know, the brain of the agent, the component that performs the necessary reasoning and makes decisions. The Agent Core is the software component that orchestrates the interactions between the LLM and the rest of the world. The Agent Core is the code in your Python, JavaScript, C#, etc. application that interacts with the LLM.&lt;/p&gt;

&lt;p&gt;Within the Agent Core, I highlighted two components: the Agent Control and the Tools. The Agent Control is the heart of the agent, the part that coordinates all the interactions between the LLM and the external world. The Tools component represents all the functionality made available to the LLM, such as calculation functions, file system access, etc. This component is also the interface to external tools and resources. The combination of the Agent Control and the Tools components builds the Agent Core: just plain old-style code, with no &lt;em&gt;intelligent&lt;/em&gt; functionality.&lt;/p&gt;

&lt;p&gt;Looking at the diagram, we notice two interesting things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The two true components of an AI agent are the Agent Core, which is deterministic, and the LLM, which is not deterministic.
&lt;/li&gt;
&lt;li&gt;The Agent Core is the only component that interacts with the LLM. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These two simple observations are fundamental to understanding the nature of an AI agent and how we can secure it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Two Souls of an AI Agent
&lt;/h2&gt;

&lt;p&gt;The first observation highlights that the two core components of an AI agent are a traditional deterministic application and an LLM, which is not deterministic.&lt;/p&gt;

&lt;p&gt;The Agent Core sends the input to the LLM, provides functionalities to it, processes the output, etc. by running deterministic code. You can analyze it, test it, you know how it works and you know that for a given input you will always get the same output.&lt;/p&gt;

&lt;p&gt;The LLM model has a different nature: it is not deterministic. Given the same input on two different occasions, a generative AI model may produce different outputs. It can reason in unexpected directions, interpret ambiguous instructions in ways you did not anticipate, and combine information from its context in ways that surprise even the people who built it. This is at the same time the power and the problem with LLMs.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Saying that LLMs &lt;strong&gt;are not deterministic is not the same as saying it is non-deterministic&lt;/strong&gt;. While commonly LLMs are considered to be non-deterministic, this is not correct in computational terms. See &lt;a href="https://theturingmachine.net/generative-ai-and-non-determinism" rel="noopener noreferrer"&gt;this article to learn why LLMs are not non-deterministic&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, in the architecture of an AI agent, we can identify what I call two souls: a &lt;strong&gt;deterministic soul&lt;/strong&gt; (the Agent Core) and a &lt;strong&gt;probabilistic soul&lt;/strong&gt; (the LLM).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you are a philosophy enthusiast, you might notice a certain reference to the dualities of the human soul: from &lt;a href="https://en.wikipedia.org/wiki/Phaedrus_(dialogue)#Chariot_allegory" rel="noopener noreferrer"&gt;Plato's myth of the winged chariot&lt;/a&gt; to &lt;a href="https://en.wikipedia.org/wiki/Augustine_of_Hippo" rel="noopener noreferrer"&gt;St. Augustine&lt;/a&gt;'s two wills to Nietzsche's distinction between the &lt;a href="https://en.wikipedia.org/wiki/Apollonian_and_Dionysian" rel="noopener noreferrer"&gt;Apollonian and Dionysian concepts&lt;/a&gt;.&lt;br&gt;&lt;br&gt;&lt;br&gt;
The tension is the same: one controlled, one wild. And the wild one gets all the attention.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Traditional software security is built almost entirely on the assumption of determinism. You know what inputs are valid, you know what outputs to expect, and you can test edge cases exhaustively. &lt;em&gt;AI agents shatter this assumption&lt;/em&gt;. The probabilistic soul introduces a category of behavior that no test suite can fully cover.&lt;/p&gt;

&lt;p&gt;The implication for security follows directly: you cannot secure the model itself. What you can do is &lt;strong&gt;architect the deterministic soul to constrain what the probabilistic soul can reach&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Laws of AI Security Applied
&lt;/h2&gt;

&lt;p&gt;Some time ago, I wrote an &lt;a href="https://auth0.com/blog/three-laws-ai-security/" rel="noopener noreferrer"&gt;article about the three laws of AI security&lt;/a&gt;. Paraphrasing Asimov's three laws of robotics, I defined similar laws to control the less deterministic part of AI. Similar to what I have analyzed in this article, I observed that the fundamental problem in building secure AI-powered applications is &lt;a href="https://auth0.com/blog/three-laws-ai-security/#The-Control-Problem-with-AI" rel="noopener noreferrer"&gt;the loss of that control&lt;/a&gt; we have become accustomed to with deterministic software.&lt;/p&gt;

&lt;p&gt;Based on the discussion we have had so far, we say that in the architecture of an AI agent there is a component that makes decisions and is not deterministic (LLM) and one that executes orders in a deterministic way (Agent Core).&lt;/p&gt;

&lt;p&gt;Paradoxically, the decision-making part is beyond our control: it is not deterministic, we have no tools to predict with certainty whether it will make the decision we expect or not.&lt;/p&gt;

&lt;p&gt;But in this scenario we forget one important thing: &lt;em&gt;it is not only the LLM that makes decisions&lt;/em&gt;. The Agent Core can make decisions as well. And that is not all. Our earlier second observation tells us that the Agent Core is the only component that interacts with the LLM. All the input coming from the user, other agents, external tools, and resources is filtered by the Agent Core before going to the LLM. All the output going to the users, other agents, external tools and resources comes through the Agent Core. &lt;strong&gt;The LLM cannot directly interact with the external world&lt;/strong&gt;. That is a great thing in terms of security!&lt;/p&gt;

&lt;p&gt;Let’s see how to apply each law to the soul-based architecture of an AI agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Data Control Law
&lt;/h2&gt;

&lt;p&gt;The first law is about gaining control over data. It states:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;An AI agent must safeguard all data entrusted to it and shall not, through action or inaction, allow this data to be exposed to any unauthorized user.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Translating this law in terms of the AI agent architecture, we can say that, when acting on behalf of a user, &lt;strong&gt;the probabilistic soul must never access data that the user is not authorized to access&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To implement this law, make sure your Agent Core has control over any data going to the LLM and to the user/other agents. Make sure that private data remains private. Filter data before sending it to the LLM, the user, or other agents. Apply access control according to your use case.&lt;/p&gt;

&lt;p&gt;A typical example of the need for data control is in retrieval algorithms implementation, such as in RAG systems. You specialize your agent’s knowledge with an external source of data, such as a vector database, and want to prevent the user from accessing data they are not authorized to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The deterministic soul of the agent has the responsibility to filter the data before passing it to the LLM&lt;/strong&gt;. Without this filter, a user asking “summarize my documents” could inadvertently receive documents belonging to other users: a data leak the LLM itself would never catch.&lt;/p&gt;

&lt;p&gt;Take a look at the following blog posts to see how to implement the data control law for RAG systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://auth0.com/blog/building-a-secure-rag-with-python-langchain-and-openfga/" rel="noopener noreferrer"&gt;Building a Secure RAG with Python, LangChain, and OpenFGA&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://auth0.com/blog/genai-langchain4j-java-openfga-rag/" rel="noopener noreferrer"&gt;Secure Java AI Agents: Authorization for RAG Using LangChain4j and Auth0 FGA&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://auth0.com/blog/secure-dotnet-rag-system-with-auth0-fga/" rel="noopener noreferrer"&gt;Secure a .NET RAG System with Auth0 FGA&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://auth0.com/blog/genai-llamaindex-js-fga/" rel="noopener noreferrer"&gt;Build a Secure RAG Agent Using LlamaIndex and Auth0 FGA on Node.js&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Command Control Law
&lt;/h2&gt;

&lt;p&gt;The second law focuses on controlling the command flow and making sure that an AI agent does what it is called to do. Nothing more. The law says:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;An AI agent must execute its functions within the narrowest scope of authority necessary. It shall not escalate its own privileges, share secrets, or obey any order that would conflict with the First Law.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let’s translate this law in terms of the agent architecture.&lt;/p&gt;

&lt;p&gt;If you want to prevent an agent from sharing secrets, do not share secrets with the agent. Or better, &lt;strong&gt;the probabilistic soul (LLM) must never access secrets or tokens&lt;/strong&gt;. The deterministic soul can manage tokens, but you must take steps to minimize the likelihood of these falling into the wrong hands. For example, you should use tokens with a short life, but this requires frequent renewals with &lt;a href="https://auth0.com/blog/refresh-tokens-what-are-they-and-when-to-use-them/" rel="noopener noreferrer"&gt;refresh tokens&lt;/a&gt;. If your agent interacts with multiple third-party services (e.g., Gmail, Slack, Stripe, etc.), do not store long-lived tokens locally for each third-party service. Use a &lt;a href="https://auth0.com/features/token-vault" rel="noopener noreferrer"&gt;token vault&lt;/a&gt; instead.&lt;/p&gt;

&lt;p&gt;Check out these articles to learn what is the best approach to protect the tokens used by your agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://auth0.com/blog/ai-assistant-langgraph-nextjs-slack-github/" rel="noopener noreferrer"&gt;Build an AI Assistant with LangGraph, Next.js, and Auth0 Connected Accounts&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://auth0.com/blog/third-party-tool-calling-llamaindex-auth0-python/" rel="noopener noreferrer"&gt;Secure Third-Party Tool Calling in LlamaIndex Using Auth0&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://auth0.com/blog/microsoft-agent-framework-python-auth0-token-vault/" rel="noopener noreferrer"&gt;MS Agent Framework and Python: Use the Auth0 Token Vault to Call Third-Party APIs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To give your agent only the permissions it needs, use scopes in your access tokens and &lt;a href="https://auth0.com/blog/oauth2-access-tokens-and-principle-of-least-privilege/" rel="noopener noreferrer"&gt;apply the principle of least privilege&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But threats to the second law do not just come from token and permission management. These can arise from the input received by the agent: consider prompt injection, which could fool the probabilistic soul of your agent and bypass certain constraints.&lt;/p&gt;

&lt;p&gt;The output generated by the LLM can also pose a threat, especially when it is intended to invoke a tool.&lt;/p&gt;

&lt;p&gt;In other words, &lt;strong&gt;the deterministic soul is responsible for sanitizing LLM’s input and output&lt;/strong&gt;. To learn some techniques for preventing prompt injections and handling improper output, read the following articles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://auth0.com/blog/prompt-injection-ai-browser/#Indirect-Prompt-Injections" rel="noopener noreferrer"&gt;Hiding Prompts in Plain Sight: A New AI Security Risk&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://auth0.com/blog/owasp-llm05-improper-output-handling/" rel="noopener noreferrer"&gt;Trusting AI Output? Why Improper Output Handling is the New XSS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Decision Control Law
&lt;/h2&gt;

&lt;p&gt;The third law is about decision control and says:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;An AI agent must cede final authority for any critical or irreversible decision to its human operator, as long as this deference does not conflict with the First or Second Law.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In traditional software, the paths between a given input and its output are somehow predetermined. In an agent, you have an unlimited variety of combinations between inputs and outputs, determined almost entirely by the decisions made by the LLM in interpreting the prompt.&lt;/p&gt;

&lt;p&gt;In the AI agent architecture, we are delegating to the probabilistic soul the task of interpreting the input and determining what tools to use to achieve a given goal. This gives the agent enormous flexibility and, at the same time, a great responsibility.&lt;/p&gt;

&lt;p&gt;To implement the decision control law, you should limit the responsibility of your agent by identifying what are the most critical actions that it can do on your behalf and ask confirmation for executing those actions. &lt;strong&gt;The deterministic soul is responsible for involving the user in critical decisions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Depending on the type of agent you are building, these confirmation requests can be interactive or asynchronous. Think of interactive requests like those made by Claude Code or GitHub Copilot when they ask for permission to write to a folder or modify your code. Asynchronous permission requests are those that an unattended agent can send you via email or push notifications.&lt;/p&gt;

&lt;p&gt;Whatever your case, engaging the user is crucial to preventing potentially irreparable damage.&lt;/p&gt;

&lt;p&gt;Here are a few examples of how you can deal with asynchronous authorization using Auth0:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://auth0.com/blog/mitigate-excessive-agency-ai-agents/" rel="noopener noreferrer"&gt;Securing AI Agents: Mitigate Excessive Agency with Zero Trust Security&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://auth0.com/blog/secure-human-in-the-loop-interactions-for-ai-agents/" rel="noopener noreferrer"&gt;Secure “Human in the Loop” Interactions for AI Agents&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://auth0.com/blog/async-ciba-python-langgraph-auth0/" rel="noopener noreferrer"&gt;Implementing Asynchronous Human-in-the-Loop Authorization in Python with LangGraph and Auth0&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://auth0.com/blog/use-ciba-authentication-with-auth0-dotnet/" rel="noopener noreferrer"&gt;Use CIBA Authentication with Auth0 and .NET&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Your Takeaways for Securing AI Agents
&lt;/h2&gt;

&lt;p&gt;The “two souls” model gives you a concrete mental model for AI agent security. The security insight here is counterintuitive: the component you control least (the LLM) is not where you focus your security effort. &lt;strong&gt;Security must focus on architecting the deterministic soul to constrain the probabilistic one&lt;/strong&gt;. This perspective allows you to apply the &lt;a href="https://auth0.com/blog/three-laws-ai-security/" rel="noopener noreferrer"&gt;Three Laws of AI Security&lt;/a&gt; by enforcing the Agent Core's responsibility for filtering data, sanitizing inputs/outputs, and managing critical human-in-the-loop decisions.&lt;/p&gt;

&lt;p&gt;Here are the main takeaways that I want to highlight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Security is in the code:&lt;/strong&gt; You cannot secure the LLM directly; secure the deterministic Agent Core that wraps and controls it.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constraint is control:&lt;/strong&gt; Use the Agent Core to strictly enforce boundaries on the LLM's access to data, secrets, tokens, and external actions.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human override:&lt;/strong&gt; Implement "Human in the Loop" protocols in the deterministic soul for all critical or irreversible decisions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Implementing all of this from scratch can be challenging and risky. &lt;a href="https://auth0.com/ai" rel="noopener noreferrer"&gt;Auth0 for AI Agents&lt;/a&gt; helps you handle the deterministic security layer for you (token management, access control, and human-in-the-loop flows) so you can focus on what your agent actually does.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>architecture</category>
      <category>llm</category>
    </item>
    <item>
      <title>Manage Your Auth0 Tenants Faster with the Gemini CLI Extension</title>
      <dc:creator>Jessica Temporal</dc:creator>
      <pubDate>Wed, 29 Apr 2026 21:01:35 +0000</pubDate>
      <link>https://dev.to/auth0/manage-your-auth0-tenants-faster-with-the-gemini-cli-extension-2dh8</link>
      <guid>https://dev.to/auth0/manage-your-auth0-tenants-faster-with-the-gemini-cli-extension-2dh8</guid>
      <description>&lt;p&gt;Stop switching between your browser and terminal to manage identity. In this video, I'll show you how to integrate the &lt;em&gt;Auth0 MCP Server&lt;/em&gt; directly into your &lt;em&gt;Gemini CLI&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Learn how to leverage AI to query your applications, initialize tenants, and manage APIs using natural language commands without leaving your development environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You'll Learn
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;How to find the Auth0 extension on the official Gemini page.&lt;/li&gt;
&lt;li&gt;The one-command installation process for the Auth0 MCP server.&lt;/li&gt;
&lt;li&gt;Authenticating and initializing your Auth0 environment via CLI.&lt;/li&gt;
&lt;li&gt;Using natural language to list applications and create new API resources.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/auth0/auth0-mcp-server" rel="noopener noreferrer"&gt;💻 Auth0 MCP Server GitHub Repo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://auth0.com/docs/get-started/auth0-mcp-server" rel="noopener noreferrer"&gt;🛠️ Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://auth0.com/blog/auth0-mcp-server-in-gemini-cli-extensions" rel="noopener noreferrer"&gt;📝 Auth0 Blog Post&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>tooling</category>
      <category>gemini</category>
    </item>
    <item>
      <title>Things Developers Get Wrong About the Backend for Frontend Pattern</title>
      <dc:creator>Andrea Chiarelli</dc:creator>
      <pubDate>Fri, 24 Apr 2026 15:51:18 +0000</pubDate>
      <link>https://dev.to/auth0/things-developers-get-wrong-about-the-backend-for-frontend-pattern-10n1</link>
      <guid>https://dev.to/auth0/things-developers-get-wrong-about-the-backend-for-frontend-pattern-10n1</guid>
      <description>&lt;p&gt;Since I published my &lt;a href="https://auth0.com/blog/the-backend-for-frontend-pattern-bff" rel="noopener noreferrer"&gt;overview of the Backend for Frontend (BFF) pattern&lt;/a&gt;, the questions I've received fall into surprisingly consistent patterns. The same misunderstandings come up again and again, from developers who genuinely want to build secure apps.&lt;/p&gt;

&lt;p&gt;Most of these misconceptions aren't just academic. They lead teams to ship apps with real security gaps while believing they've done the right thing. Let me address the ones I see most often.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why PKCE Isn't a Replacement for BFF
&lt;/h2&gt;

&lt;p&gt;This is the one I encounter most, and it's the most consequential.&lt;/p&gt;

&lt;p&gt;The OAuth working group deprecated the &lt;a href="https://oauth.net/2/grant-types/implicit/" rel="noopener noreferrer"&gt;Implicit Grant&lt;/a&gt; for SPAs and recommended &lt;a href="https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow-with-pkce" rel="noopener noreferrer"&gt;Authorization Code with PKCE&lt;/a&gt; as the replacement. That guidance is correct. In addition, OAuth 2.1 recommends PKCE for any client, not just SPAs. Somehow, many developers concluded that PKCE also addresses token storage security. It doesn't.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://auth0.com/blog/demystifying-oauth-security-state-vs-nonce-vs-pkce/" rel="noopener noreferrer"&gt;PKCE (Proof Key for Code Exchange)&lt;/a&gt; protects the authorization code in transit. It prevents an attacker who intercepts the authorization code from exchanging it for tokens. Valuable, but it solves only one step of the OAuth flow.&lt;/p&gt;

&lt;p&gt;Once your app receives tokens, PKCE has done its job. It has nothing to say about where those tokens live in the browser or what happens if a &lt;a href="https://auth0.com/blog/cross-site-scripting-xss/" rel="noopener noreferrer"&gt;Cross-Site Scripting (XSS)&lt;/a&gt; attack runs in your app's context. Tokens in &lt;code&gt;localStorage&lt;/code&gt;, &lt;code&gt;sessionStorage&lt;/code&gt;, or even JavaScript memory are all reachable by malicious scripts.&lt;/p&gt;

&lt;p&gt;BFF solves a different problem: &lt;strong&gt;it keeps tokens out of the browser entirely&lt;/strong&gt;. The BFF exchanges the authorization code for tokens and stores them server-side. The browser gets an &lt;code&gt;HttpOnly&lt;/code&gt; session cookie. An XSS attack running in that browser can't steal what isn't there.&lt;/p&gt;

&lt;p&gt;PKCE and BFF are complementary, not alternatives. If your &lt;a href="https://owasp.org/www-community/Threat_Modeling" rel="noopener noreferrer"&gt;threat model&lt;/a&gt; includes XSS (and for most apps, it should), PKCE alone isn't enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  BFF Is Not Just a Proxy
&lt;/h2&gt;

&lt;p&gt;In a couple of cases, I've reviewed architectures labeled as "BFF" that were actually reverse proxies forwarding requests (and tokens) to a backend. That's not a BFF.&lt;/p&gt;

&lt;p&gt;The defining characteristic of a Backend for Frontend is that it acts as a &lt;strong&gt;confidential OAuth client&lt;/strong&gt;. It holds a client secret. It handles the full OAuth flow, including token exchange. Most critically, &lt;strong&gt;tokens never leave the server&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A reverse proxy that forwards an Authorization header containing a bearer token is not a BFF. The token is still accessible to the browser. You've added a network hop without the security benefit.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps" rel="noopener noreferrer"&gt;IETF OAuth 2.0 for Browser-Based Apps BCP&lt;/a&gt; actually distinguishes between a &lt;em&gt;Token-Mediating Backend&lt;/em&gt; (a backend that obtains tokens and then forwards them to the frontend) and a proper BFF (where tokens are never passed to the frontend at all). These are different patterns with different security properties.&lt;/p&gt;

&lt;p&gt;If your backend is passing tokens to the browser in any form, you haven't implemented BFF. You've implemented token mediation, which is better than nothing, but it's not the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  No, Cookies Are Not Less Secure Than Tokens
&lt;/h2&gt;

&lt;p&gt;This one has roots in a legitimate historical concern. Cookies have a complicated reputation: they've been misused, and &lt;a href="https://auth0.com/blog/cross-site-request-forgery-csrf/" rel="noopener noreferrer"&gt;Cross-Site Request Forgery (CSRF)&lt;/a&gt; attacks were a real problem before &lt;code&gt;SameSite&lt;/code&gt; became standard. Some of the "don't use cookies" instinct also came from REST API orthodoxy, where stateless communication was treated as a design virtue.&lt;/p&gt;

&lt;p&gt;But here's what modern reality looks like: an &lt;code&gt;HttpOnly&lt;/code&gt; session cookie cannot be read by JavaScript. That means XSS attacks can't steal it directly. A JWT in &lt;code&gt;localStorage&lt;/code&gt; can be read by any script running on your page.&lt;/p&gt;

&lt;p&gt;The attack surface isn't symmetric. A CSRF attack using an &lt;code&gt;HttpOnly&lt;/code&gt; cookie requires the attacker to trick a user into making a specific authenticated request from another origin. An XSS attack that steals a &lt;code&gt;localStorage&lt;/code&gt; token gives the attacker full, direct access to call any API as that user, from anywhere, without any user interaction required.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;SameSite=Strict&lt;/code&gt; or &lt;code&gt;SameSite=Lax&lt;/code&gt;, CSRF attacks against &lt;code&gt;HttpOnly&lt;/code&gt; cookies are already difficult in most real-world scenarios. Add explicit CSRF tokens and they become practically infeasible.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;HttpOnly&lt;/code&gt; cookies aren't immune to CSRF, but the attack surface is far smaller than XSS-based token theft. You're trading one attack vector (XSS-based token theft) for a different, more constrained one (CSRF). That trade is almost always worth making.&lt;/p&gt;

&lt;h2&gt;
  
  
  BFF Does Not Solve All Your Browser Auth Security Problems
&lt;/h2&gt;

&lt;p&gt;BFF shifts the security boundary. It doesn't eliminate it.&lt;/p&gt;

&lt;p&gt;When you adopt BFF, you trade the token theft problem (XSS can steal tokens from browser storage) for the session management problem (your BFF now manages sessions and those need to be secured properly). This is a good trade in most cases, but it comes with responsibilities that BFF doesn't automatically fulfill.&lt;/p&gt;

&lt;p&gt;Things BFF does not handle on its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CSRF protection.&lt;/strong&gt; Your BFF uses cookies, which means state-changing requests need CSRF protection. &lt;code&gt;SameSite&lt;/code&gt; cookies help significantly, but this is still your responsibility to configure correctly.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session invalidation.&lt;/strong&gt; When a user logs out, you need to revoke the session server-side, not just clear the cookie client-side. If you don't, stolen session cookies remain valid until natural expiry.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure cookie configuration.&lt;/strong&gt; &lt;code&gt;Secure&lt;/code&gt;, &lt;code&gt;HttpOnly&lt;/code&gt;, and the right &lt;code&gt;SameSite&lt;/code&gt; setting are all required. Missing any of them weakens the pattern's security properties.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization checks in your API.&lt;/strong&gt; BFF protects the token in transit. It doesn't automatically secure your API endpoints. You still need proper authorization logic on the backend.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don’t implement BFF and then relax your overall security posture, assuming the pattern covers everything. It doesn't. Treat it as one layer in a defense-in-depth approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  You Don’t Need to Rewrite Your Entire Application
&lt;/h2&gt;

&lt;p&gt;The assumption that teams must rewrite the entire application prevents them from adopting BFF even when they should.&lt;/p&gt;

&lt;p&gt;The full vision of BFF, as &lt;a href="https://samnewman.io/patterns/architectural/bff/" rel="noopener noreferrer"&gt;Sam Newman originally described it&lt;/a&gt;, is a server tailored to the specific needs of one frontend. That can mean a significant rearchitecting effort. But you don't have to implement the full pattern at once to get the security benefits.&lt;/p&gt;

&lt;p&gt;In practice, many teams introduce BFF incrementally. The most common path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add a lightweight backend (Node.js, Next.js server components, ASP.NET Core, Python, or whatever fits your stack) that handles the OAuth flow.
&lt;/li&gt;
&lt;li&gt;The BFF exchanges authorization codes for tokens, stores them server-side, and issues session cookies to the browser.
&lt;/li&gt;
&lt;li&gt;Your existing frontend continues making API calls, now using session cookies instead of bearer tokens.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your existing backend APIs often don't change at all. You're inserting the BFF as the authentication layer, not replacing your entire architecture.&lt;/p&gt;

&lt;p&gt;The incremental path is real, and the auth-focused version of BFF is where most of the security value lives anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start With the Threat Model
&lt;/h2&gt;

&lt;p&gt;Before deciding whether to implement BFF, be honest about your app's threat model.&lt;/p&gt;

&lt;p&gt;If your application handles sensitive data, if XSS is a credible risk (and it usually is, especially for apps loading third-party scripts or rendering user-generated content), or if you operate in a regulated industry, BFF is the right choice. The complexity cost is manageable and the security benefit is concrete.&lt;/p&gt;

&lt;p&gt;If you're building a simple public-facing app with no sensitive user data and strong existing XSS defenses, a well-implemented SPA with PKCE and in-memory token storage may be acceptable.&lt;/p&gt;

&lt;p&gt;The BFF pattern exists because the browser is a hostile environment for tokens. If that threat is real for your application (and you're the best judge of that) BFF addresses it in ways that PKCE and in-browser token handling simply can't.&lt;/p&gt;

</description>
      <category>oauth</category>
      <category>bff</category>
      <category>security</category>
      <category>designpatterns</category>
    </item>
    <item>
      <title>From Backend Engineer to Building AI Infrastructure at a Startup</title>
      <dc:creator>Carla Urrea Stabile</dc:creator>
      <pubDate>Wed, 22 Apr 2026 16:05:00 +0000</pubDate>
      <link>https://dev.to/auth0/from-backend-engineer-to-building-ai-infrastructure-at-a-startup-1cco</link>
      <guid>https://dev.to/auth0/from-backend-engineer-to-building-ai-infrastructure-at-a-startup-1cco</guid>
      <description>&lt;p&gt;What does it look like to go from a six-person startup team to running the infrastructure behind 1,000+ AI models?&lt;/p&gt;

&lt;p&gt;Matteo and I have known each other for over 10 years, but I realized I'd never actually asked him about his engineering journey in depth. I knew he was doing infrastructure work, but I didn't know the full story of how he got there. This conversation filled in a lot of gaps for me.&lt;/p&gt;

&lt;p&gt;In Episode 4 of Making Software, I talked to &lt;strong&gt;Matteo Ferrando&lt;/strong&gt;, Platform and Infra Engineer at &lt;a href="https://fal.ai" rel="noopener noreferrer"&gt;fal.ai&lt;/a&gt;, about exactly that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we covered
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The pivot that changed everything.&lt;/strong&gt; The company didn't start as an AI company. Matteo talks about what the original product was, how the pivot happened, and why letting go of something that's working is one of the hardest things you'll do at a startup as an engineer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;How a backend engineer ends up doing infra.&lt;/strong&gt; There was no "switch." Matteo explains the reality of early-stage startups where in the beginning, there's no database, no Kubernetes cluster, no nothing. You just build it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A decision-making framework worth stealing.&lt;/strong&gt; One-way doors vs. two-way doors. Simple concept, but it changes how you think about MVPs, technical debt, and when to actually invest in doing things "the right way."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Optimizing a routing layer from 100ms to 5ms.&lt;/strong&gt; The use case that forced this is wild. I'll let Matteo tell that story.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Do you still need computer science fundamentals?&lt;/strong&gt; We talked about AI coding tools, what they're great at, and a very specific failure mode that Matteo keeps seeing on his team.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A couple of things Matteo said that stuck with me
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"I'm not obsessed about technical debt like many engineers are. It's fine to have technical debt and we'll get to it."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This resonated a lot with me. I think a lot of us carry guilt about tech debt like it's something we did wrong. Matteo's framing is different: it's not a problem until it's actually a problem. That shift in perspective is freeing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"A mistake introduced by an AI that you prompted is still your mistake. You still have to understand what you're shipping."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This one hit. Especially right now, when so many of us are leaning on AI tools to write code faster. Faster is great, but Matteo makes a really compelling case for why "I didn't write it" is not an excuse. He shares a specific example from his team that I think every developer using AI tools needs to hear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Talking to Matteo reminded me that there's no clean, linear path in engineering. You don't go from "backend engineer" to "infra engineer" because you planned it. You get there because something needs to exist and you're the one who builds it. I think that's something a lot of us can relate to, especially if you've ever worked at a startup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Have you ever had to build something way outside your comfort zone because no one else was going to?&lt;/strong&gt; Let me know in the comments!&lt;/p&gt;

&lt;h2&gt;
  
  
  Listen to the full episode
&lt;/h2&gt;

&lt;p&gt;Available on &lt;a href="https://www.youtube.com/playlist?list=PLZ14qQz3cfJKRDmX3yasmbwoC4kipeQfu" rel="noopener noreferrer"&gt;&lt;strong&gt;YouTube&lt;/strong&gt;&lt;/a&gt;, &lt;a href="https://podcasts.apple.com/us/podcast/making-software/id1872107131" rel="noopener noreferrer"&gt;&lt;strong&gt;Apple Podcasts&lt;/strong&gt;&lt;/a&gt;, and &lt;a href="https://open.spotify.com/show/6J856S2fijMvP3rzFkRnBi" rel="noopener noreferrer"&gt;&lt;strong&gt;Spotify&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thanks for reading! 👋&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>startup</category>
      <category>career</category>
    </item>
    <item>
      <title>How to Use Auth0 Agent Skills in Claude Code &amp; AI Coding Assistants</title>
      <dc:creator>Jessica Temporal</dc:creator>
      <pubDate>Mon, 06 Apr 2026 18:27:59 +0000</pubDate>
      <link>https://dev.to/auth0/how-to-use-auth0-agent-skills-in-claude-code-ai-coding-assistants-56e5</link>
      <guid>https://dev.to/auth0/how-to-use-auth0-agent-skills-in-claude-code-ai-coding-assistants-56e5</guid>
      <description>&lt;p&gt;Tired of your AI coding assistant hallucinating APIs or writing insecure auth patterns? In this video, I'll show you how to use Auth0 Agent Skills to teach your AI assistant (like Claude Code or GitHub Copilot) how to implement Auth0 correctly. Say goodbye to XSS vulnerabilities and manual JWT decoding—ship production-ready, secure authentication from the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You'll Learn
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Why standard AI coding assistants struggle with secure authentication.&lt;/li&gt;
&lt;li&gt;How to install Auth0 Agent Skills via NPX or Claude Code plugins.&lt;/li&gt;
&lt;li&gt;The difference between Core Skills and SDK Skills (React, Next.js, etc.).&lt;/li&gt;
&lt;li&gt;A side-by-side comparison of "hallucinated" code vs. secure Auth0 patterns.&lt;/li&gt;
&lt;li&gt;How to implement production-ready auth in Next.js using Agent Skills.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;p&gt;📝 &lt;a href="https://auth0.com/blog/auth0-agent-skills-ai-coding-assistants/" rel="noopener noreferrer"&gt;Read the full blog post&lt;/a&gt;&lt;br&gt;
🛠️ &lt;a href="https://auth0.com/docs/quickstart/agent-skills" rel="noopener noreferrer"&gt;Auth0 Documentation&lt;/a&gt;&lt;br&gt;
💻 &lt;a href="https://github.com/auth0/agent-skills" rel="noopener noreferrer"&gt;GitHub Repo&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>productivity</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Auth0 MCP Server Extension for Gemini CLI</title>
      <dc:creator>Jessica Temporal</dc:creator>
      <pubDate>Fri, 27 Mar 2026 11:00:00 +0000</pubDate>
      <link>https://dev.to/auth0/auth0-mcp-server-extension-for-gemini-cli-405m</link>
      <guid>https://dev.to/auth0/auth0-mcp-server-extension-for-gemini-cli-405m</guid>
      <description>&lt;p&gt;The Auth0 MCP Server is now listed on the official Gemini CLI extensions page. This means the Auth0 MCP Server is now directly installable through Gemini CLI with one command, allowing you to authenticate to Auth0 directly from your Gemini CLI session and load tenant information automatically.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/iiSuv8BnPC0"&gt;
  &lt;/iframe&gt;
 &lt;/p&gt;
&lt;h2&gt;
  
  
  What the Auth0 MCP Server Extension Provides
&lt;/h2&gt;

&lt;p&gt;The extension packages the Auth0 MCP Server for Gemini CLI and adds three integration layers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Discoverability&lt;/strong&gt;: Listed on &lt;a href="https://geminicli.com/extensions" rel="noopener noreferrer"&gt;geminicli.com/extensions&lt;/a&gt;, searchable by name, installable without manual configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication Commands&lt;/strong&gt;: Built-in slash commands for Auth0 tenant management:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/auth0:init&lt;/code&gt; - Device authorization flow with tenant selection&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/auth0:logout&lt;/code&gt; - Session termination&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/auth0:session&lt;/code&gt; - Current authentication status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Context Injection&lt;/strong&gt;: After authentication, Gemini gains your tenant information so the AI can query applications, APIs, connections, actions, and logs without requiring manual tenant specification in every prompt.&lt;/p&gt;
&lt;h2&gt;
  
  
  Installation and Setup
&lt;/h2&gt;

&lt;p&gt;Install the extension:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gemini extensions &lt;span class="nb"&gt;install &lt;/span&gt;https://github.com/auth0/auth0-mcp-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the command successfully finishes you should see a message stating &lt;code&gt;Extension “Auth0” installed successfully&lt;/code&gt;:&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.amazonaws.com%2Fuploads%2Farticles%2Ffzi13ha0j201bszply11.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.amazonaws.com%2Fuploads%2Farticles%2Ffzi13ha0j201bszply11.png" alt="Terminal showing the installation of the Auth0 MCP server as a Gemini CLI extension"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Initialize the Auth0 MCP Server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/auth0:init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure to allow the command to run when prompted. The server will run automatically.&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.amazonaws.com%2Fuploads%2Farticles%2Fr7q3o4dmnm02utqeu4ji.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.amazonaws.com%2Fuploads%2Farticles%2Fr7q3o4dmnm02utqeu4ji.png" alt="Gemini CLI terminal showing /auth0:init command with permission prompt to allow command execution"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You'll authenticate via &lt;a href="https://auth0.com/docs/get-started/authentication-and-authorization-flow/device-authorization-flow" rel="noopener noreferrer"&gt;device code flow&lt;/a&gt; to select your tenant:&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.amazonaws.com%2Fuploads%2Farticles%2Frnik2h6j061y0y9brksu.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.amazonaws.com%2Fuploads%2Farticles%2Frnik2h6j061y0y9brksu.png" alt="Auth0 device authorization screen displaying device code and instructions to complete authentication"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And confirm the permissions:&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.amazonaws.com%2Fuploads%2Farticles%2F0wx8dzfzrmbnntzeaj7b.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.amazonaws.com%2Fuploads%2Farticles%2F0wx8dzfzrmbnntzeaj7b.png" alt="Auth0 authorization screen showing requested permissions for Auth0 MCP Server extension"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once authenticated, you should see a message within Gemini saying the Auth0 MCP Server is configured and to restart Gemini CLI to see the changes:&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.amazonaws.com%2Fuploads%2Farticles%2F10jdvkxmdmkufvebcm26.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.amazonaws.com%2Fuploads%2Farticles%2F10jdvkxmdmkufvebcm26.png" alt="Gemini CLI terminal displaying successful Auth0 MCP Server initialization with tenant connection confirmed"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Refresh the MCP server list with the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/mcp refresh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gemini now has Auth0 context. Ask: "show me my applications" and the AI will receive the structured information about your applications, which Gemini CLI will display as a structured tool call result:&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.amazonaws.com%2Fuploads%2Farticles%2Fwrab4gmufeqlx8rtwf8m.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.amazonaws.com%2Fuploads%2Farticles%2Fwrab4gmufeqlx8rtwf8m.png" alt="Gemini CLI tool call output showing structured JSON data of Auth0 applications from the MCP server"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And since Gemini now understands your tenant structure, existing configurations, and naming conventions, it can also show you the same information in a more readable format:&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.amazonaws.com%2Fuploads%2Farticles%2F7n6pl8wgu6ipiazokouq.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.amazonaws.com%2Fuploads%2Farticles%2F7n6pl8wgu6ipiazokouq.png" alt="Gemini CLI displaying formatted, human-readable list of Auth0 applications with names, types, and client IDs"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;Before this extension, using the Auth0 MCP Server with Gemini CLI required manual server configuration, environment variable setup, and custom initialization scripts. The extension collapses that into a single install command and three slash commands.&lt;/p&gt;

&lt;p&gt;More importantly: context persistence. Once authenticated, every Gemini session knows your Auth0 environment. You're not re-explaining your tenant structure or copy-pasting app IDs. The AI assistant operates with the same tenant awareness you have.&lt;/p&gt;

&lt;p&gt;This is the same Auth0 MCP Server that powers VS Code integrations, now packaged for Gemini CLI's extension model. Same capabilities, different CLI.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;The Auth0 MCP Server supports tenant management, application configuration, API setup, and log analysis. For implementation details and the full MCP Server feature set, &lt;a href="https://github.com/auth0/auth0-mcp-server?tab=readme-ov-file#%EF%B8%8F-supported-tools" rel="noopener noreferrer"&gt;see the list on GitHub here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The extension is available now at &lt;a href="https://geminicli.com/extensions" rel="noopener noreferrer"&gt;geminicli.com/extensions&lt;/a&gt;. Install, authenticate, and start managing Auth0 tenants through natural language.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>tutorial</category>
      <category>tooling</category>
    </item>
    <item>
      <title>The Future of Coding is Communication, Not Just Code</title>
      <dc:creator>Carla Urrea Stabile</dc:creator>
      <pubDate>Thu, 26 Mar 2026 14:31:00 +0000</pubDate>
      <link>https://dev.to/auth0/the-future-of-coding-is-communication-not-just-code-328p</link>
      <guid>https://dev.to/auth0/the-future-of-coding-is-communication-not-just-code-328p</guid>
      <description>&lt;p&gt;We recently had a great conversation on the Making Software podcast with Bobby Tierney. Bobby is a Principal Architect at Okta and Auth0 who focuses on agentic security, AI governance, and the Model Context Protocol (MCP).&lt;br&gt;
I have been feeling some "AI FOMO" lately because the industry is moving so quickly. Talking to Bobby helped me bridge the gap between the "vibes" of modern AI coding and the security standards we need as professional engineers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vibe Coding vs. Mission Critical Production:&lt;/strong&gt; Bobby shared his perspective on where "vibe coding" fits into the software development life cycle and when it becomes a risky proposition for an enterprise. He explained why prototypes are a great way to explore a solution space even if you eventually throw them away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managing "YOLO Mode" with Sandboxing:&lt;/strong&gt; We discussed the "YOLO mode" found in many AI tools and how engineers can use progressive security configurations to keep things under control. 
-** The Shift to Spec-Driven Development:** Bobby talked about moving away from probabilistic guesswork and toward "spec coding". He explained how to use tools like "skills" or "slash commands" to align an AI with your team's specific DNA and ways of working.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "Confused Deputy" Problem:&lt;/strong&gt; We touched on the security risks of AI agents and why you shouldn't just give them machine credentials to act on a user's behalf. Bobby highlighted the importance of finding the right intersection between what a user is allowed to do and what the AI is permitted to do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Changing Role of the Engineer:&lt;/strong&gt; We explored how AI is shrinking the development life cycle and why communication might be the most valuable skill for a developer in the future. Bobby also shared how designers and PMs are starting to use these tools to contribute directly to codebases.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Bobby reminded me that while tools are changing, the core of engineering is still about requirements and making good decisions. &lt;/p&gt;

&lt;p&gt;The age of AI is not about replacing engineers. It is about helping them. By being good communicators, teachers, and security-minded builders, we can use these amazing tools to make better, safer software.&lt;/p&gt;

&lt;p&gt;What is your take on YOLO mode coding? Let me know in the comments! 🌱&lt;br&gt;
If you want to hear more, check out the full episode: &lt;a href="https://listen.casted.us/public/49/Making-Software-2b1cff7b" rel="noopener noreferrer"&gt;https://listen.casted.us/public/49/Making-Software-2b1cff7b&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>security</category>
      <category>vibecoding</category>
    </item>
  </channel>
</rss>
