What Is a Bearer Token? The Authorization Header Explained

Guilliano MolaireGuilliano Molaire15 min read

A bearer token is an access credential that grants access to whoever presents it, with no proof of a private key required. The IETF’s RFC 6750, “Bearer Token Usage” (2012) defines how OAuth 2.0 bearer tokens are sent, normally as an Authorization: Bearer header followed by the token, over HTTPS, and the token itself can be an opaque string or a JWT.

Here is such a request, with a placeholder where your token goes:

GET /v1/orders HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMifQ.c2lnbmF0dXJl

The same call with curl looks like this:

curl https://api.example.com/v1/orders \
  -H "Authorization: Bearer $ACCESS_TOKEN"

The rest of this post explains what “bearer” actually means, the exact rules RFC 6750 sets for sending the token, how an API should answer when the token is missing or wrong, and how bearer tokens relate to JWTs, refresh tokens, API keys and proof-of-possession tokens.

Key takeaways

  • “Bearer” describes how a token is used (possession is enough), while “JWT” describes a token format. A bearer token can be a JWT or an opaque string.
  • RFC 6750 section 2 allows three ways to send the token: the Authorization header, a form-encoded body parameter, and a URI query parameter that the spec says should not be used.
  • When a request has no usable token, the API answers with a WWW-Authenticate: Bearer challenge (RFC 6750 section 3). If a token was sent and rejected, the challenge can carry one of three error codes: invalid_request, invalid_token or insufficient_scope (RFC 6750 section 3.1).
  • Refresh tokens are not sent to APIs. They go only to the authorization server (RFC 6749 section 1.5).
  • If possession alone is too weak for your threat model, sender-constrained tokens (mTLS or DPoP) bind the token to a key the client holds.

What does “bearer” mean in a bearer token?

RFC 6750 defines a bearer token as a security token with the property that any party in possession of it can use it in any way that any other party in possession of it can, and it adds that using one does not require the bearer to prove possession of cryptographic key material (RFC 6750 section 1.2). In plain terms, the API does not check who is sending the token. It checks that the token is valid, and if it is, the request goes through.

A good everyday comparison is a cinema ticket. The usher checks that the ticket is real and for this screening, but never asks whether you are the person who bought it. That is convenient, because the client does not have to prove possession of any key, and it is also the main risk, because anyone who copies the token from a log file, a browser history or a proxy can use it for as long as it stays valid. RFC 6750’s security recommendations start from this risk: client implementations must ensure bearer tokens are not leaked to unintended parties (RFC 6750 section 5.3).

In OAuth 2.0, the token comes from an authorization server. The IETF’s RFC 6749, “The OAuth 2.0 Authorization Framework” (2012) defines how a client obtains an access token, and RFC 6750 defines how the client then presents it to a resource server, which is OAuth’s name for your API.

How do you send a bearer token? The three methods in RFC 6750

RFC 6750 gives clients three ways to send the token, and it says a client must not use more than one of them in the same request (RFC 6750 section 2).

1. The Authorization request header (use this one)

This is the method shown at the top of the post, and it is the one clients should use. Resource servers must support it (RFC 6750 section 2). The header value is the word Bearer, at least one space (the grammar allows one or more), and the token. The spec’s grammar for the token, called b64token, allows letters, digits and the characters -, ., _, ~, + and /, followed by optional = padding (RFC 6750 section 2.1). That is why a JWT, which uses base64url segments separated by dots, fits in the header without any extra encoding.

The IETF’s RFC 9110, “HTTP Semantics” (2022), says HTTP “uses a case-insensitive token to identify the authentication scheme” (RFC 9110 section 11.1), so a server should accept bearer as well as Bearer. On the client side, sending Bearer with a capital B matches the way RFC 6750 writes it.

2. A form-encoded body parameter

A client can instead put the token in the request body as a parameter named access_token, but only when every one of these conditions holds (RFC 6750 section 2.2):

  • the request’s Content-Type is application/x-www-form-urlencoded,
  • the body is single-part and contains only ASCII characters,
  • the HTTP method has defined body semantics, which rules out GET.
POST /v1/orders HTTP/1.1
Host: api.example.com
Content-Type: application/x-www-form-urlencoded

item=42&access_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMifQ.c2lnbmF0dXJl

The spec says this method should not be used unless the client cannot use the header, for example in an environment that cannot set request headers.

3. A URI query parameter (avoid it)

The third method puts the token in the URL, as ?access_token=.... RFC 6750 says it “SHOULD NOT be used unless it is impossible to transport the access token in the Authorization request header field or the HTTP request entity-body”, and that clients using it “SHOULD also send a Cache-Control header containing the ‘no-store’ option” (RFC 6750 section 2.3). The reason is that URLs end up in places headers do not. The spec’s security section warns that browsers, web servers and other software may not protect URLs well, so a token in a page URL can end up in browser history, web server logs and other data structures (RFC 6750 section 5.3). Since anyone holding the token can use it, a token in an access log is a token anyone with log access can use.

How should an API respond when a bearer token is missing or invalid?

When a request to a protected resource has no credentials, or has no access token that grants access to that resource, RFC 6750 says the resource server must include a WWW-Authenticate header with the Bearer scheme, and it can carry a realm, a scope, an error, an error_description and an error_uri (RFC 6750 section 3). This header is how a client learns why it was rejected without parsing your error body.

If the request had no credentials at all, the server should not include an error code (RFC 6750 section 3), so the response is just the challenge:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example"

If the client sent a token that the server will not accept, the server adds an error code:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="The access token expired"

RFC 6750 defines three error codes and the status each goes with (RFC 6750 section 3.1):

Error code HTTP status What it means What the client should do
invalid_request 400 Bad Request The request is malformed: a required parameter is missing, a parameter is repeated, or the token was sent in more than one way Fix the request; a new token will not help
invalid_token 401 Unauthorized The token is expired, revoked, malformed or invalid for another reason Get a new access token and retry
insufficient_scope 403 Forbidden The token is valid but does not carry the scope this resource needs Ask for a token with the scope named in the challenge’s scope attribute

The split between 401 and 403 is worth getting right. A 401 means “I do not accept this credential”, and for invalid_token RFC 6750 says the client may request a new access token and retry (RFC 6750 section 3.1). A 403 with insufficient_scope means the request requires higher privileges than the access token provides (RFC 6750 section 3.1), so fetching a token with the same scope again will fail the same way.

If you describe your API with OpenAPI, the Swagger documentation, “Bearer Authentication” (OpenAPI 3.0) shows the matching security scheme, and it says bearerFormat “is used mainly for documentation purposes, as a hint to the clients”:

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
security:
  - bearerAuth: []

Bearer token vs JWT: is a bearer token a JWT?

Not necessarily. “Bearer” describes how the token is used, and “JWT” describes what the token looks like inside, so the two answer different questions. RFC 6750 says nothing about the token’s format, and RFC 6749 describes an access token as a string that is “usually opaque to the client” (RFC 6749 section 1.4).

Two formats you will meet are:

  • Opaque tokens are strings whose content the API is not meant to read. The API does not learn anything from the token on its own, so it can ask the authorization server about it through token introspection (the IETF’s RFC 7662, “OAuth 2.0 Token Introspection” (2015)), which returns a JSON document with an active field and, when active, details such as scope, client and expiry.
  • JWTs are JSON Web Tokens (the IETF’s RFC 7519, “JSON Web Token (JWT)” (2015)). A JWT can be signed (a JSON Web Signature, or JWS) or encrypted (a JSON Web Encryption, or JWE). In the compact form, a JWT is a sequence of base64url-encoded parts separated by dots (RFC 7519 section 3); for a signed JWT those parts are a header, the claims and a signature, which the API can check with the issuer’s key before it reads the claims.

Either format can be used as a bearer access token. Keycloak’s documentation says an API can validate Keycloak access tokens locally using the public key of the issuing realm, looked up from the certificate endpoint with the key ID embedded in the JWS (Keycloak, “OpenID Connect layers”). For more on both approaches, see our posts on token introspection vs local JWT validation and JWT best practices for developers.

One practical side effect of JWT bearer tokens is size. A JWT carries its claims with it, so every role or group in the token makes the Authorization header longer, and the IETF’s RFC 6585, “Additional HTTP Status Codes” (2012), defines status 431 (Request Header Fields Too Large) for a server that refuses header fields that are too large (RFC 6585 section 5). See also our post on fixing Keycloak’s 431 “Request Header Fields Too Large”.

Are refresh tokens bearer tokens?

It is easy to think of refresh tokens as just another bearer token for your API, but that blurs an important line. A refresh token is a credential the client uses to get new access tokens, and RFC 6749 says refresh tokens are intended for use only with authorization servers and are never sent to resource servers (RFC 6749 section 1.5). So the token your API should expect in an Authorization: Bearer header is an access token, not a refresh token.

The ID token in OpenID Connect is different again. OpenID Connect Core 1.0 (OpenID Foundation, 2014) describes it as a security token that contains claims about the authentication of an end-user, and its audience must contain the client ID of the relying party (OpenID Connect Core 1.0 section 2), so it is meant for the client, not for your API. For more, see our posts on access tokens vs refresh tokens and ID tokens vs access tokens.

How do scope and audience limit a bearer token?

Because anyone holding a bearer token can use it, the safest token is one that can do very little. RFC 6750’s security recommendations include issuing short-lived bearer tokens (it suggests one hour or less) and issuing scoped bearer tokens that contain an audience restriction, so a token is only usable at the resource it was meant for (RFC 6750 section 5.3).

Two claims do this work in a JWT access token:

  • aud (audience) “identifies the recipients that the JWT is intended for”, and a recipient that does not find itself in it must reject the token (RFC 7519 section 4.1.3); otherwise a token issued for one service could be replayed against another. The IETF’s RFC 8707, “Resource Indicators for OAuth 2.0” (2020) adds a resource parameter that indicates the target service or resource to which access is being requested.
  • scope lists what the token is allowed to do at that API, such as orders:read. This is what an insufficient_scope error is about.

Audience answers “is this token meant for me?” and scope answers “is this token allowed to do this?”, and your API should check both on every request. For more, see our guide on OAuth scopes and the post on verifying a Keycloak-issued access token on the backend.

Identity management as a service, on open source

Skycloak does what Auth0 and Okta do, SSO, MFA, SCIM, audit logs and enterprise federation, on an open source core. Unlimited users and applications on every plan, no charge per monthly active user, and you can export and self-host whenever you want.

Bearer tokens vs proof-of-possession tokens (mTLS and DPoP)

The weakness of a bearer token is that it works for anyone. Sender-constrained tokens fix that by binding the token to a key the legitimate client holds, so a stolen token is useless without the key. There are two standard ways to do it:

  • Mutual TLS (mTLS) certificate-bound tokens (the IETF’s RFC 8705 (2020) section 3): when the client uses mutual TLS on its connection to the token endpoint, the authorization server can bind the token to the client’s certificate, either by putting the certificate’s SHA-256 thumbprint in a JWT access token (the x5t#S256 member of the cnf claim, section 3.1) or through token introspection. The API then accepts the token only over a mutual TLS connection that uses the same certificate. This binding is independent of client authentication: a public client can use a self-signed certificate for it (RFC 8705 section 4).
  • DPoP, Demonstrating Proof of Possession (the IETF’s RFC 9449, “OAuth 2.0 Demonstrating Proof of Possession (DPoP)” (2023)): the client signs a small proof JWT for each request with its own private key and sends it in a DPoP header (RFC 9449 section 4). The token is bound to the client’s public key, and the request uses the DPoP authorization scheme instead of Bearer (RFC 9449 section 7.1).

With DPoP, the request looks like this:

GET /v1/orders HTTP/1.1
Host: api.example.com
Authorization: DPoP eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMifQ.c2lnbmF0dXJl
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2In0.eyJodG0iOiJHRVQifQ.cHJvb2Y

Proof-of-possession takes more work, because every client needs a key pair and every API has to check the proof. It is worth considering for high-value APIs or for clients that run where tokens can leak. For public clients, Keycloak’s documentation recommends its DPoP preview feature, which it describes as binding an access token and a refresh token together with the public part of the client’s key pair (Keycloak, “OpenID Connect layers”). See also our post on DPoP with the Keycloak Admin API in Node.js.

Bearer tokens vs API keys

An API key is a different kind of credential. RFC 6750 is about OAuth access tokens: an authorization server issues them, it can give them a lifetime (the expires_in field of the token response, RFC 6749 section 5.1), and it can limit them by scope and audience. None of these RFCs defines API keys, so how a key is issued, how long it lasts and what it allows is set by each API. See also our post on API keys vs OAuth.

A brief Keycloak example: from token endpoint to 401

Here is the whole cycle with Keycloak as the authorization server, using the client credentials grant that a backend service would use. The token endpoint for a realm is /realms/{realm-name}/protocol/openid-connect/token (Keycloak, “OpenID Connect layers”). The example assumes a client with Client authentication turned on and Service account roles enabled, whose service account has the roles your API needs (Keycloak Server Administration Guide, “Using a service account”). It uses jq to save the returned token in ACCESS_TOKEN:

ACCESS_TOKEN=$(curl -s -X POST \
  https://auth.example.com/realms/myrealm/protocol/openid-connect/token \
  -d grant_type=client_credentials \
  -d client_id=orders-worker \
  -d client_secret="$CLIENT_SECRET" | jq -r .access_token)

The response is a JSON document (trimmed here) whose token_type gives the client the information it needs to use the token (RFC 6749 section 7.1). RFC 6749 section 5.1 lists access_token and token_type as required fields of a successful response:

{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIn0...",
  "expires_in": 300,
  "token_type": "Bearer",
  "scope": "profile email"
}

The client then puts the token in the header exactly as shown at the top of this post:

curl https://api.example.com/v1/orders \
  -H "Authorization: Bearer $ACCESS_TOKEN"

When the token expires, a well-behaved API answers with the challenge from RFC 6750, and the client can request a new token from the token endpoint and retry, as RFC 6750 section 3.1 allows for invalid_token:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="orders-api", error="invalid_token", error_description="The access token expired"

On the API side you have two choices for deciding whether to accept the token. You can validate the JWT locally against the realm’s public keys, published at /realms/{realm-name}/protocol/openid-connect/certs, or you can ask Keycloak through the introspection endpoint at /realms/{realm-name}/protocol/openid-connect/token/introspect, which only confidential clients can call (Keycloak, “OpenID Connect layers”). Keycloak’s documentation notes that introspection needs a network call to the Keycloak server and can become a bottleneck at scale, while local validation can cache the realm’s public key (Keycloak, “OpenID Connect layers”); in return, an introspection response’s active field reports whether the token is currently active (RFC 7662 section 2.2). On Skycloak, session management lets you view active sessions and terminate them remotely. For more on the two approaches, see our post on introspection vs local validation.

Bearer token security checklist

These come from RFC 6750’s own recommendations (section 5.3) plus the checks covered above:

  1. Send tokens only over HTTPS, and have clients validate the server’s TLS certificate chain.
  2. Use the Authorization header. Never put tokens in URLs.
  3. Keep access tokens short-lived and use refresh tokens, sent only to the authorization server, to get new ones.
  4. Restrict each token’s audience to the API that should accept it, and check aud and scope in the API.
  5. Do not store bearer tokens in cookies that can be sent in the clear.
  6. Keep tokens out of logs, error messages and analytics tools.
  7. Return the right challenge: 401 with invalid_token for a bad token, 403 with insufficient_scope for a token that lacks permission.
  8. Consider DPoP or mTLS for APIs where a leaked token would do real damage.

Frequently asked questions

What is the difference between Bearer and Basic authentication?

The IETF’s RFC 7617, “The Basic HTTP Authentication Scheme” (2015), has the client send a user ID and password, base64-encoded, in the Authorization header (RFC 7617 section 2). Bearer authentication sends a token that an authorization server issued, which can expire, carry limited scope and be revoked without changing anyone’s password. Both use the Authorization header, but with different scheme names.

Is a bearer token the same as an access token?

In OAuth 2.0, the access token is the credential, and “bearer” is the way it is used. An access token sent with the Bearer scheme is a bearer token, but an access token can also be sender-constrained with DPoP or mTLS, in which case it is not a bearer token.

Can I decode a bearer token?

If it is a signed JWT that is not also encrypted, yes: its header and claims are base64url-encoded parts (RFC 7519 section 3), so anyone who holds it can decode them, which is why RFC 7519 says privacy-sensitive claims need protection from disclosure (RFC 7519 section 12). An encrypted JWT (a JWE, RFC 7519 section 3) cannot be read without the key. Decoding is not the same as verifying, though, and your API must still check the signature. If the token is opaque, there is nothing to decode, and the API uses introspection instead.

Why does my API return 401 when I send a bearer token?

Start with the reasons RFC 6750 gives for invalid_token: the token is expired, revoked, malformed or invalid for other reasons (RFC 6750 section 3.1). Then compare the token’s aud and iss with what your API expects, check that the header is the word Bearer, a space and the token, and make sure you sent an access token rather than an ID token or refresh token. The error and error_description in the WWW-Authenticate header, if your API sends them, will narrow it down.

Should a bearer token go in the URL?

No. RFC 6750 discourages the query parameter method because URLs are logged and stored in browser history, and anyone who finds the token there can use it.

Next steps

If a request with a bearer token is failing and the token is a JWT, paste it into our JWT token analyzer, which decodes JWTs in your browser, and check its aud, scope and exp claims against what your API expects. Then send the curl example above against your API and read the WWW-Authenticate header on the response, which tells you whether the problem is the request, the token or its permissions.

Sources

Identity management as a service, on open source

Skycloak does what Auth0 and Okta do, SSO, MFA, SCIM, audit logs and enterprise federation, on an open source core. Unlimited users and applications on every plan, no charge per monthly active user, and you can export and self-host whenever you want.

Guilliano Molaire
Written by
Founder, Skycloak

Guilliano is the founder of Skycloak and a cloud infrastructure specialist with deep expertise in product development and scaling SaaS products. He discovered Keycloak while consulting on enterprise IAM and built Skycloak to make managed Keycloak accessible to teams of every size.

© 2026 Skycloak. All Rights Reserved. Design by Yasser SolimanKeycloak™ is a trademark of the Linux Foundation. Skycloak is not affiliated with the Keycloak project.