Functional Weave
Code in TypeScript

auth.access-token@1.0.0

README.md

2,661 bytes · view raw

# auth.access-token

The access-token rules of a small API, so its request handlers are only I/O:

```
# login, after the password checked out
token = issueAccessToken(str(user.id), user.name, user.email, user.token_version,
                         random_jti, now, 3600, secret)
# -> {accessToken, tokenType: "Bearer", expiresAt: "2026-09-26T13:00:00Z", expiresIn: 3600}

# every authenticated request
check = readAccessToken(headers.get("Authorization"), secret, now, 30)
if check.ok:
    user = db.user(check.subject)                       # may be None
    check = confirmAccessToken(check, user and user.token_version, db.is_revoked(check.jti))
if not check.ok: answer 401            # check.error says why, for the log
```

It is a group because the three share one token layout: `issueAccessToken`
writes the claims `sub`, `name`, `email`, `ver`, `jti`, `iat` and `exp`,
signed HS256 with `auth.jwt`, and `readAccessToken` insists on finding `sub`,
`jti`, `ver` and `exp` in anything it accepts. A token that is validly signed
but lacks them (or has no `exp`, so would never expire) is `invalid_claims`.

**Revocation** takes two forms, and `confirmAccessToken` applies both once
the caller has done the lookups a pure function cannot:

- **Log out** revokes one token: store its `jti` (until its `expiresAt`, after
  which it is dead anyway) and pass `revoked = true` when it comes back.
- **Change password** revokes every token the user holds: increment the user's
  token version in the database. Every token carries the version it was issued
  at (`ver`), and one that no longer matches is refused.
- A user who no longer exists (`currentTokenVersion` null) is `user_not_found`.

A check that has already failed passes through `confirmAccessToken`
unchanged, so the three calls can be chained without branching.

Nothing here reads the clock or makes randomness: `now` is Unix seconds from
the caller and `jti` is random text from the caller (for instance
`secrets.token_urlsafe(16)`). `expiresAt` is written by `time.unix-to-iso`.
Errors from `readAccessToken` are `auth.jwt`'s codes plus `missing_token`
(no `Authorization: Bearer` header, read with `auth.bearer-token`); an API
should answer all of them with the same 401 and keep the code for its logs.

A secret shorter than 32 bytes, a negative leeway, an empty subject or jti,
a negative token version or a lifetime under one second are the caller's
mistakes and throw.

Sources: RFC 7519 sections 4.1.2 (sub), 4.1.4 (exp), 4.1.6 (iat), 4.1.7
(jti) (https://www.rfc-editor.org/rfc/rfc7519); RFC 6750 section 3, the
WWW-Authenticate response for 401 (https://www.rfc-editor.org/rfc/rfc6750).