TestingAPIs is still being built. If something breaks, email support@testingapis.com.

Sessions, API keys and tokens

The three ways an API remembers who you are, and what each one costs when it leaks.

HTTP has no memory. Each request arrives at the server on its own, and nothing in the protocol connects it to the one before. So every request to a protected API has to carry its own proof of who sent it. There are three common ways to do that, and nearly every scheme in this tutorial is a variation on one of them: a session, a key, or a token.

The examples use the test endpoints on this API, which accept published demo credentials. The authentication test API lists all of them.

Sessions: the server remembers

With a session, you log in once. The server creates a record of the login, gives it a random ID, and sends the ID back in a cookie:

Terminal
$ curl -i -X POST https://api.testingapis.com/api/cookies/login \
  -H "Content-Type: application/json" \
  -d '{"username": "test", "password": "test123"}'

HTTP/1.1 200 OK
Set-Cookie: apibee_session=9afd521e3d6df756f6c2b5effc1ac0e1; max-age=3600; path=/; secure; samesite=lax; httponly

{"message":"Logged in. The session cookie is sent automatically on later requests.","username":"test","cookie":"apibee_session","expiresAt":"2026-10-11T11:19:59Z"}
This example uses endpoints for signed-in members.Sign inCreate a free account

The cookie value is just a random string. It means nothing on its own; the server looks it up in its session store to find out who you are. Browsers send it back automatically on every later request to the same site. From curl you send it yourself:

Terminal
$ curl https://api.testingapis.com/api/cookies/me \
  -H "Cookie: apibee_session=9afd521e3d6df756f6c2b5effc1ac0e1"

{"user":{"id":1,"username":"test","name":"Test User","email":"test@example.com","role":"user"},"session":{"expiresAt":"2026-10-11T11:19:59Z"}}
This example uses endpoints for signed-in members.Sign inCreate a free account

Because the session lives on the server, the server can end it. After logging out, the same cookie stops working, even though nothing about the cookie changed:

Terminal
$ curl -X POST https://api.testingapis.com/api/cookies/logout \
  -H "Cookie: apibee_session=9afd521e3d6df756f6c2b5effc1ac0e1"
{"message":"Logged out"}

$ curl -i https://api.testingapis.com/api/cookies/me \
  -H "Cookie: apibee_session=9afd521e3d6df756f6c2b5effc1ac0e1"
HTTP/1.1 401 Unauthorized

{"status":401,"error":"Unauthorized","message":"Not logged in. POST /api/cookies/login first to get the 'apibee_session' cookie."}
This example uses endpoints for signed-in members.Sign inCreate a free account

That replay test is one of the most useful in this lesson. A logout that only deletes the cookie in the browser, while the session stays alive on the server, leaves anyone who copied the cookie logged in. Always replay the old cookie after logout and expect a 401.

The cookie's attributes

The Set-Cookie header above carries more than the value. Each attribute is a test case:

AttributeWhat it doesWhat to check
HttpOnlyPage scripts can't read the cookiePresent on every session cookie, so a cross-site scripting bug can't steal it
SecureOnly sent over HTTPSPresent in production. This API sets it only over HTTPS
SameSite=LaxNot sent on most requests started by other sitesPresent; it's the main defence against cross-site request forgery
max-age=3600The browser drops it after an hourMatches the session length the server enforces
path=/Sent for every path on the siteNot wider than it needs to be

Sessions suit web apps, where a browser handles cookies for you. They suit mobile apps and server-to-server calls less well, and they bring their own attack, cross-site request forgery, covered in the last lesson.

API keys: a password for a program

An API key is a long secret string that identifies the calling application, not a person. It's usually created once in a dashboard and pasted into a config file, where it stays for months. The server checks it on every request. This API accepts the same key in three places, which covers most of what you'll meet in the wild:

Terminal
# In a header
curl https://api.testingapis.com/api/auth/api-key/header -H "X-API-Key: apibee-key-123"

# In the query string
curl "https://api.testingapis.com/api/auth/api-key/query?api_key=apibee-key-123"

# In a cookie
curl https://api.testingapis.com/api/auth/api-key/cookie -H "Cookie: api_key=apibee-key-123"
This example uses endpoints for signed-in members.Sign inCreate a free account

Send it from here with and without the right value:

GET/api/auth/api-key/header
This request uses an endpoint for signed-in members.Sign inCreate a free account
GET/api/auth/api-key/header
This request uses an endpoint for signed-in members.Sign inCreate a free account

Where the key travels matters more than it looks. A key in the query string is part of the URL, and URLs end up in server access logs, proxy logs, browser history and analytics tools. A header is the safest of the three. If an API accepts keys in the URL, check whether its own logs record them.

This API answers both a missing key and a wrong key with 401, which is what the HTTP specification intends: RFC 9110 uses 401 for credentials that are missing ordon't check out, and 403 for a caller the server recognises but won't allow. Plenty of APIs return 403 for a wrong key anyway. Either way, what you test is that the API is consistent and documented, because client code branches on these codes. Send the key in the wrong place, say in the header to the query endpoint, and the 401 message names the right place: Missing API key. Send it in the api_key query parameter.That's a good error.

Bearer tokens: whoever holds it

A bearer token goes in the Authorization header, after the word Bearer. The name comes from RFC 6750 and means exactly what it says: whoever bears the token gets in, no questions asked. Unlike an API key, a token is usually issued by a login or an OAuth server, expires after minutes or hours, and can be limited to certain actions.

GET/api/auth/bearer
This request uses an endpoint for signed-in members.Sign inCreate a free account

When the token is wrong, the server should say so in a standard header as well as in the body:

Terminal
$ curl -i https://api.testingapis.com/api/auth/bearer -H "Authorization: Bearer nope"
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="apibee", error="invalid_token", error_description="The access token is invalid"

{"status":401,"error":"Unauthorized","message":"Invalid bearer token."}
This example uses endpoints for signed-in members.Sign inCreate a free account

error="invalid_token" in WWW-Authenticate is the signal well-behaved clients use to decide to refresh or log in again. Tokens come in two kinds. Opaque tokens, like apibee-token-123here, are random strings the server looks up, much like a session ID. Self-contained tokens, usually JWTs, carry the user's details inside them, signed so they can't be changed. The JWT lesson takes one apart.

API key vs bearer token

People mix the two up because both are strings in a header. The difference is in who they stand for and how long they last:

API keyBearer token
IdentifiesAn application or accountUsually a user, through an application
Issued byA dashboard, onceA login or an OAuth server, on demand
LifetimeMonths or years, until someone rotates itMinutes to hours
LimitsOften all-or-nothingOften scoped to certain actions
If it leaksWorks until someone notices and revokes itWorks until it expires

Neither is better in general. API keys fit server-to-server integrations where no user is present. Bearer tokens fit apps acting for a signed-in person. What you test differs: for keys, revocation and rotation (does an old key stop working the moment it's rotated?); for tokens, expiry and scope.

Session vs token authentication

The deeper split is where the truth lives. A session is stateful: the server holds the record, so it can end the session instantly, but every request needs a lookup. A self-contained token is stateless: the server checks a signature and trusts what's inside, which is fast and needs no shared store, but the server can't easily take it back before it expires. A stolen session ID dies at logout. A stolen JWT keeps working until its expiry time.

That trade-off is why token systems pair short-lived access tokens with refresh tokens, the subject of lesson 4. For a tester, it decides the most important question about logout and password changes: does access stop at once, or only when the current token runs out? Find out which, then check the product's promises match.

What to test, by type

  • Sessions: replay the cookie after logout, after a password change, and after the stated expiry.
  • Sessions: the cookie has HttpOnly, Secure and a SameSite value.
  • API keys: missing, wrong, revoked, and in the wrong place, each with the documented status.
  • API keys: whether keys in URLs appear in logs you control.
  • Bearer tokens: missing, malformed, expired and revoked, each with a 401 and WWW-Authenticate.
  • All three: the error never says which part was wrong in a way that helps a guesser, such as "user exists, password wrong".

Exercise

Log in with POST /api/cookies/loginsign in to see as test / test123 and save the cookie. Call /api/cookies/mesign in to see with it. Log in a second time and call /mewith the first cookie again: does an older session survive a newer login? Then log out with the second cookie and test both. Write down which sessions are still alive, and say whether that's what you'd want from a banking app.

Practise and go further