InteractiveFrameworks

Bearer tokens

Protect the API with tokens in the Authorization header: one token that may read, one that may also write, and the three different refusals a client can get.

What you'll learn

  • Guard routes with bearerAuth({ token }) and tell its three refusals apart: no header, a malformed header, a wrong token
  • Accept several tokens with an array, and give reads and writes different guards with app.on()
  • Know where a fixed token stops and a verified one begins: verifyToken, and the next lesson's JWT

Basic auth carries a password on every request, which is fine for a person at a browser and wrong for a program: a script, a mobile app or another service should hold a token that can be issued, scoped and revoked without anyone changing a password. The convention is the Authorization: Bearer <token> header, and Hono's bearerAuth middleware checks it. This lesson gives the cats API two tokens, one that may read and one that may also write, and looks closely at the three ways a request can be refused, because clients see those and need to tell them apart.

One token

import { bearerAuth } from 'hono/bearer-auth';

const token = 'honoiscool';

app.use('/api/*', bearerAuth({ token }));

app.get('/api/page', (c) => c.json({ message: 'You are authorized' }));

A request with Authorization: Bearer honoiscool reaches the handler. Any other request is answered by the middleware, and there are three distinct answers:

The requestStatusWWW-AuthenticateBody
no Authorization header401Bearer realm=""Unauthorized
a header that is not Bearer <token>400Bearer error="invalid_request"Bad Request
a well-formed token that does not match401Bearer error="invalid_token"Unauthorized

The second row is the one people do not expect. Authorization: Token abc, or a token with a character outside the allowed set (A-Z a-z 0-9 . _ ~ + / - and = padding), is not a failed login; it is a request the server cannot read, and 400 says so. The realm option fills the quoted name in the first row, and the prefix option changes the scheme word when an API uses something other than Bearer. Each of the three responses can be replaced through noAuthenticationHeader, invalidAuthenticationHeader and invalidToken, which take a message and a wwwAuthenticateHeader.

Several tokens, several permissions

token also takes an array, and any token in it is accepted. Different permissions are different guards, and app.on() registers a guard for some methods only:

const readToken = 'read';
const privilegedToken = 'read+write';

app.on('GET', '/api/page/*', bearerAuth({ token: [readToken, privilegedToken] }));
app.on(['POST', 'PUT', 'PATCH', 'DELETE'], '/api/page/*', bearerAuth({ token: privilegedToken }));

A GET with either token passes; a POST with the read token is refused as an invalid_token, since to that guard it is one. Both lines sit above the routes they protect, as any middleware does.

Verifying instead of comparing

A fixed token is a shared secret, and the middleware compares it in constant time, as basicAuth does. When tokens are issued per client and stored somewhere, verifyToken does the lookup:

app.use('/api/*', bearerAuth({ verifyToken: async (token, c) => token === 'dynamic-token' }));

It receives the token string and the context and returns whether to accept, so it can query a store. What it cannot do is know anything about the token's holder without that store: a bearer token is opaque. The next lesson's JSON Web Tokens carry their claims inside the token, signed, so the server can trust them without a lookup.

Your task

The API has a public GET /health and three routes under /api that anyone can call.

  1. GET requests under /api require either READ_TOKEN or WRITE_TOKEN.
  2. POST and DELETE requests under /api require WRITE_TOKEN.
  3. /health stays public.

Send GET /api/cats from the request bar three times: with no header, with Authorization: Token cat-reader, and with Authorization: Bearer cat-burglar. The three responses are the table above.

When it fails

  • Every request under /api answers 200: the guards are registered below the routes, so the routes answered first. Move them above.
  • GET /health asks for a token: a guard was registered with app.use() and no path, or with '*'. Scope it to /api/*.
  • POST /api/cats with the writer token is refused: the write guard's token is the reader's, or the array was given to the write guard too, which lets the reader write.
  • bearerAuth() throws at startup with requires options for "token" or "verifyToken": it was called with neither.

Remember

  • bearerAuth({ token }) checks Authorization: Bearer <token>; token may be an array.
  • Three refusals: no header is 401 realm="", a malformed header is 400 invalid_request, a wrong token is 401 invalid_token.
  • app.on(methods, path, guard) gives reads and writes different guards.
  • verifyToken looks a token up; a bearer token itself is opaque, unlike a JWT.
Stuck? Show a hint

bearerAuth({ token: [READ, WRITE] }) accepts either token; bearerAuth({ token: WRITE }) only the second. app.on('GET', '/api/*', guard) and app.on(['POST', 'DELETE'], '/api/*', guard) register a guard for some methods only. Register both above the routes.