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 request | Status | WWW-Authenticate | Body |
|---|---|---|---|
no Authorization header | 401 | Bearer realm="" | Unauthorized |
a header that is not Bearer <token> | 400 | Bearer error="invalid_request" | Bad Request |
| a well-formed token that does not match | 401 | Bearer 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.
GETrequests under/apirequire eitherREAD_TOKENorWRITE_TOKEN.POSTandDELETErequests under/apirequireWRITE_TOKEN./healthstays 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
/apianswers200: the guards are registered below the routes, so the routes answered first. Move them above. GET /healthasks for a token: a guard was registered withapp.use()and no path, or with'*'. Scope it to/api/*.POST /api/catswith the writer token is refused: the write guard'stokenis the reader's, or the array was given to the write guard too, which lets the reader write.bearerAuth()throws at startup withrequires options for "token" or "verifyToken": it was called with neither.
Remember
bearerAuth({ token })checksAuthorization: Bearer <token>;tokenmay be an array.- Three refusals: no header is
401 realm="", a malformed header is400 invalid_request, a wrong token is401 invalid_token. app.on(methods, path, guard)gives reads and writes different guards.verifyTokenlooks 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.