API authentication best practices come down to one decision per caller: which credential it holds and how long that credential lasts. A browser gets a session, a partner’s server gets a revocable API key, your own worker gets a short-lived token, and a cloud service gets an identity with no stored secret at all.

What API authentication best practices mean: match the caller to the credential

API authentication best practices give each caller its own credential: a session for a browser, a scoped key for a partner server, a short-lived token for your own worker, a platform identity for a cloud service, and a user-scoped token for an agent. Authentication says who is calling; authorization, checked on the server, decides what it may do.

This page is the machine side of the authentication checklist: every caller that is not a person typing a password, from a partner’s server to an AI agent. The table is my working answer for one app, one row per caller, and its second column is the short list of different types of authentication in a REST API, each tied to the caller it suits.

CallerCredentialWhere it livesHow it is revokedHow long it lasts
A browser or mobile app acting for a userA session cookie or a short-lived access token from the login flow (authentication best practices for user flows)The browser’s cookie store or the app’s secure storageSign-out, or ending the session on the serverThe session’s lifetime
A partner’s or customer’s serverAn API key, or OAuth client credentials when they need scopes and expiryThe partner’s own secrets storeRevoke that one key or clientUntil you rotate or revoke it
Your own worker or second serviceA short-lived token; a shared secret only where the platform offers nothing better, rotatedIssued at runtime; any shared secret sits in your secrets managerLet the token expire; rotate the secretShort, and it expires on its own
A cloud service calling anotherA platform identity (an IAM role or a service account), no stored secretNowhere you manage: the platform issues itRemove the role or the bindingTemporary credentials for each session
An agent or MCP serverA token scoped to its user and to what it may doThe agent’s runtime, never your repositoryRevoke the grantShort-lived
A webhook from a providerA signature you verify, not an auth header (Stripe webhook signature verification)The signing secret, on your serverRotate the signing secretUntil rotated

The security side of REST API authentication best practices is the same for every row: TLS on every call, no credential in a URL or a log, and one named owner for each credential.

Rows two to five cover server-to-server traffic, and the machine-to-machine (M2M) authentication best practices behind them are simple to state: every non-human caller holds its own credential, you can revoke one caller without touching the others, and nothing it holds outlives its job by much. Microservices authentication best practices follow the third row, where each service gets its own identity and a token that expires. That is also what I mean by application to application authentication on this page: servers calling servers, not one mobile app handing a sign-in to another on the same phone.

Authentication is only the first half of API authentication and authorization best practices; the check that decides what a caller may touch is authorization, and it runs on the server, on every request, never against a user id or role the request itself carries. The tenant rule is a separate topic, multi tenant data isolation, and roles are in RBAC examples. In microservices the same authorization best practices hold at each hop, in my reading: every service checks what the caller may do itself instead of trusting that the service in front of it already checked. GraphQL authorization best practices follow from that too: the check sits in the resolvers on the server, so each field a query reaches is checked, not only the endpoint.

A public read API is the closest thing to securing an API without authentication, and even there I’d give each caller a key, if only to rate limit them.

Why it matters: what a service-to-service gap looks like in an AI-built app

In my June and July 2026 audits, 10 of the 21 third-party apps trusted the client: the server accepted whatever the browser asserted. 11 of the 21 third-party apps had unauthenticated endpoints doing privileged work. 6 of the 21 third-party apps shipped a real secret.

Those 21 are the apps I audited, a selected set rather than a random sample, so the counts say what turned up there and are not a rate for AI-built apps in general. For an API, each finding has a plain shape, in my reading. Trusting the client means the server believes a user id or a role sent in the request. An unauthenticated privileged endpoint is a route that does admin work for anyone who finds its URL. A shipped secret, at its worst, is a provider’s service key in the browser bundle, which lets every visitor act as the service.

The ten checks for a whole API, including recomputing trusted values on the server and keeping privileged secrets out of the client, are in the API Security Checklist for AI-Built Apps; this page stays with who the caller is. Weak authentication has its own entry in OWASP’s API Security Top 10: OWASP API2:2023 Broken Authentication.

How it works: the credentials, one by one

Each machine credential answers three questions: who issued it, what it may do, and when it stops working. Basic auth and API keys are long-lived secrets you rotate, tokens expire on their own, cloud identities have no stored secret, and certificates identify the caller at the connection.

HTTP basic, digest and bearer: what still has a place between services and what does not

Basic authentication sends a user-id and password in the Authorization header, Base64-encoded, not encrypted, so it is not considered secure without TLS, and I’d use it only as a machine credential you can rotate. Bearer means whoever holds the token can use it, so the security lives in how the token was issued and how long it lives.

RFC 7617 defines the scheme: the client joins the user-id, a single colon and the password, then encodes the result with Base64. A request that lacks credentials can get a challenge back first: the RFC’s example is a 401 Unauthorized response carrying WWW-Authenticate: Basic realm="WallyWorld", where “WallyWorld” is the string the server assigns to identify the protection space. This is the RFC’s own HTTP basic authentication example, which sends the user-id “Aladdin” with the password “open sesame” as an Authorization: Basic header:

Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==

Decode that string and the password is back in plain view. MDN’s HTTP authentication guide says the scheme sends the credentials “encoded but not encrypted” and that “HTTPS/TLS should be used with basic authentication to prevent credential interception.”

Credentials in a URL are the weakest form of all. RFC 3986 says the user:password format in the userinfo field “is deprecated”, and that passing authentication information in clear text “has proven to be a security risk in almost every case where it has been used.” So basic authentication in a URL, such as a link with the password before the host, is a pattern to remove, not to secure.

Digest, defined in RFC 7616, is a challenge-response scheme in which “the password is never sent in the clear”, yet the same RFC says it is “vulnerable to dictionary attacks” with human-memorable passwords and “SHOULD be over a secure channel like HTTPS”. Digest authentication vs basic is therefore a small gain, in my reading: better than basic without TLS, no reason to skip TLS, and rarely worth building into a new API.

Bearer tokens travel in the same header. RFC 6750 defines a bearer token as one where “any party in possession of the token (a “bearer”) can use the token in any way that any other party in possession of it can.” Next to a JWT sent as a bearer token, basic auth sends the password itself on every call, while the token stands in for it and can expire.

Modern authentication is Microsoft’s name for the replacement. Microsoft’s basic authentication deprecation page says the Exchange Online change requires customers to move from basic authentication to “Modern authentication (OAuth 2.0 token-based authorization)”, and notes that OAuth access tokens “have a limited usable lifetime and are specific to the applications and resources for which they’re issued”.

Between an API key and basic auth I’d pick the key: both are long-lived secrets sent on every request, but a key can be scoped to one job and revoked without anyone’s password changing. If a PHP app receives basic credentials, the PHP manual’s HTTP authentication page says the values arrive in $_SERVER as PHP_AUTH_USER, PHP_AUTH_PW and AUTH_TYPE, and that only the Basic method is supported there, so check those rather than parsing the Authorization header yourself.

API keys done right: generate, hash, scope, rotate

An API key done right is generated randomly with a recognizable prefix, stored only as a hash, shown once, scoped to what it may do, tied to a named owner, rotated on a schedule, revocable on its own, logged on use and rate limited.

These are my working rules for how to implement API key authentication, in order:

  1. 01 Generate each key with a cryptographically secure random generator and give it a recognizable prefix, so a leaked key is easy to spot and trace
  2. 02 Store only a hash of the key and show the full key once, when it is created
  3. 03 Scope each key to the actions it needs and record who owns it
  4. 04 Rotate keys on a schedule, and make sure one key can be revoked without touching the others
  5. 05 Log each use of a key by its id, never the key itself
  6. 06 Rate limit per key

Per-key limits are covered in rate limiting in an API, and where the key lives on the caller’s side belongs to secrets management best practices. What an API key is, and how to get one from a provider, is a separate question from running keys for your own API. With API key based authentication the key stays a long-lived secret however well it is run, which is why REST API authentication token best practices lean toward tokens: where a caller can hold a short-lived token instead of a key, give it one.

Gateways implement the same idea. Azure API Management subscriptions define a subscription as “a named container for a pair of subscription keys”, so an application “can switch from key A to key B and regenerate key A with minimal disruption”. The same page says API Management “doesn’t provide built-in features to manage the lifecycle of subscription keys, such as setting expiration dates or automatically rotating keys”, so the rotation schedule is yours. For a public API, the Azure page’s advice on API keys fits the best practices above: a publisher could allow anonymous access, but “configuring another mechanism to secure client access is recommended.”

On AWS, API key security at API Gateway starts from AWS’s own warning on its page “Usage plans and API keys for REST APIs in API Gateway”: “Don’t use API keys for authentication or authorization to control access to your APIs.” The reason AWS gives is that a user with a valid key for one API in a usage plan can access all APIs in that usage plan, and it says to use “an IAM role, a Lambda authorizer, or an Amazon Cognito user pool” instead. So the AWS API Gateway authentication best practices are to authenticate the caller one of those ways and keep the key and its usage plan for throttling and quotas.

In Spring Boot, API key authentication is a custom filter added to the chain that Spring Security’s architecture page describes as deciding “which Spring Security Filter instances should be invoked for the current request”, and the page’s own example adds a filter with addFilterBefore. A JWT presented as a bearer token has to be validated on every request, and the checks that belong in that validation policy are a subject of their own: JWT security.

Take a founder who connects a partner’s server to the app’s API by pasting in a bearer token copied from the founder’s own admin session. The partner’s server now acts as the founder on every call, with the founder’s admin rights, and the only way to cut it off is to end the founder’s own session. The lesson I take from it: a machine caller needs its own credential, scoped and revocable on its own, because a bearer token asks for nothing but possession, and a borrowed user token gives the caller everything the user has.

Cloud identities: IAM roles, service accounts, and no secret at all

Cloud identities let services on one cloud call each other with no stored secret: the platform can issue the caller an identity and short-lived credentials. On Amazon EKS, IAM roles for service accounts associate an IAM role with a Kubernetes service account instead of distributing AWS credentials to containers. If the platform can issue the identity, create no long-lived key.

That last sentence is my working rule, and the reason it works is in how AWS IAM roles behave: a role has no “standard long-term credentials such as a password or access keys”, and assuming it “provides you with temporary security credentials for your role session.” IAM roles for service accounts bring that to pods: AWS notes that Kubernetes “has long used service accounts as its own internal identity system”, and IRSA maps an IAM role onto one. AWS’s EKS Pod Identity does the same job, and AWS’s page calls it “a simpler method than IAM roles for service accounts, as this method doesn’t use OIDC identity providers.”

A security token service (STS) is the piece that issues those short-lived credentials in exchange for proof of identity. If you are after a secure token service tutorial, start from your platform’s identity docs rather than building one; the AWS STS details sit with secrets management.

On Google Cloud (GCP), Cloud Run service-to-service authentication follows the same pattern: Google recommends “a per-service user-managed service account that has been granted the minimum set of permissions required to do its work”, and the calling service adds “a Google-signed OpenID Connect ID token” to each request.

Off-cloud, Okta’s client credentials guide is one vendor’s worked description of service-to-service authentication with the OAuth 2.0 client credentials grant: it serves client apps “with no end user”, and the app exchanges its client ID and secret with Okta “for an access token”. The guide uses a custom authorization server, and Okta notes that API Access Management, “a requirement to use Custom Authorization Servers”, “is an optional add-on in production environments.” It also sends the client ID and secret to the token endpoint through basic authentication, the machine-credential use of basic from the section above.

On the receiving side, service-to-service authentication with a JWT needs no custom code in Spring Boot: Spring Security’s resource server documentation describes two steps, the dependencies and the authorization server’s location, after which it will “automatically configure itself to validate JWT-encoded Bearer Tokens.”

Client certificates and mTLS: when a certificate is the right service credential, and how to test one

Mutual TLS has both sides prove who they are with certificates: the server identifies the caller at the connection, before any header, by the client certificate it presents. It suits a fixed set of machine callers you control, or a partner who requires it, and not browsers, a long tail of partners, or certificates rotated without automation.

In TLS 1.3 terms, mTLS certificate authentication is the optional half of the handshake: RFC 8446 says the client sends its certificate “if and only if the server has requested client authentication”, and a later message gives “explicit proof that an endpoint possesses the private key corresponding to its certificate.” Certificate authentication, explained without the PKI vocabulary: the caller proves it holds a private key that matches a certificate the server trusts, and when the server does the same in return, that is mutual certificate authentication. Here an authentication certificate means the digital kind, a client certificate, not the government document of the same name.

OAuth has a version too. RFC 8705 describes OAuth client authentication “and certificate-bound access and refresh tokens” using mutual TLS, and gives a protected resource a way to check that a token “was issued to the client presenting the token.” The fit verdict above is mine, and so are my certificate-based authentication best practices: give each caller its own certificate from an issuer you control for this purpose, automate rotation, and test the refusals.

The simplest certificate authentication example is a test against your own staging server. This is how to test client certificate authentication in four steps, using the --cert and --key options as the curl manual names them:

  1. 01 Present the certificate and its key: expect a normal response
  2. 02 Send the same request with no certificate: expect a refusal
  3. 03 Present an expired certificate: expect a refusal
  4. 04 Present a certificate from an issuer your server does not trust: expect a refusal
curl -i --cert client.pem --key client-key.pem https://staging.example.com/
curl -i https://staging.example.com/
curl -i --cert expired.pem --key expired-key.pem https://staging.example.com/
curl -i --cert other-issuer.pem --key other-issuer-key.pem https://staging.example.com/

The host and file names are placeholders, and -i shows the response headers so the status line lands in the output. Keep the four outputs with the date: they are the evidence.

A refusal can look two ways, and both pass. RFC 8446 lets a server that gets no certificate either “continue the handshake without client authentication” or “abort the handshake with a “certificate_required” alert”, and it allows the same choice for a chain it does not accept. An aborted handshake gives curl no HTTP response at all, only a TLS error; a continued one means your app has to answer with an HTTP error itself. The refusal message people search for, that a valid client certificate is required for authentication, depends on your server, so the test passes on any refusal rather than on one wording. It needs a TLS endpoint that you or your host can set to request client certificates; where the host terminates TLS and offers no such setting, the test cannot run there, in my reading.

Agents, MCP servers and token exchange: authenticating machine callers you did not write

An agent calling your API is a machine with a user behind it. It gets its own identity and a short-lived token scoped to that user and to what it may do, never the user’s session and never your service key. The MCP authorization specification builds this on OAuth.

The MCP auth spec is the MCP authorization specification, version 2026-07-28, and it opens with a condition: “Authorization is OPTIONAL for MCP implementations.” When it is supported, implementations on an HTTP-based transport “SHOULD conform to this specification”: authorization servers “MUST implement OAuth 2.1”, MCP servers “MUST implement OAuth 2.0 Protected Resource Metadata (RFC9728)” so clients can find the authorization server, and servers “MUST validate that access tokens were issued specifically for them as the intended audience” and “MUST NOT accept or transit any other tokens.” A server on the STDIO transport “SHOULD NOT follow this specification, and instead retrieve credentials from the environment.”

When a request has to go one hop further, RFC 8693 token exchange defines “how to request and obtain security tokens from OAuth 2.0 authorization servers, including security tokens employing impersonation and delegation”, so the next service can get a token of its own rather than the one the agent was handed. For MCP Inspector authentication testing, the MCP Inspector is “the reference developer tool for testing and debugging MCP servers”, and its browser, command-line and terminal clients share “the same OAuth state on disk”. I’d walk a server’s sign-in through it before any agent connects.

A2A authentication follows ordinary web practice: the A2A specification treats agents “as standard enterprise applications, relying on established web security practices”, and an A2A server “MUST authenticate every incoming request based on the provided credentials and its declared authentication requirements.”

Before an agent or MCP server may call your API, I want five things true. These are my MCP authentication best practices, and they hold for any agent, MCP or not:

  1. 01 It has its own identity, separate from any user and from your own service
  2. 02 Its token is scoped to one user and one job, and it expires
  3. 03 Every call it makes is logged with the agent's id
  4. 04 Destructive actions need a second factor or a person to approve them
  5. 05 The model output that becomes an API call is validated before it runs

The log entry is covered in audit logging best practices, the second factor in how to implement two factor authentication, and the case for validating model output in what is prompt injection. MCP from the Claude Code side, the credentials in .mcp.json and the OAuth scopes a server can request, is in Claude Code MCP. The other direction, an assistant you connect to your own accounts and why it should not sign in as you, is covered in giving an AI agent admin access.

How to check your own app: five rejection tests

Rejection tests prove the API turns away the wrong caller with five requests: no credential, a revoked key, an expired token, a token issued for another service, and a read-only key attempting a write. Keep each request and response with the date.

Handling authentication in an API well shows in how it fails, so run these against your own staging API, never someone else’s, with a real key or token for each case:

  1. 01 No credential at all: expect 401, and keep the response
  2. 02 A key you have revoked: expect 401, and keep the revocation time and the response
  3. 03 An expired token: expect 401, not a silent refresh, and keep the token's expiry claim and the response
  4. 04 A token issued for another service or audience: expect 401 or 403, and keep the token's audience and the response
  5. 05 A read-only key attempting a write: expect 403, and keep the key's scope and the response

The codes follow MDN’s guide: a server that receives invalid credentials “should respond with a 401 Unauthorized”, and one that receives valid credentials “that are inadequate to access a given resource” should answer with “the 403 Forbidden status code.” One test is missing on purpose: one customer’s key reading another customer’s record is object-level authorization, the first check in the API security checklist linked above. Its second check lists more token cases to try, such as malformed and tampered tokens, logout and password-reset invalidation, and account lockout.

In the Production Hardening Sprint, deliverable 1.3 is verified this way: we call protected actions directly as unauthorized and underprivileged users and confirm rejection. Deliverable 1.2 is verified this way: we confirm expired and revoked sessions cannot continue accessing protected resources.

Where the sprint does this

In the sprint, we enforce permissions on every protected route, API endpoint, and server action (deliverable 1.3), and verify expiry, refresh, and session revocation on logout and password change (deliverable 1.2). The results go in the production readiness report (deliverable 13.1), which accounts for all 123 IDs, keeps failures visible until resolved and explains genuine non-applicable items. New features that change the product’s core capabilities are separate work; the sprint includes only the supporting interfaces the listed controls need. Every deliverable is listed in the published scope.

Common questions about authenticating API callers

What are the best practices for API authentication?

Give each caller its own credential, scoped to what it may do and revocable on its own, send it only over TLS, and check authorization on the server on every request. That is my working rule, and the caller table near the top of this page shows which credential fits which caller.

Why not use Basic Auth?

Basic Auth sends a reusable password on every request, encoded rather than encrypted, and RFC 7617 says it “is not considered to be a secure method of user authentication unless used in conjunction with some external secure system such as TLS”. My rule: never for user sign-in, and for machines only over TLS with a credential you rotate.

What is mTLS vs TLS?

Plain TLS proves the server’s identity to the client; mTLS adds the other direction. RFC 8446 puts it as “The server side of the channel is always authenticated; the client side is optionally authenticated”, and mTLS is the case where the server asks for a client certificate and the client proves it holds the matching key.

How does M2M authentication work?

A machine proves its identity and gets a credential back. On a cloud platform that is short-lived credentials for an identity the platform manages; off-cloud it is an access token from an authorization server through OAuth 2.0 client credentials, a flow Okta’s guide ties to machine callers: “Typically, that means for machine-to-machine communication.” Where neither exists, a key or shared secret stands in, scoped and rotated.

What is the difference between an IAM role and a service role?

A service role is one kind of IAM role: in AWS’s words, “A service role is an IAM role that a service assumes to perform actions on your behalf.” An IAM role in general can delegate access to users, applications or services, and it hands out temporary credentials instead of a password or access keys.