[GH-ISSUE #8535] Expose verifyApiKey on authClient for server-to-server and hosted Better Auth use cases #28434

Closed
opened 2026-04-17 19:53:14 -05:00 by GiteaMirror · 3 comments
Owner

Originally created by @heiwen on GitHub (Mar 10, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/8535

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Summary

A valid use case for authClient.apiKey.verify(...) is when Better Auth is hosted as a separate auth service and another backend needs to verify API keys against it over HTTP.

In that setup, the consuming backend cannot use auth.api.verifyApiKey(...) because auth.api is only available in-process on the Better Auth server. The only current option is to call POST /api-key/verify manually with fetch().

Why this is a problem

That means remote backends lose the normal Better Auth client ergonomics:

  • no typed client method
  • no path abstraction
  • no shared request/response typing
  • manual error handling around a supported endpoint

This feels like an odd gap because the endpoint already exists and is meant to be callable over HTTP, but there is no equivalent client wrapper.

Current behavior

Available today:

  • in-process server usage via auth.api.verifyApiKey(...)
  • direct HTTP usage via POST /api-key/verify

Missing today:

  • authClient.apiKey.verify(...)

Concrete use case

We host Better Auth as a standalone service and want another backend service to verify API keys against it.

That second service is not a browser client, but it is also not running in the same process as the Better Auth server, so auth.api is not available. A typed authClient method would be the natural integration point.

Proposed API

const result = await authClient.apiKey.verify({
  key: apiKey,
  configId: "secret",
  permissions: {
    projects: ["read"],
  },
});

Why this seems reasonable

  • /api-key/verify already exists
  • authClient is also used outside the browser, including server-side HTTP integrations
  • this would close the gap between supported HTTP endpoints and exposed client methods
  • consumers with hosted Better Auth deployments would no longer need to hand-roll fetch() calls

Notes

I understand the reasoning for treating API key verification as server-side only. This request is not about encouraging browser-side verification. It is about supporting remote server-to-server usage with a typed client wrapper.

If exposing this on the default browser-oriented client surface is a concern, an alternative could be documenting it as a server-side authClient capability from better-auth/client, rather than as a browser-facing pattern.

What version of Better Auth are you using?

1.5

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "unknown",
    "release": "25.2.0",
    "cpuCount": 8,
    "cpuModel": "Apple M2",
    "totalMemory": "16.00 GB",
    "freeMemory": "0.58 GB"
  },
  "node": {
    "version": "v24.3.0",
    "env": "development"
  },
  "packageManager": {
    "name": "bun",
    "version": "1.3.10"
  },
  "frameworks": null,
  "databases": null,
  "betterAuth": {
    "version": "Unknown",
    "config": null
  }
}

Which area(s) are affected? (Select all that apply)

Backend, Client

Auth config (if applicable)

n/a

Additional context

No response

Originally created by @heiwen on GitHub (Mar 10, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/8535 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce **Summary** A valid use case for `authClient.apiKey.verify(...)` is when Better Auth is hosted as a separate auth service and another backend needs to verify API keys against it over HTTP. In that setup, the consuming backend cannot use `auth.api.verifyApiKey(...)` because `auth.api` is only available in-process on the Better Auth server. The only current option is to call `POST /api-key/verify` manually with `fetch()`. **Why this is a problem** That means remote backends lose the normal Better Auth client ergonomics: - no typed client method - no path abstraction - no shared request/response typing - manual error handling around a supported endpoint This feels like an odd gap because the endpoint already exists and is meant to be callable over HTTP, but there is no equivalent client wrapper. **Current behavior** Available today: - in-process server usage via `auth.api.verifyApiKey(...)` - direct HTTP usage via `POST /api-key/verify` Missing today: - `authClient.apiKey.verify(...)` **Concrete use case** We host Better Auth as a standalone service and want another backend service to verify API keys against it. That second service is not a browser client, but it is also not running in the same process as the Better Auth server, so `auth.api` is not available. A typed `authClient` method would be the natural integration point. **Proposed API** ```ts const result = await authClient.apiKey.verify({ key: apiKey, configId: "secret", permissions: { projects: ["read"], }, }); ``` **Why this seems reasonable** - `/api-key/verify` already exists - `authClient` is also used outside the browser, including server-side HTTP integrations - this would close the gap between supported HTTP endpoints and exposed client methods - consumers with hosted Better Auth deployments would no longer need to hand-roll `fetch()` calls **Notes** I understand the reasoning for treating API key verification as server-side only. This request is not about encouraging browser-side verification. It is about supporting remote server-to-server usage with a typed client wrapper. If exposing this on the default browser-oriented client surface is a concern, an alternative could be documenting it as a server-side `authClient` capability from `better-auth/client`, rather than as a browser-facing pattern. ### What version of Better Auth are you using? 1.5 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "unknown", "release": "25.2.0", "cpuCount": 8, "cpuModel": "Apple M2", "totalMemory": "16.00 GB", "freeMemory": "0.58 GB" }, "node": { "version": "v24.3.0", "env": "development" }, "packageManager": { "name": "bun", "version": "1.3.10" }, "frameworks": null, "databases": null, "betterAuth": { "version": "Unknown", "config": null } } ``` ### Which area(s) are affected? (Select all that apply) Backend, Client ### Auth config (if applicable) ```typescript n/a ``` ### Additional context _No response_
GiteaMirror added the lockedenhancement labels 2026-04-17 19:53:15 -05:00
Author
Owner

@heiwen commented on GitHub (Mar 11, 2026):

As a follow-up, in recent versions of better-auth, /api-key/verify does NOT exist anymore as HTTP endpoint. It was removed in the following PR: https://github.com/better-auth/better-auth/pull/6275

This makes the server-to-server API key validation impossible today. As a minimal fix, suggesting to re-introduce the external route.

Or are there any security concerns with this approach?

<!-- gh-comment-id:4037810063 --> @heiwen commented on GitHub (Mar 11, 2026): As a follow-up, in recent versions of better-auth, `/api-key/verify` does NOT exist anymore as HTTP endpoint. It was removed in the following PR: https://github.com/better-auth/better-auth/pull/6275 This makes the server-to-server API key validation impossible today. As a minimal fix, suggesting to re-introduce the external route. Or are there any security concerns with this approach?
Author
Owner

@himself65 commented on GitHub (Mar 11, 2026):

Closing this as won't-fix. The /api-key/verify endpoint is intentionally not exposed over HTTP for security reasons:

  • No authentication — The endpoint has no session middleware (by design, since it validates the API key itself). Exposing it publicly creates an unauthenticated brute-force surface.
  • Destructive side effects — Verification decrements usage counts, triggers refills, and deletes expired keys. A public endpoint lets any caller consume API key quotas.
  • Information disclosure — Error responses reveal key state (KEY_DISABLED, KEY_EXPIRED, USAGE_EXCEEDED, RATE_LIMITED), and success responses return full key metadata.

The endpoint is defined without a path string (the "virtual/path-less" overload of createAuthEndpoint), which the router intentionally skips during HTTP route registration. This is a deliberate design choice.

For server-to-server use cases

If you're hosting Better Auth as a standalone service and need remote verification, the recommended approach is to create a thin authenticated wrapper endpoint on your auth server:

// On your Better Auth server
app.post("/internal/verify-api-key", authMiddleware, async (req) => {
  const result = await auth.api.verifyApiKey({
    body: req.body,
  });
  return result;
});

This way you control the authentication, rate limiting, and access scope for remote callers, rather than exposing the raw verify endpoint publicly.

<!-- gh-comment-id:4040928974 --> @himself65 commented on GitHub (Mar 11, 2026): Closing this as won't-fix. The `/api-key/verify` endpoint is intentionally **not exposed over HTTP** for security reasons: - **No authentication** — The endpoint has no session middleware (by design, since it validates the API key itself). Exposing it publicly creates an unauthenticated brute-force surface. - **Destructive side effects** — Verification decrements usage counts, triggers refills, and deletes expired keys. A public endpoint lets any caller consume API key quotas. - **Information disclosure** — Error responses reveal key state (`KEY_DISABLED`, `KEY_EXPIRED`, `USAGE_EXCEEDED`, `RATE_LIMITED`), and success responses return full key metadata. The endpoint is defined without a path string (the "virtual/path-less" overload of `createAuthEndpoint`), which the router intentionally skips during HTTP route registration. This is a deliberate design choice. ### For server-to-server use cases If you're hosting Better Auth as a standalone service and need remote verification, the recommended approach is to create a thin **authenticated** wrapper endpoint on your auth server: ```typescript // On your Better Auth server app.post("/internal/verify-api-key", authMiddleware, async (req) => { const result = await auth.api.verifyApiKey({ body: req.body, }); return result; }); ``` This way you control the authentication, rate limiting, and access scope for remote callers, rather than exposing the raw verify endpoint publicly.
Author
Owner

@heiwen commented on GitHub (Mar 12, 2026):

Thank you for the clear explanation, let me create my on verify-api-key wrapper endpoint for now.

Wondering whether server-to-server / better-auth as standalone service is an interesting scenario to make a first class citizen in better-auth in the future?

That would make it easier to migrate from hosted services like Clerk, which require this model naturally.

(https://clerk.com/docs/reference/backend-api/tag/api-keys/post/api_keys/verify)

<!-- gh-comment-id:4043743071 --> @heiwen commented on GitHub (Mar 12, 2026): Thank you for the clear explanation, let me create my on verify-api-key wrapper endpoint for now. Wondering whether `server-to-server` / `better-auth as standalone service` is an interesting scenario to make a first class citizen in better-auth in the future? That would make it easier to migrate from hosted services like `Clerk`, which require this model naturally. (https://clerk.com/docs/reference/backend-api/tag/api-keys/post/api_keys/verify)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#28434