{"slug": "kinde-is-missing-from-mastra-s-auth-lineup-so-i-built-the-provider", "title": "Kinde Is Missing from Mastra's Auth Lineup, So I Built the Provider", "summary": "A developer built `mastra-auth-kinde`, an open-source auth provider that integrates Kinde with the Mastra AI agent framework. The provider enables multi-tenant authentication, machine-to-machine communication, and access control for Mastra's API routes and Studio UI. It addresses a gap in Mastra's official auth lineup, which previously lacked Kinde support.", "body_md": "*If you're building a SaaS AI agent product and you're already on Kinde, you already know the problem.*\n\nMastra is the TypeScript-first AI agent framework. It ships with official auth providers for Clerk, Auth0, Supabase, Firebase, WorkOS, and Better Auth. Kinde is not on that list.\n\nThe obvious question is why not reach for one of those providers, since Auth0 is already there.\n\nMost developers who choose Kinde rely on far more than its login. Kinde ships with the organizational structures, permission systems, and monetization tools that products actually need, bringing auth, billing, feature flags, and multi-tenancy together in one platform. If you're building a B2B SaaS product on Kinde, you're using Kinde orgs to segment your customers, Kinde billing to manage subscriptions, and Kinde feature flags to gate features by plan. Switching to Auth0 or Clerk to support a Mastra agent would mean rebuilding all of that elsewhere, which is not a real option.\n\nThat gap is the problem. You need Kinde to work with Mastra, and until now there was no clean way to connect them.\n\nThat's why I built `mastra-auth-kinde`\n\n.\n\nWhen you add an auth provider to Mastra, it protects two things at once: all your API routes (`/api/agents/*`\n\n, `/api/workflows/*`\n\n, and so on) and your Mastra Studio UI. Every request to a protected route goes through your provider before it reaches anything else.\n\nYou extend Mastra's `MastraAuthProvider`\n\nbase class and implement two methods:\n\n`authenticateToken(token, request)`\n\nverifies the JWT and returns the decoded user, or `null`\n\nif it fails`authorizeUser(user, request)`\n\nreturns `true`\n\nto let the request through, or `false`\n\nfor a 403Mastra handles everything else: extracting the Bearer token from the Authorization header, calling your methods in order, and storing the verified user in the request context so your agents and tools can access it.\n\nBeyond the billing and org story above, a few things make Kinde the right fit for agent developers in particular.\n\nFirst, Kinde's org model maps directly to multi-tenancy, so each customer gets isolated data and configuration without you building it. Each Kinde organization is a tenant, and the `org_code`\n\nclaim on every token tells you exactly which org the user belongs to. For a multi-tenant agent, one that serves different customers on the same infrastructure, this is exactly what you need.\n\nSecond, machine-to-machine authentication comes standard rather than as an expensive add-on. AI agents frequently need to run background jobs, scheduled workflows, and nightly pipelines with no human user in the loop. Kinde handles this natively with client credentials tokens, and this provider handles those too, which I cover below.\n\nThird, the free tier is usable for real products, including 10,500 monthly active users, unlimited organizations, and all authentication methods including SSO. You can build a real multi-tenant agent product without paying anything until you're at scale.\n\n```\nnpm install github:sholajegede/mastra-auth-kinde\n```\n\nYou also need `@mastra/core`\n\nif you don't already have it:\n\n```\nnpm install @mastra/core\n```\n\nWire the provider into your Mastra instance:\n\n``` js\nimport { Mastra } from '@mastra/core'\nimport { MastraAuthKinde } from 'mastra-auth-kinde'\n\nexport const mastra = new Mastra({\n  server: {\n    auth: new MastraAuthKinde({\n      domain: 'https://yourapp.kinde.com',\n    }),\n  },\n})\n```\n\nOr use environment variables:\n\n```\nKINDE_DOMAIN=https://yourapp.kinde.com\nKINDE_AUDIENCE=https://api.yourapp.com  # optional — see the audience section below\njs\nexport const mastra = new Mastra({\n  server: {\n    auth: new MastraAuthKinde(),\n  },\n})\n```\n\nOnce this is in place, every request to `/api/*`\n\nneeds a valid Kinde access token in the `Authorization: Bearer <token>`\n\nheader. Unauthenticated requests get a 401, and requests that fail the authorization check get a 403.\n\nA standard Kinde user access token carries these claims:\n\n```\niss, sub, aud, azp, exp, iat, jti, scp\n```\n\nThe provider verifies the token against Kinde's JWKS endpoint, validates the issuer and expiry, and checks the audience claim if you have the option configured.\n\n[Kinde's](https://kinde.com/?utm_source=devto&utm_medium=content&utm_campaign=shola&campaignid=chatgptapp&network=&adgroup=&keyword=&matchtype=&creative=16&device=&adposition=) JWKS endpoint sits at `/.well-known/jwks`\n\nwith no `.json`\n\nextension, even though the common assumption is `/.well-known/jwks.json`\n\n. Verify it against your tenant's discovery document before assuming:\n\n```\ncurl -s https://yourapp.kinde.com/.well-known/openid-configuration | jq .jwks_uri\n# \"https://yourapp.kinde.com/.well-known/jwks\"\n```\n\nThe provider handles this correctly out of the box.\n\nOnly set `audience`\n\n(or `KINDE_AUDIENCE`\n\n) once you've registered and bound an API audience in the Kinde dashboard. A default Kinde user token carries an empty `aud`\n\narray, and any token with an empty `aud`\n\nwill fail the audience check if the option is set.\n\n```\nnew MastraAuthKinde({\n  domain: 'https://yourapp.kinde.com',\n  audience: 'https://api.yourapp.com', // only add this after binding an API audience in Kinde\n})\n```\n\nIn the standard agent setup, a human user logs in, the frontend gets a token, and that token is passed to the Mastra API, which works for user-facing agents. Real agent architectures also have background jobs, things like nightly workflows, scheduled pipelines, and cron tasks, running with no human attached.\n\nKinde handles this with machine-to-machine apps using the OAuth client credentials flow. The resulting token looks different from a user token:\n\n```\n{\n  \"aud\": [\"https://yourapp.kinde.com/api\"],\n  \"azp\": \"your_client_id\",\n  \"exp\": 1234567890,\n  \"gty\": [\"client_credentials\"],\n  \"iat\": 1234567890,\n  \"iss\": \"https://yourapp.kinde.com\",\n  \"org_code\": \"org_abc123\",\n  \"scope\": \"read:users\",\n  \"scp\": [],\n  \"v\": \"2\"\n}\n```\n\nTwo things to notice: there is no `sub`\n\nclaim, and there is a `gty`\n\nclaim set to `[\"client_credentials\"]`\n\n. That combination tells you this is a machine rather than a person.\n\nThe provider detects this and treats the token as a trusted system actor instead of rejecting it. You can check for it in your agent tools or route handlers:\n\n``` js\nimport { isSystemActor } from 'mastra-auth-kinde'\n\nconst user = requestContext.get('user')\n\nif (isSystemActor(user)) {\n  // trusted background process — no human user attached\n  console.log('running for org:', user.org_code)\n}\n```\n\nSo a nightly workflow can mint a Kinde M2M token, pass it to your Mastra API, and authenticate cleanly, with no human user required.\n\nIf you're building a multi-tenant product where different organizations should only access their own agents and data, use `allowedOrgCodes`\n\n:\n\n```\nnew MastraAuthKinde({\n  domain: 'https://yourapp.kinde.com',\n  allowedOrgCodes: ['org_abc123', 'org_def456'],\n})\n```\n\nThe provider checks the `org_code`\n\nclaim on every token and rejects anything that doesn't match. This works for both user tokens (when org claims are enabled in Kinde) and M2M tokens, which carry `org_code`\n\nautomatically when the app is org-scoped in Kinde.\n\nOn user tokens, `org_code`\n\nonly appears once you've enabled org claims in the Kinde dashboard token customization settings, so if you decode a default user token and the claim is missing, that is a Kinde config step rather than a bug in the provider.\n\nThe provider uses `jose`\n\nfor JWT verification, which runs on Web Crypto and `fetch`\n\n, with no Node.js built-ins and no `nodejs_compat`\n\nflag needed on Cloudflare Workers.\n\nThis matters if you're planning to deploy your Mastra agent to the edge. Node-only JWKS libraries fail at build time on Workers with missing `crypto`\n\n, `buffer`\n\n, `stream`\n\n, `http`\n\n, `https`\n\n, `events`\n\n, and `util`\n\n. With `jose`\n\n, it builds clean and runs without any additional flags. This is the same approach Mastra's own Auth0 provider uses.\n\nHere's a full setup for a multi-tenant Mastra agent that supports both human users and background M2M workflows:\n\n``` js\nimport { Mastra } from '@mastra/core'\nimport { Agent } from '@mastra/core/agent'\nimport { openai } from '@ai-sdk/openai'\nimport { MastraAuthKinde } from 'mastra-auth-kinde'\n\nconst supportAgent = new Agent({\n  name: 'support-agent',\n  instructions: 'You are a helpful support agent.',\n  model: openai('gpt-4o-mini'),\n})\n\nexport const mastra = new Mastra({\n  agents: { supportAgent },\n  server: {\n    auth: new MastraAuthKinde({\n      domain: process.env.KINDE_DOMAIN!,\n      audience: process.env.KINDE_AUDIENCE,\n      allowedOrgCodes: process.env.KINDE_ALLOWED_ORGS?.split(','),\n    }),\n  },\n})\n```\n\nOn the frontend, get the access token from the Kinde SDK and pass it as a Bearer token:\n\n``` js\nimport { useKindeAuth } from '@kinde-oss/kinde-auth-nextjs'\n\nconst { getToken } = useKindeAuth()\n\nconst response = await fetch('/api/agents/support-agent/generate', {\n  method: 'POST',\n  headers: {\n    'Authorization': `Bearer ${await getToken()}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    messages: [{ role: 'user', content: 'Hello' }],\n  }),\n})\n```\n\nFor background workflows, get an M2M token via the client credentials flow:\n\n``` js\n// Server-side only — never expose client_secret in the browser\nconst tokenResponse = await fetch(\n  `${process.env.KINDE_DOMAIN}/oauth2/token`,\n  {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n    body: new URLSearchParams({\n      grant_type: 'client_credentials',\n      client_id: process.env.KINDE_M2M_CLIENT_ID!,\n      client_secret: process.env.KINDE_M2M_CLIENT_SECRET!,\n      audience: process.env.KINDE_AUDIENCE!,\n    }),\n  }\n)\n\nconst { access_token } = await tokenResponse.json()\n\nconst agentResponse = await fetch('/api/agents/support-agent/generate', {\n  method: 'POST',\n  headers: {\n    'Authorization': `Bearer ${access_token}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    messages: [{ role: 'user', content: 'Run the nightly summary' }],\n  }),\n})\n```\n\nBoth token types work through the same provider, the same API, and the same Mastra instance.\n\n``` js\nimport {\n  MastraAuthKinde,  // The main auth provider class\n  isSystemActor,    // Returns true for M2M / client credentials tokens\n} from 'mastra-auth-kinde'\n```\n\nFull options:\n\n```\nnew MastraAuthKinde({\n  domain: 'https://yourapp.kinde.com',     // Required\n  audience: 'https://api.yourapp.com',      // Optional — only set after binding in Kinde\n  allowedOrgCodes: ['org_abc123'],           // Optional — restrict to specific orgs\n  name: 'kinde',                             // Optional — overrides the provider name\n})\n```\n\nThe provider is open-source and usable today at [github.com/sholajegede/mastra-auth-kinde](https://github.com/sholajegede/mastra-auth-kinde).\n\n[Kinde is free for up to 10,500 monthly active users](https://kinde.com/?utm_source=devto&utm_medium=content&utm_campaign=shola&campaignid=chatgptapp&network=&adgroup=&keyword=&matchtype=&creative=16&device=&adposition=), with no credit card required to start. You can create an account and try this provider against a test organization at kinde.com.\n\n*If this saved you time, drop a reaction. And if you're building something with Mastra agents, I'd love to hear about it in the comments.*", "url": "https://wpnews.pro/news/kinde-is-missing-from-mastra-s-auth-lineup-so-i-built-the-provider", "canonical_source": "https://dev.to/sholajegede/kinde-is-missing-from-mastras-auth-lineup-so-i-built-the-provider-5gc9", "published_at": "2026-06-21 21:10:11+00:00", "updated_at": "2026-06-21 21:25:20.366579+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents"], "entities": ["Kinde", "Mastra", "Auth0", "Clerk", "Supabase", "Firebase", "WorkOS", "Better Auth"], "alternates": {"html": "https://wpnews.pro/news/kinde-is-missing-from-mastra-s-auth-lineup-so-i-built-the-provider", "markdown": "https://wpnews.pro/news/kinde-is-missing-from-mastra-s-auth-lineup-so-i-built-the-provider.md", "text": "https://wpnews.pro/news/kinde-is-missing-from-mastra-s-auth-lineup-so-i-built-the-provider.txt", "jsonld": "https://wpnews.pro/news/kinde-is-missing-from-mastra-s-auth-lineup-so-i-built-the-provider.jsonld"}}