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

Test endpoints for every API authentication scheme

Basic, Digest, bearer tokens, API keys, HMAC signatures, JWT, OAuth 1.0a and OAuth 2.0 with PKCE, with credentials.

Examples last run against the API on

To test an API client's authentication without setting up an identity provider, use the public test endpoints at https://api.testingapis.com/api/authsign in to see. There's one endpoint per scheme: HTTP Basic, Digest, static bearer tokens, API keys in a header, query or cookie, and HMAC request signing. A JWT login has refresh tokens and an expired token for negative tests, an OAuth 2.0 server supports PKCE and client credentials, and an OAuth 1.0a server checks signed requests. The credentials are fixed and public, listed below and at GET /api/authsign in to see. Wrong credentials return a proper 401, and a valid token without the right role returns a 403.

Endpoints and credentials

SchemeEndpointCredentials
HTTP BasicGET /api/auth/basicsign in to seeapibee / password123
HTTP Digest (MD5, qop=auth)GET /api/auth/digestsign in to seeapibee / password123
Bearer tokenGET /api/auth/bearersign in to seeAuthorization: Bearer apibee-token-123
API key, headerGET /api/auth/api-key/headersign in to seeX-API-Key: apibee-key-123
API key, queryGET /api/auth/api-key/querysign in to see?api_key=apibee-key-123
API key, cookieGET /api/auth/api-key/cookiesign in to seeCookie: api_key=apibee-key-123
HMAC-SHA256 signaturePOST /api/auth/hmacsign in to seeSecret apibee-hmac-secret
Roles and scopesGET /api/auth/roles/adminsign in to seeadmin-token, user-token, readonly-token
JWTPOST /api/auth/jwt/loginsign in to seeadmin / admin123, user / user123
OAuth 2.0/api/auth/oauth/*sign in to seeapibee-client / apibee-secret, public client apibee-public
OAuth 1.0a/api/auth/oauth1/*sign in to seeConsumer apibee-consumer-key / apibee-consumer-secret, token apibee-access-token / apibee-token-secret

Basic and Digest

A missing or wrong password returns 401 with a WWW-Authenticate header, which is what makes browsers and HTTP libraries prompt or retry with credentials:

Terminal
$ curl -i -u apibee:password123 https://api.testingapis.com/api/auth/basic
HTTP/1.1 200 OK

{"authenticated":true,"scheme":"basic","user":"apibee"}

$ curl -i https://api.testingapis.com/api/auth/basic
HTTP/1.1 401 Unauthorized
www-authenticate: Basic realm="apibee", charset="UTF-8"

{"status":401,"error":"Unauthorized","message":"Missing or malformed Basic Authorization header."}
This example uses endpoints for signed-in members.Sign inCreate a free account

Digest takes two round trips. The first 401 carries a nonce, and the client answers with a hash of the password instead of the password itself. curl's --digest does both:

Terminal
$ curl -i --digest -u apibee:password123 https://api.testingapis.com/api/auth/digest
HTTP/1.1 401 Unauthorized
www-authenticate: Digest realm="apibee", qop="auth", algorithm=MD5, nonce="MTc5...", opaque="069d..."

HTTP/1.1 200 OK

{"authenticated":true,"scheme":"digest","user":"apibee","qop":"auth","nc":"00000001"}
This example uses endpoints for signed-in members.Sign inCreate a free account

To test a client that has to cope with servers hiding protected resources, /api/auth/basic-hidden/{user}/{pass}sign in to see returns 404 instead of 401 when the credentials don't match.

Bearer tokens and API keys

The three API key endpoints accept the same key in different places, so you can check that your client puts it where the API expects. A wrong bearer token returns WWW-Authenticate: Bearer error="invalid_token".

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

401 vs 403

401 means the server doesn't know who you are: no credentials, or bad ones. 403 means it knows exactly who you are, and you aren't allowed. A client should handle them differently. After a 401 it can log in again or refresh a token. After a 403, logging in again changes nothing. The roles endpoints return each one:

Terminal
$ curl -i https://api.testingapis.com/api/auth/roles/admin
HTTP/1.1 401 Unauthorized
www-authenticate: Bearer realm="apibee"

$ curl -i -H "Authorization: Bearer user-token" https://api.testingapis.com/api/auth/roles/admin
HTTP/1.1 403 Forbidden

{"status":403,"error":"Forbidden","message":"This endpoint requires the 'admin' role.","roles":["user"],"scopes":["read","write"]}

$ curl -H "Authorization: Bearer admin-token" https://api.testingapis.com/api/auth/roles/admin
{"authorized":true,"user":"Alice Admin","roles":["admin","user"],"scopes":["read","write","delete"]}
This example uses endpoints for signed-in members.Sign inCreate a free account

DELETE /api/auth/roles/resourcesign in to see checks a scope instead of a role. readonly-token only has read, so it gets a 403 that names the missing delete scope.

HMAC request signing

Payment and webhook APIs often sign requests instead of sending a secret. Here the signature is the lowercase hex HMAC-SHA256 of timestamp + "." + raw body, sent in X-Signature with the timestamp in X-Timestamp. Sign the exact bytes you send. Re-serialising the JSON after signing is the most common way to break it.

JavaScript
import { createHmac } from "node:crypto";

const secret = "apibee-hmac-secret";
const body = JSON.stringify({ orderId: 1001, amount: 49.99 });
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = createHmac("sha256", secret)
  .update(`${timestamp}.${body}`)
  .digest("hex");

const res = await fetch("https://api.testingapis.com/api/auth/hmac", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Timestamp": timestamp,
    "X-Signature": signature,
  },
  body,
});
console.log(res.status, await res.json());
This example uses endpoints for signed-in members.Sign inCreate a free account
Output
200 { authenticated: true, scheme: 'hmac-sha256', timestamp: 1791341858, bodyLength: 31 }

# The same signature sent with a different body:
401 {
  status: 401,
  error: 'Unauthorized',
  message: 'Signature mismatch.',
  hint: 'X-Signature = lowercase hex HMAC-SHA256(secret, X-Timestamp + "." + raw request body). Optional \'sha256=\' prefix.',
  bodyLength: 30
}

To check a signature by hand, the hash generator computes HMAC-SHA256 in the browser.

JWT login, expiry and refresh

/api/auth/jwt/loginsign in to see returns a signed access token that lasts 15 minutes and a refresh token. Refresh tokens rotate: each refresh returns a new one, and the old one stops working. /api/auth/jwt/expiredsign in to see hands out a correctly signed token that has already expired, so you can test the expiry path without waiting 15 minutes.

JavaScript
const BASE = "https://api.testingapis.com/api/auth/jwt";

const login = await fetch(`${BASE}/login`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ username: "user", password: "user123" }),
});
const tokens = await login.json();
// { accessToken, tokenType: "Bearer", expiresIn: 900, refreshToken, user }

const auth = { Authorization: `Bearer ${tokens.accessToken}` };
console.log((await fetch(`${BASE}/me`, { headers: auth })).status);    // 200
console.log((await fetch(`${BASE}/admin`, { headers: auth })).status); // 403: role is "user"

// A correctly signed token that expired an hour ago, for the negative test.
const { accessToken: expired } = await (await fetch(`${BASE}/expired`)).json();
const old = await fetch(`${BASE}/me`, { headers: { Authorization: `Bearer ${expired}` } });
console.log(old.status, old.headers.get("www-authenticate"));
// 401 Bearer error="invalid_token", error_description="The token expired at '...'"

// Refresh tokens rotate: use the new one next time.
const refreshed = await fetch(`${BASE}/refresh`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ refreshToken: tokens.refreshToken }),
});
console.log(refreshed.status); // 200
This example uses endpoints for signed-in members.Sign inCreate a free account

Paste any of these tokens into the JWT decoder to see their claims and expiry time.

OAuth 2.0 with PKCE

The OAuth server supports the authorization code grant with PKCE, client credentials, password and refresh token grants, plus introspection and revocation. Authorization is approved automatically as user apibee, so the whole flow runs in a script. Redirect URIs on localhost or 127.0.0.1(any port) and Postman's callback get a real 302 redirect.

Authorization code with PKCE

JavaScript
import { createHash, randomBytes } from "node:crypto";

const BASE = "https://api.testingapis.com/api/auth/oauth";
const redirectUri = "http://localhost:3000/callback";

// 1. A random code verifier and its S256 challenge.
const verifier = randomBytes(32).toString("base64url");
const challenge = createHash("sha256").update(verifier).digest("base64url");

// 2. Send the user to /authorize. This server approves at once and redirects
//    to the redirect URI with ?code=...&state=...
const authorize = new URL(`${BASE}/authorize`);
authorize.search = new URLSearchParams({
  response_type: "code",
  client_id: "apibee-public",
  redirect_uri: redirectUri,
  scope: "read profile",
  state: "xyz123",
  code_challenge: challenge,
  code_challenge_method: "S256",
});
const redirect = await fetch(authorize, { redirect: "manual" });
const code = new URL(redirect.headers.get("location")).searchParams.get("code");

// 3. Exchange the code and the verifier for tokens.
const tokenRes = await fetch(`${BASE}/token`, {
  method: "POST",
  body: new URLSearchParams({
    grant_type: "authorization_code",
    client_id: "apibee-public",
    code,
    redirect_uri: redirectUri,
    code_verifier: verifier,
  }),
});
const tokens = await tokenRes.json();

// 4. Call a protected endpoint.
const me = await fetch(`${BASE}/userinfo`, {
  headers: { Authorization: `Bearer ${tokens.access_token}` },
});
console.log(me.status, await me.json());
This example uses endpoints for signed-in members.Sign inCreate a free account
Output
200 {
  sub: 'apibee',
  name: 'TestingAPIs Tester',
  preferred_username: 'apibee',
  email: null,
  email_verified: null
}

The two failures a PKCE client should handle:

Text
# Sending the same code a second time
400 { error: 'invalid_grant', error_description: 'Authorization code is invalid, expired or already used.' }

# Sending a code_verifier that doesn't match the challenge
400 { error: 'invalid_grant', error_description: 'PKCE verification failed: code_verifier does not match code_challenge.' }

Client credentials

For service-to-service calls, with no user involved:

Terminal
$ curl -X POST https://api.testingapis.com/api/auth/oauth/token \
  -d "grant_type=client_credentials&client_id=apibee-client&client_secret=apibee-secret&scope=read"

{"access_token":"oat_3fc4...","token_type":"Bearer","expires_in":3600,"scope":"read"}
This example uses endpoints for signed-in members.Sign inCreate a free account

OAuth 1.0a signed requests

OAuth 1.0a signs every request with two secrets, the consumer secret and the token secret, instead of sending a token on its own. The test server accepts HMAC-SHA1, HMAC-SHA256, HMAC-SHA512 and PLAINTEXT signatures. Use the static access token apibee-access-token / apibee-token-secret, which never expires, or run the three-legged flow for a token of your own. Authorization is approved automatically as user apibee.

  1. POST /api/auth/oauth1/request_tokensign in to see, signed with the consumer key and secret only, with oauth_callback set to a URL or oob.
  2. GET /api/auth/oauth1/authorize?oauth_token=…sign in to see redirects to a localhost or Postman callback with oauth_verifier, and returns it as JSON for oob.
  3. POST /api/auth/oauth1/access_tokensign in to see, signed with the request token and its secret, plus oauth_verifier. The access token lasts an hour.
The protected resource, with the static access token:
1. New request: GET https://api.testingapis.com/api/auth/oauth1
2. Authorization → Type: OAuth 1.0
     Signature Method:  HMAC-SHA1
     Consumer Key:      apibee-consumer-key
     Consumer Secret:   apibee-consumer-secret
     Access Token:      apibee-access-token
     Token Secret:      apibee-token-secret
     Add authorization data to: Request Headers
3. Send. The response has "authenticated": true.

The three-legged flow, with the same Authorization settings:
1. POST https://api.testingapis.com/api/auth/oauth1/request_token
   Leave Access Token and Token Secret empty.
   Advanced → Callback URL: oob
2. Open GET https://api.testingapis.com/api/auth/oauth1/authorize?oauth_token=<oauth_token from step 1>
   and copy oauth_verifier from the JSON.
3. POST https://api.testingapis.com/api/auth/oauth1/access_token
     Access Token:  oauth_token from step 1
     Token Secret:  oauth_token_secret from step 1
     Advanced → Verifier: oauth_verifier from step 2
4. Use the oauth_token and oauth_token_secret it returns as
   Access Token and Token Secret for GET /api/auth/oauth1.

PLAINTEXT, in the cURL tab, needs no signing library: the signature is the signing key itself, consumer_secret&token_secret, with the & sent as %26. Use it to check the credentials, then switch to HMAC-SHA1 to test your signing code.

When a signature doesn't match, the 401 includes the signature base string the server built, so you can diff it against your client's. The failures worth testing:

Text
# Signed with the wrong token secret
401 signature_invalid: Signature mismatch.
details.signatureBaseString: GET&https%3A%2F%2Fapi.testingapis.com%2Fapi%2Fauth%2Foauth1&oauth_consumer_key%3D...

# The same oauth_nonce sent twice
401 nonce_used: This oauth_nonce was already used with this timestamp. Send a new nonce.

# The same request token exchanged twice
401 token_used: This request token was already exchanged for an access token.

The OAuth 1.0a signing rules cover where the parameters go, the timestamp window and every oauth_problem code.

Negative tests worth writing

  • No credentials: expect 401 and a WWW-Authenticate header.
  • Wrong password, wrong key, tampered token: 401, never 500.
  • Valid user, missing role or scope: 403, and the client does not log the user out.
  • Expired JWT: the client refreshes once, then retries the original request.
  • A refresh token used twice: the second use fails.
  • An OAuth code used twice, or with the wrong verifier: invalid_grant.
  • An HMAC signature over a different body: 401.
  • An OAuth 1.0a request with a reused nonce or a wrong signature: 401 nonce_used or signature_invalid.

The concepts behind these tests are in the authentication and authorization lesson.