How to check authorization header

Print what the request really carries: run curl -v -H 'Authorization: Bearer TOKEN' https://api.example.com/me and read the Authorization line curl echoes back. A 200 means the server accepted it. A 401 with www-authenticate: Bearer means the header was absent, malformed, or written in the wrong scheme. Anything after the scheme name is opaque to HTTP itself.

Why check this

Half of the tickets that read "the token is not working" are about a header that never left the client, or left it holding something other than what the author intended. Run this check whenever an API call fails with 401 and the credentials look correct, before filing against the backend, and on any client that builds the header from more than one source.

The concrete failure it prevents: a request helper that sets a default Authorization header and also passes user credentials, so the two collide and one of them wins without a warning. Step 4 shows that collision in curl, where a hand-written header silently replaces the one --user would have produced. The request looks authenticated, the server sees a scheme it was not expecting, and nothing in the client logs mentions a conflict.

The header has two parts, a scheme name and a credential, separated by one space. HTTP defines the first and treats the second as bytes. That is why a wrong scheme and a wrong token produce the same 401, and why reading the line is faster than reasoning about it.

Prerequisites

const http = require('http');
const USER = 'tester', PASS = 's3cret';
const PAGE = `<!doctype html><meta charset=utf-8><title>auth demo</title>
<script>fetch('/echo', { headers: { Authorization: 'Bearer demo-token-123' } })
  .then(r => r.text()).then(t => document.title = t.trim());</script>`;
http.createServer((req, res) => {
  const got = req.headers.authorization || '';
  if (req.url === '/') {
    res.writeHead(200, { 'content-type': 'text/html' });
    return res.end(PAGE);
  }
  if (req.url === '/echo') {
    res.writeHead(200, { 'content-type': 'text/plain' });
    return res.end('authorization: ' + (got || '(absent)') + '\n');
  }
  const want = 'Basic ' + Buffer.from(USER + ':' + PASS).toString('base64');
  if (got === want) {
    res.writeHead(200, { 'content-type': 'text/plain' });
    return res.end('ok, sent: ' + got + '\n');
  }
  res.writeHead(401, { 'www-authenticate': 'Basic realm="staging", charset="UTF-8"' });
  res.end('denied, sent: ' + (got || '(absent)') + '\n');
}).listen(8731, '127.0.0.1', () => console.log('listening on 8731'));

Steps

  1. Step 1.

    Ask a server you control to read the header back to you.

    curl -s -H 'Authorization: Bearer token-abc' http://127.0.0.1:8731/echo
    
    authorization: Bearer token-abc

    This is the only reading that settles an argument, because it comes from the receiving end.

  2. Step 2.

    Send the same shape to a real service and read the response.

    curl -s -v -H 'Authorization: Bearer h2check-demo-token' https://httpbin.org/bearer
    
    > GET /bearer HTTP/2
    > Host: httpbin.org
    > User-Agent: curl/8.21.0
    > Accept: */*
    > Authorization: Bearer h2check-demo-token
    >
    < HTTP/2 200
    < content-type: application/json
    < content-length: 62
    <
    {
    "authenticated": true,
    "token": "h2check-demo-token"
    }
  3. Step 3.

    Remove the header and read the challenge the server sends instead.

    curl -s -v https://httpbin.org/bearer
    
    > GET /bearer HTTP/2
    > User-Agent: curl/8.21.0
    > Accept: */*
    >
    < HTTP/2 401
    < content-length: 0
    < server: gunicorn/19.9.0
    < www-authenticate: Bearer
  4. Step 4.

    Give curl both a credential pair and a hand-written header, and see which one reaches the wire.

    curl -s -v --user tester:s3cret -H 'Authorization: Bearer token-abc' http://127.0.0.1:8731/echo
    
    > GET /echo HTTP/1.1
    > Host: 127.0.0.1:8731
    > User-Agent: curl/8.21.0
    > Accept: */*
    > Authorization: Bearer token-abc
    >
    authorization: Bearer token-abc

    The Basic credential is gone and there is no warning. Reversing the two options changes nothing.

  5. Step 5.

    Decode a Basic credential to see what the scheme hides, which is nothing.

    echo 'dGVzdGVyOnMzY3JldA==' | base64 -d
    
    tester:s3cret
  6. Step 6.

    Read the same header in the browser. Open http://127.0.0.1:8731/ with DevTools on the Network tab, click the echo row, then the Headers panel, and read Request Headers.

    { "__url": "http://127.0.0.1:8731/" }
    { "__url": "http://127.0.0.1:8731/echo",
    "Authorization": "Bearer demo-token-123" }
    title: authorization: Bearer demo-token-123

    The document request carries no Authorization. Only the fetch the page made has one.

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | Authorization: Bearer eyJ… | A JWT is being sent | Decode it before blaming the server. Expiry and algorithm are separate checks. | | Authorization: Basic … | A user name and password, base64 encoded | Step 5 recovers them. The route needs TLS. | | Authorization: Digest … | A challenge and response exchange | Two requests per call is normal. Read the digest procedure. | | No Authorization line at the receiving end | The header never left, or was stripped in transit | Compare the client transcript with the server echo. They disagree at the hop that removed it. | | 401 with www-authenticate: Bearer | The server wants a token and did not get a usable one | Missing, expired and malformed all look identical here. Decode what you sent. | | The value is Bearer with nothing after it | The client built the string from an empty variable | Fix the client. The server cannot tell this from a bad token. |

Common mistakes

Sign: A request helper sets a default Authorization header and the per-call credentials are ignored.Cause: A literal header wins over generated credentials and the loser is dropped silently. Step 4 reproduces it in curl: --user and -H together put only the -H value on the wire, in either order.
Sign: Two Authorization headers are set and the server acts on the wrong one.Cause: curl puts both lines on the wire when -H is given twice. The Node 22 server used here reported only the first, and other stacks reject the request or join the values. Which one wins is a property of the server, not of the request.
Sign: A token pasted from a log file is rejected although it looks identical.Cause: Loggers truncate long values with an ellipsis and some wrap them. Compare lengths, not appearances, and take the token from the network panel rather than from application output.
Sign: A client writes the header name in lowercase and expects its own credentials to be added alongside.Cause: Matching is case insensitive, so -H 'authorization: Bearer x' replaces what --user would send exactly as the capitalised form does. An empty value, -H 'Authorization:', removes the header rather than sending it empty, so a client built from an unset variable sends nothing at all.

What to check next

FAQ

How to check the authorization header in the browser?

Open DevTools, Network tab, click the request, then the Headers panel and the Request Headers section. A header the page set through fetch or XMLHttpRequest appears there. The document request itself carries none unless the browser was challenged first, which step 6 shows.

How to find a bearer token?

Read it from the Request Headers of a call the application already makes, as in step 6. Tokens also sit in localStorage, sessionStorage or a cookie, but the header is the only place that proves which value the client actually sent.

Basic token vs bearer token, what is the difference?

Basic carries a user name and password joined by a colon and base64 encoded, so it is reversible with one command. Bearer carries a credential the server issued, and whoever holds it can use it. Neither is encrypted by the scheme.

Does the header name have to be capitalised that way?

No. Header names are case insensitive, and HTTP/2 lowercases them on the wire. The echo in step 1 shows authorization because Node normalises what it receives, while curl printed Authorization for what it sent.

Why does the header appear after Accept with -H and before it with --user?

curl appends a hand-written header to the end of its list and inserts a generated one earlier. Compare the order in step 2 with the basic auth transcript. Order carries no meaning in HTTP, but it does tell you which option produced the line.

Verified

Verified by Maks Vernycurl 8.21.0node 22.23.2Chrome 152.0.7977.76

Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.

basic6 minpublished updated Maks Verny