Functional Weave
Code in TypeScript

auth.jwt@1.0.0

README.md

4,170 bytes · view raw

# auth.jwt

HS256 JSON Web Tokens: `signJwt` makes one, `verifyJwt` decides whether to
trust one, and `decodeJwt` reads one without the key. They are a group
because they share one token format and one JSON reader; install only what
each side needs (`only=decodeJwt` in a browser, which never holds the secret).

```
token  = signJwt({"sub": "42", "exp": now + 3600}, secret)
result = verifyJwt(token, secret, now, 30)   # {valid, claims, error, message}
claims = decodeJwt(token)                    # no key; null if malformed
```

The secret is bytes (a list of integers 0 to 255, as everywhere in the
registry; `utf8Encode` a text secret) and must be at least 32 bytes: RFC 7518
section 3.2 requires an HS256 key at least as long as the hash. A shorter one
is refused by both `signJwt` and `verifyJwt`, loudly, since it is a
configuration mistake rather than a bad token.

**verifyJwt never throws for a bad token.** It answers
`{valid: false, error, message}` with one of these codes, checked in this order:

| error | when |
|---|---|
| `malformed_token` | not three canonical base64url parts, a header or payload that is not a JSON object, not UTF-8, or JSON the three languages could read differently (below) |
| `unsupported_algorithm` | the header's `alg` is not exactly `HS256` (`none`, `RS256`, missing), or it has a `crit` header (RFC 7515 section 4.1.11 requires refusing extensions you do not understand) |
| `invalid_signature` | the HMAC-SHA256 of `header.payload` does not match, compared in constant time |
| `invalid_claims` | `exp`, `nbf` or `iat` is present but not a number |
| `token_expired` | `now >= exp + leeway` (RFC 7519 section 4.1.4: now must be *before* exp) |
| `token_not_yet_valid` | `now + leeway < nbf` (section 4.1.5) |
| `token_issued_in_future` | `iat > now + leeway` |

The algorithm is checked before any key is used, so the classic attacks
(`alg: none`, or an RS256 public key used as an HMAC secret) cannot reach the
signature check. `now` is Unix seconds read by the caller; the capability
never reads the clock. `exp`, `nbf` and `iat` are each optional here: a token
without `exp` never expires, so a caller that requires one should check the
claims (`auth.access-token` does). `iss`, `aud`, `sub` and `jti` are returned,
not judged.

**signJwt** writes the header `{"alg":"HS256","typ":"JWT"}` and the claims as
one canonical JSON text: no whitespace, object keys sorted by code point at
every level, non-ASCII written as UTF-8 rather than `\u` escapes, and the
usual JSON escapes for quotes, backslashes and control characters. Equal
claims therefore sign to byte-identical tokens in every language. Numbers must
be whole and within ±(2^53 − 1), the integers JavaScript holds exactly;
NumericDates (`exp`, `iat`) are whole seconds anyway. It does not add any
claim itself.

**decodeJwt** is for a client reading its own token (to show a name, or to
refresh before `exp`). It checks the shape only; anyone can make a token that
decodes. Never use it to decide access.

**One reading of JSON.** A token is attacker-controlled input, and three JSON
parsers disagree at the edges, so each language refuses the same things:
integers beyond 2^53, numbers that overflow to infinity, `NaN`, an escaped
lone surrogate (`"\ud800"`), nesting deeper than 32, and anything that is not
strict RFC 8259 JSON. A repeated key keeps its last value, as JavaScript and
Python both do. Base64url segments must be canonical and unpadded (RFC 7515
section 2).

The RFC 7515 appendix A.1 token (also RFC 7519 section 3.1) is among the
vectors, verified and decoded with the RFC's key. Its header has a line break
and `typ` first, which is why `signJwt` of the same claims gives a different,
canonical token: a JWT is verified as sent, never re-serialised.

Sources: RFC 7519, JSON Web Token (https://www.rfc-editor.org/rfc/rfc7519);
RFC 7515, JSON Web Signature, section 7.1 and appendix A.1
(https://www.rfc-editor.org/rfc/rfc7515); RFC 7518, JSON Web Algorithms,
section 3.2 (https://www.rfc-editor.org/rfc/rfc7518#section-3.2); RFC 8725,
JSON Web Token Best Current Practices, sections 2.1 and 3.1
(https://www.rfc-editor.org/rfc/rfc8725).