A reusable bearer access token grants its holder defined access; sender-constrained tokens additionally require proof tied to the sender. Putting it in a URL exposes that capability to systems that routinely copy URLs: access logs, monitoring records, browser navigation history, bookmarks, and shared links. The exact exposure depends on the request and policy, but a secret-looking query parameter is not automatically protected by its name. Treat transport placement as part of the credential’s disclosure boundary.

For OAuth bearer-token requests, use the Authorization header. RFC 9700 now prohibits clients from passing access tokens in URI query parameters using RFC 6750’s query method. HTTPS remains essential: moving a value from the query to a header does not encrypt it, and TLS protects request data in transit to its termination point. After termination, applications and intermediaries can still inspect it. A header keeps the value out of the request URL, rather than granting immunity from logging or diagnostic capture.

Distinguish reduced exposure from secrecy

Some access logs record only method, request target, and status. In that format, a header token does not appear in the target, while a query token does. Other systems capture request headers, so Authorization needs explicit handling in log, trace, and error-reporting configuration. Verify what the actual pipeline stores instead of assuming that headers bypass every logging layer. Avoid exposing real credentials while performing that inspection.

Referrer-Policy can reduce onward source-URL disclosure, but does not remove a query already sent to the destination, erase browser records, or rewrite copied links. Likewise, Cache-Control: no-store is not a general instruction to purge access logs or navigation history. Moving a token to a fragment changes what is sent in an HTTP request, but still leaves a value accessible to page scripts and browser URL state; it is not a universal credential-storage solution.

When it breaks

A team may replace query authentication with a header and leave old token-bearing URLs in reports or shared bookmarks. The transport change does not undo that exposure. Investigate the affected copies and invalidate exposed reusable credentials where the system supports it. Another failure is a supposedly temporary link whose capability remains useful to anyone who obtains it during its validity period. Short lifetime reduces that window without making disclosure harmless.

Signed URLs can be an intentional design for narrowly scoped resource access when the chosen client flow requires them. Their allowed resource, operation, expiry, and handling need explicit review. Expiration is not the same as single use: preventing reuse before expiry requires a mechanism that actually enforces consumption. A webhook or browser navigation does not automatically justify putting an unrestricted reusable API credential into a query; choose an authentication flow suitable for the real client.

An OAuth query prohibition precedes query-target and header-target examples, separate URL-record surfaces despite suppressed Referer, a distinct signed-resource design, and the retained technical/browser hold.
Header placement reduces URL exposure; headers still need protection. Signed-resource URLs are a separately scoped design, not an OAuth exception. Runtime and technical checks remain blocked.

Check it quickly

Save this as token-placement.py, run python3 token-placement.py --serve, and open its printed page. The original article reports Python 3.14.4 and Chrome 152; this source review did not reproduce that run. Explicit Python checks also run under optimization. Every value is the literal synthetic marker DEMO-NOT-A-SECRET; never substitute a real credential. The loopback server is an observation fixture, not an authentication service. It records request targets and whether a demonstration Authorization header was present, without recording that header’s value. Python disables proxy discovery and rejects redirects; control browser proxy policy separately. Serve mode lasts 120 seconds, with --serve-seconds limited to 1–300. Control-C also requests cleanup. Only the separate cleanup result confirms that the server thread stopped and listener closed; bounded joins fail if shutdown remains unverified.

import argparse
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
from threading import Event, Thread
from urllib.parse import urlsplit
from urllib.request import HTTPRedirectHandler, ProxyHandler, Request, build_opener

parser = argparse.ArgumentParser()
parser.add_argument("--serve", action="store_true")
parser.add_argument("--serve-seconds", type=int, default=120)
args = parser.parse_args()
if not 1 <= args.serve_seconds <= 300:
    parser.error("--serve-seconds must be between 1 and 300")

def require(condition, message):
    if not condition:
        raise AssertionError(message)

# Synthetic demonstration value only; this server is not an authentication service.
demo = "DEMO-NOT-A-SECRET"
events = []


class NoRedirect(HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        raise RuntimeError("Unexpected redirect in local fixture")


opener = build_opener(ProxyHandler({}), NoRedirect())


class LocalServer(HTTPServer):
    def get_request(self):
        connection, address = super().get_request()
        connection.settimeout(2)
        return connection, address


class Handler(BaseHTTPRequestHandler):
    def log_message(self, *args):
        pass

    def do_GET(self):
        path = urlsplit(self.path).path
        if path == "/events":
            body = json.dumps(events).encode()
            kind = "application/json"
        elif path == "/echo":
            entry = {"request_target": self.path,
                     "authorization_present": self.headers.get("Authorization") == "Bearer " + demo,
                     "referer": self.headers.get("Referer")}
            events.append(entry)
            body = json.dumps(entry).encode()
            kind = "application/json"
        elif path == "/":
            events.append({"request_target": self.path, "document": True})
            body = ("""<!doctype html><meta charset="utf-8"><title>Token placement lab</title>
<h1>A clean referrer does not erase the request URL</h1><pre id="result">Running...</pre><script>
const demo=""" + json.dumps(demo) + """;
function require(condition, message) {
  if (!condition) throw new Error(message);
}
function verifyObservation(result, origin) {
  const queryTarget='/echo?access_token='+demo;
  require(new URL(result.page).origin===origin &&
          new URL(result.page).pathname==='/' &&
          new URL(result.page).search==='?access_token='+demo, 'page URL mismatch');
  require(result.query.request_target===queryTarget &&
          result.query.authorization_present===false, 'query placement mismatch');
  require(result.header.request_target==='/echo' &&
          result.header.authorization_present===true, 'header placement mismatch');
  require(result.query.referer===null && result.header.referer===null,
          'Referer was not suppressed');
  require(result.resourceUrls.includes(origin+queryTarget) &&
          result.resourceUrls.includes(origin+'/echo'), 'resource URLs missing');
  require(result.events.some(e=>e.document===true &&
          e.request_target==='/?access_token='+demo), 'document event missing');
  for (const expected of [result.query,result.header]) {
    require(result.events.some(e=>e.request_target===expected.request_target &&
            e.authorization_present===expected.authorization_present &&
            e.referer===null), 'matching server event missing');
  }
}
async function readJson(path, options={}) {
  const response=await fetch(path,{...options,redirect:'error',
                                  signal:AbortSignal.timeout(5000)});
  require(response.ok, 'HTTP status '+response.status);
  return response.json();
}
async function run() {
  require(location.protocol==='http:' && location.hostname==='127.0.0.1',
          'Use the printed loopback URL');
  performance.clearResourceTimings();
  const query=await readJson('/echo?access_token='+demo);
  const header=await readJson('/echo',{headers:{Authorization:'Bearer '+demo}});
  const deadline=performance.now()+5000;
  let resourceUrls=[];
  do {
    resourceUrls=performance.getEntriesByType('resource').map(e=>e.name)
      .filter(name=>new URL(name).pathname==='/echo');
    if (resourceUrls.length>=2) break;
    await new Promise(resolve=>setTimeout(resolve,25));
  } while (performance.now()<deadline);
  const result={page:location.href,query,header,resourceUrls,
                events:await readJson('/events')};
  verifyObservation(result,location.origin);
  window.labResult={status:'PASS',...result};
  document.getElementById('result').textContent=JSON.stringify(window.labResult,null,2);
}
run().catch(error=>{
  window.labError=String(error);
  window.labResult={status:'FAIL',error:window.labError};
  document.getElementById('result').textContent=JSON.stringify(window.labResult,null,2);
});
</script>""").encode()
            kind = "text/html; charset=utf-8"
        else:
            self.send_error(404)
            return
        self.send_response(200)
        self.send_header("Content-Type", kind)
        self.send_header("Content-Length", str(len(body)))
        self.send_header("Referrer-Policy", "no-referrer")
        self.send_header("Cache-Control", "no-store")
        self.end_headers()
        self.wfile.write(body)


def close_server(server, thread):
    stopper = None
    try:
        if thread.is_alive():
            stopper = Thread(target=server.shutdown, daemon=True)
            stopper.start()
            stopper.join(timeout=3)
        if thread.ident is not None:
            thread.join(timeout=3)
        require((stopper is None or not stopper.is_alive()) and not thread.is_alive(),
                "Server shutdown not verified")
    finally:
        server.server_close()
    require(server.socket.fileno() == -1, "Listener close not verified")
    print(json.dumps({"cleanup": "server thread stopped and listener closed"}), flush=True)


server = LocalServer(("127.0.0.1", 0), Handler)
thread = Thread(target=lambda: server.serve_forever(poll_interval=0.1), daemon=True)
origin = f"http://127.0.0.1:{server.server_port}"
try:
    thread.start()
    with opener.open(origin + "/echo?access_token=" + demo, timeout=5) as response:
        query = json.load(response)
    with opener.open(Request(origin + "/echo", headers={"Authorization": "Bearer " + demo}), timeout=5) as response:
        header = json.load(response)
    require(query["request_target"] == "/echo?access_token=" + demo and
            query["authorization_present"] is False, "query placement mismatch")
    require(header["request_target"] == "/echo" and
            header["authorization_present"] is True, "header placement mismatch")
    events.clear()
    print(json.dumps({"app_url": origin + "/?access_token=" + demo,
                      "wire_placement_checks": "PASS", "browser_checks": "NOT RUN BY PYTHON",
                      "serve_seconds": args.serve_seconds if args.serve else 0,
                      "real_credentials_used": False}), flush=True)
    if args.serve:
        Event().wait(args.serve_seconds)
except KeyboardInterrupt:
    pass
finally:
    close_server(server, thread)

The page requires the printed loopback address, then makes query and header requests with five-second abort signals. Its browser assertions require null received Referer values, exact request targets, both resource-timing URLs, the current page URL, and matching server events. Resource entries get a five-second polling deadline; browser suspension can delay timers. Only then does the page show PASS; missing or mismatched values show FAIL. The current page URL is narrower evidence than a persistent-history audit.

The article reports comparison with browser state and the server event list; no hash-bound browser trace was recovered here. Python checks only request-target placement and the demo-header match. The added browser assertions have not run here; Python output cannot certify them. Runtime and end-to-end technical acceptance remain blocked. The experiment does not test production authentication, TLS termination, log redaction, or signed-link consumption. Apply the observation to the actual client and logging system: keep reusable credentials out of URL surfaces where possible, deliberately protect the remaining credential-bearing fields, and verify the storage and sharing paths that continue after the request finishes.

Sources