Your auth system was built for humans. AI agents are not humans. Something has to give.
In this article, you will learn:
- Why the traditional user auth model breaks down for AI agents
- What non-human identities are and why they need their own auth layer
- The difference between global and org-scoped machine identity
- How the OAuth 2.0 client credentials flow works for non-human callers
- How to use Kinde M2M applications to authenticate AI agents properly
- How to scope agent access to a specific tenant using org-scoped M2M apps
- The biggest security mistakes developers make when adding agent auth and how to avoid them
Let's dive in!
The Problem Nobody Warned You About
You shipped auth. Users can sign up, sign in, and get sessions. You followed the OAuth flow, you set up JWTs, you protected your routes. Everything works.
Then someone on your team says: "We're adding an AI agent that acts on behalf of users."
And suddenly you have a question your auth system was never designed to answer: how do you authenticate something that has no browser, no session, no email address, and no ability to type in a password?
The lazy answer is to create a fake user account for the agent. Give it an email like agent@yourapp.com, hand it a password, and call it done. It works — until it becomes a security nightmare. You have an account with full user-level access, no MFA, credentials that never rotate, and no audit trail that distinguishes what the agent did from what a real user did. That is not an auth model. That is a ticking clock.
The real answer is that AI agents are not users. They are non-human identities. They need their own authentication layer, their own token type, their own scopes, and their own access boundaries. And in 2026, with AI agents becoming a core part of SaaS products, getting this right is no longer optional.
Here is what that looks like in practice, and how Kinde's M2M application system handles it properly.
What Is a Non-Human Identity?
A non-human identity (NHI) is any software principal that needs to authenticate and call APIs without a human being directly involved in the auth flow. In the context of modern SaaS products, non-human identities include things like backend services, CI/CD pipelines, scheduled jobs, microservices, and — increasingly — AI agents.
What all of these have in common is that they cannot participate in an interactive auth flow. There is no redirect. There is no browser. There is no one sitting at a keyboard to enter a verification code. A human auth flow is fundamentally the wrong tool for the job.
The problem is that most auth systems — and most developer mental models — were built around human identities first. Non-human auth was bolted on later, often improperly, as an afterthought.
Why the Fake User Approach Fails
Before getting into the right solution, it is worth being specific about why the fake user workaround is dangerous. These are not theoretical risks.
Over-permissioned by default. A user account typically has all the permissions of a regular user. For an AI agent that only needs to read data from one endpoint and write to another, that is massive over-permission. Least-privilege is the foundational principle of access control, and fake user accounts violate it immediately.
No credential rotation. User passwords are designed to be long-lived and human-memorable. They are not designed to be rotated on a schedule the way service credentials should be. In practice, the fake user's password never changes, which means a leaked credential is a permanent breach.
No audit trail separation. When an agent operating as agent@yourapp.com makes a write operation, your logs show a user action. You cannot distinguish agent activity from human activity. Debugging becomes a nightmare, compliance becomes impossible, and incident response becomes guesswork.
Sessions and cookies do not make sense. User auth produces sessions tied to browsers. Agents do not have browsers. They do not maintain state across requests the same way users do. Forcing them through a session-based flow creates fragile plumbing that breaks under load or on token expiry.
MFA breaks the whole thing. If you ever enforce MFA for user accounts — and you should — your fake agent account will be locked out the moment MFA is required. Either you exempt it, which creates a security hole, or you break your automation.
The Right Mental Model: Machine-to-Machine Authentication
The correct solution has existed in the OAuth 2.0 specification for years. It is called the client credentials flow, and it is specifically designed for non-human identities authenticating without user involvement.
Instead of a user logging in with credentials, a machine authenticates using a client_id and client_secret. It sends those credentials directly to the token endpoint and receives an access token in return. No redirects. No browser. No session. Just a token that the machine can use to call APIs on its own behalf.
The access token issued through the client credentials flow is structurally different from a user token. It does not have a sub claim representing a user. It does not expire into a refresh token cycle the way user sessions do. It is scoped to the capabilities of the machine application, not the permissions of a specific person. This distinction is crucial.
In Kinde, this pattern is implemented through M2M applications — Machine-to-Machine applications. An M2M application is a registered identity in Kinde with its own client_id and client_secret, its own set of API scopes, and its own token. It is the right primitive for any non-human caller: a CI/CD pipeline, a backend service, an automation script, or an AI agent.
Two Types of Machine Identity in Kinde
Kinde supports two types of M2M applications, and understanding the difference is important for building the right auth model for your product.
Global M2M Applications
A global M2M application is not tied to any specific organization in your Kinde setup. It operates across your entire tenant space. This makes it suitable for internal tooling, admin automation, infrastructure scripts, and anything that needs to operate at the system level rather than within a specific customer's context.
A typical use case is a CI/CD pipeline that needs to call the Kinde Management API to create users, update flags, or deploy configuration changes across all of your organizations. It does not need to be scoped to one tenant — it needs cross-tenant access, and a global M2M app provides exactly that.
Org-Scoped M2M Applications
An org-scoped M2M application is tied to a specific organization — a specific tenant — in your Kinde setup. Tokens issued to this app automatically include the org_code claim, which enforces that all API calls made by this machine identity are restricted to that organization's context.
This is the right pattern for AI agents acting on behalf of a specific customer. If Acme Corp has an AI agent that reads their data, writes to their workspace, and takes actions in their context, that agent should not have any access to the data or context of Beta Inc. Org-scoped M2M applications enforce this boundary at the token level — before your API even has to think about it.
Note: Org-scoped M2M applications are available on the Kinde Plus and Kinde Scale plans.
Global vs Org-Scoped M2M: At a Glance
| Feature | Global M2M App | Org-Scoped M2M App |
|---|---|---|
| Org context in token | No | Yes (org_code claim) |
| Tenant data isolation | Manual (your API must enforce it) | Enforced at token issuance |
| Best for | Admin scripts, internal automation, infra | Per-tenant AI agents, scoped APIs |
| Token restrictions | None | Scoped to one organization |
| Credential rotation | Manual, in Kinde dashboard | Manual, in Kinde dashboard |
| Available on | All plans | Kinde Plus and Scale |
How the Client Credentials Flow Works in Kinde
When an M2M application needs to call an API, it follows the OAuth 2.0 client credentials flow. Here is exactly what that looks like in code.
Step #1: Request a Token
The machine application sends a POST request to your Kinde domain's token endpoint. It authenticates with its client_id and client_secret and requests the scopes it needs.
curl --request POST \
--url https://YOUR_KINDE_DOMAIN.kinde.com/oauth2/token \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data grant_type=client_credentials \
--data client_id=YOUR_M2M_CLIENT_ID \
--data client_secret=YOUR_M2M_CLIENT_SECRET \
--data audience=https://YOUR_KINDE_DOMAIN.kinde.com/api \
--data scope=read:users write:flags
Note: Replace YOUR_KINDE_DOMAIN, YOUR_M2M_CLIENT_ID, and YOUR_M2M_CLIENT_SECRET with the values from your Kinde dashboard.
Step #2: Receive the Access Token
Kinde responds with an access token:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"expires_in": 86400,
"scope": "read:users write:flags"
}
For an org-scoped M2M app, the decoded token payload will also include the org_code claim:
{
"iss": "https://YOUR_KINDE_DOMAIN.kinde.com",
"sub": "YOUR_M2M_CLIENT_ID",
"aud": ["https://YOUR_KINDE_DOMAIN.kinde.com/api"],
"exp": 1234567890,
"iat": 1234567890,
"scope": "read:users write:flags",
"org_code": "org_abc123"
}
The org_code claim is embedded at token issuance time and cannot be modified. Your API can trust it completely.
Step #3: Call Your API with the Token
The agent uses the access token as a Bearer token in subsequent API calls:
// Node.js example — calling your API as an M2M client
const response = await fetch("https://api.yourapp.com/v1/users", {
method: "GET",
headers: {
// pass the M2M token as a Bearer token
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
const data = await response.json();
Step #4: Verify the Token on Your API
Your API receives the token and verifies it. For an org-scoped M2M app, you can trust the org_code claim directly from the verified token to enforce tenant isolation without any additional lookup:
// Express.js middleware — verifying and using the org_code from an M2M token
import jwt from "jsonwebtoken";
import jwksClient from "jwks-rsa";
const client = jwksClient({
jwksUri: `https://YOUR_KINDE_DOMAIN.kinde.com/.well-known/jwks.json`,
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
const signingKey = key.getPublicKey();
callback(null, signingKey);
});
}
function verifyM2MToken(req, res, next) {
const authHeader = req.headers["authorization"];
const token = authHeader && authHeader.split(" ")[1];
if (!token) {
return res.status(401).json({ error: "No token provided" });
}
jwt.verify(
token,
getKey,
{
audience: "https://YOUR_KINDE_DOMAIN.kinde.com/api",
issuer: "https://YOUR_KINDE_DOMAIN.kinde.com",
},
(err, decoded) => {
if (err) {
return res.status(403).json({ error: "Invalid token" });
}
// org_code is now trusted and available — no additional lookup needed
req.orgCode = decoded.org_code;
req.scopes = decoded.scope ? decoded.scope.split(" ") : [];
next();
}
);
}
// Use in a route to enforce org isolation
app.get("/v1/users", verifyM2MToken, async (req, res) => {
// All queries are automatically scoped to this organization
const users = await db.users.findAll({
where: { org_code: req.orgCode },
});
res.json({ users });
});
Wonderful! Your API now enforces tenant isolation based entirely on what is in the verified token — no additional queries, no custom middleware logic, no trust-but-verify gymnastics.
Setting Up an M2M Application in Kinde
Creating a Global M2M Application
- In your Kinde dashboard, navigate to Settings → Applications.
- Select Add application.
- Give your application a name — something descriptive like
"Reporting Pipeline"or"Internal Automation Agent". - Select Machine to machine as the application type.
- Select Save.
Kinde generates a client_id and client_secret for your new application. Save the secret immediately — it is only shown once.
Assigning Scopes to the M2M Application
Before your M2M application can call any API, you need to assign the scopes it is permitted to request. Scopes define the boundaries of what the token can do.
Navigate to the APIs section of your M2M application and select the APIs the application needs access to. For each API, choose only the scopes genuinely required by the agent. A reporting agent that only reads data should have read:reports, not write:reports or delete:reports. Least privilege applies here just as much as it does for users.
Creating an Org-Scoped M2M Application
- In your Kinde dashboard, navigate to Organizations.
- Select the organization you want to scope the M2M app to.
- Select the M2M apps tab.
- Enter a name for the application — for example,
"Acme Corp AI Agent". - Select M2M application as the type.
- Select Save.
Kinde generates a client_id and client_secret tied specifically to that organization. Tokens issued with these credentials will automatically include the org_code claim set to that organization's code.
A Real-World Scenario: Per-Tenant AI Agent
Here is a concrete scenario to make this tangible. You are building a B2B SaaS product with an AI feature that automatically summarizes activity for each customer's workspace. The agent needs to read data from your API, process it, and write a summary back.
Without org-scoped M2M, your agent either has global access (dangerous) or you build custom tenant-checking middleware on every endpoint (fragile). With org-scoped M2M in Kinde, each customer's agent gets its own set of credentials that are cryptographically tied to their organization. The token tells your API exactly which tenant it belongs to, and your API can trust that claim without any additional work.
The full flow for this scenario looks like:
- On onboarding, create an org-scoped M2M application for the new customer's organization in Kinde.
- Store the
client_idandclient_secretsecurely (environment variables, secrets manager, etc.). - When the agent needs to act, it requests a token using the client credentials flow.
- It calls your API with the token as a Bearer header.
- Your API verifies the token, extracts
org_code, and scopes all database queries to that organization. - The agent can never access another tenant's data, even if its credentials were somehow leaked — because the token itself is bound to one org.
What About Kinde's MCP Server?
In January 2026, Kinde shipped an MCP (Model Context Protocol) server. This lets AI agents and AI tools connect to Kinde directly using the MCP protocol — which is rapidly becoming the standard interface for AI-to-tool communication.
What this means in practice is that AI agents using MCP-compatible frameworks (like Claude Code, Cursor, or any agent built on an MCP client library) can interact with Kinde's management capabilities — creating users, managing organizations, checking permissions, and more — through a standardized tool interface rather than raw API calls.
For teams building AI-native products, this is a big deal. Instead of writing custom integration code to give your agent access to Kinde's capabilities, you can connect it via the MCP server and let it use those capabilities as native tools.
The MCP server and M2M applications complement each other. M2M handles the authentication layer — proving the agent's identity and authorizing its access. The MCP server handles the interface layer — giving the agent a structured way to use Kinde's functionality once it is authenticated.
Security Best Practices for M2M Auth
There are a few principles that are non-negotiable when running machine identities in production.
One M2M application per service. Do not share credentials across multiple agents or services. If one service is compromised, its credentials can be revoked without affecting anything else. Shared credentials mean shared blast radius.
Minimum necessary scopes. Every M2M application should be granted only the scopes it genuinely needs to do its job. An agent that reads data should not have write access. An agent that writes to one resource should not have write access to all resources. Define your API scopes granularly and assign them carefully.
Rotate client secrets on a schedule. Client secrets are long-lived credentials and they should be rotated periodically. Kinde provides secret rotation directly in the dashboard. Build secret rotation into your operational runbook and do not treat it as optional.
Never put client secrets in client-side code. M2M credentials authenticate a service, not a user. They must live server-side — in environment variables, in a secrets manager, in a secure deployment config. Putting them anywhere that gets shipped to a browser is an immediate credential leak.
Track client_id and org_code in your logs. When an M2M token is used to call your API, log the client_id alongside the org_code. This gives you a clean audit trail that distinguishes what each agent did, in which tenant's context, at what time. When something goes wrong — and eventually something will — this makes debugging and incident response dramatically faster.
Token expiry is your friend. M2M tokens expire. Do not try to cache them indefinitely. Let them expire and re-request as needed. Short expiry windows limit the blast radius of a leaked token significantly. Kinde issues M2M tokens with configurable expiry — keep it as short as your use case allows.
Conclusion
In this article, you learned why the traditional user auth model breaks down for AI agents, what non-human identities are and why they need their own auth layer, and how the OAuth 2.0 client credentials flow provides the right foundation for machine authentication. You also saw how Kinde's M2M applications implement this properly — with global apps for internal tooling and org-scoped apps for per-tenant AI agents that enforce isolation at the token level.
The shift to AI-native products is not slowing down. Every SaaS product built in 2026 is adding or planning to add agent capabilities. The teams that get non-human identity right from the beginning are the ones that avoid the security incidents, the audit nightmares, and the architectural rewrites that come from bolting fake user accounts onto a system that was never designed for them.
Kinde gives you the primitives to do this properly from day one. Create a free Kinde account and start building your M2M auth layer today.






![Kinde dashboard — Organizations > [specific org] > M2M apps tab, showing the](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%2F186czcebzvmcjsotba3p.png)


Top comments (0)