Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/oauth2-readme-storage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@exortek/oauth2': patch
---

Document server storage, and correct the modules table.

The README never mentioned storage, so a reader following it deployed an
authorization server on in-memory stores and discovered the problem on the
second instance. It now covers the Redis-backed stores, which clients they
work with, and why memory is not a default to run behind a load balancer.

The modules table also named `mountOAuthLogin` as the fastify export — that
function is Express-only; the fastify subpath exports `oauthLogin` and
`oauthLoginPlugin`. The `./server` row listed three of its exports and omitted
the rest.
41 changes: 38 additions & 3 deletions packages/oauth2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,41 @@ challenge), PKCE, PAR (RFC 9126), resource indicators (RFC 8707), RAR
profile. Resource servers get `verifyDpopForResource` for the `ath` /
`cnf.jkt` check.

### Storage (read this before you run two instances)

`createServer` defaults every store to memory: authorization codes, PAR
request URIs, refresh families, device codes, and the DPoP replay cache. That
is fine for one process and wrong for two — a code issued on instance A cannot
be redeemed on instance B, and a DPoP proof replayed against a different
instance is not seen as a replay.

Pass Redis-backed stores for any deployment with more than one process:

```js
import {
createServer,
createRedisAuthCodeStore,
createRedisRefreshStore,
createRedisParStore,
createRedisDeviceStore,
} from '@exortek/oauth2/server';

const server = createServer({
/* … */
stores: {
authCode: createRedisAuthCodeStore(redis),
refresh: createRedisRefreshStore(redis),
par: createRedisParStore(redis),
device: createRedisDeviceStore(redis),
},
});
```

`redis` is your own client — `ioredis` or `node-redis`, both supported and
both covered by the integration suite. They are peer dependencies, so install
whichever you already use. Any object matching the store interface works too,
if you would rather not use Redis.

### OpenID Connect (opt-in)

Pass an `oidc` config and the server becomes an OpenID Provider: it issues a
Expand Down Expand Up @@ -202,10 +237,10 @@ const server = createServer({
| Subpath | Status | Purpose |
| -------------------------------- | ------ | ---------------------------------------------------------------------------- |
| `@exortek/oauth2` | ✅ | `createOAuth` RP flow, PKCE (RFC 7636), `state` / `nonce`, `OAuth2Error` |
| `@exortek/oauth2/express` | ✅ | `mountOAuthLogin` — Express login + callback middleware (web + api modes) |
| `@exortek/oauth2/fastify` | ✅ | `mountOAuthLogin` — Fastify login + callback plugin |
| `@exortek/oauth2/express` | ✅ | `oauthLogin` / `mountOAuthLogin` — Express login + callback (web + api modes) |
| `@exortek/oauth2/fastify` | ✅ | `oauthLogin` / `oauthLoginPlugin` — Fastify login + callback plugin |
| `@exortek/oauth2/providers/*` | ✅ | Pre-wired presets (google, github, microsoft, apple, okta, …) |
| `@exortek/oauth2/server` | ✅ | `createServer` + `jwtIssuer` / `pasetoIssuer` + `verifyDpopForResource` |
| `@exortek/oauth2/server` | ✅ | `createServer`, `oauth2Handlers`, `jwtIssuer` / `pasetoIssuer`, `defineClient` / `createClientRegistry`, `createIdTokenSigner`, `verifyDpopForResource`, the Redis stores, `ServerError` |
| `@exortek/oauth2/server/express` | ✅ | `mountOAuth2Server` — mount the AS on Express |
| `@exortek/oauth2/server/fastify` | ✅ | `oauth2ServerPlugin` — mount the AS on Fastify |

Expand Down
5 changes: 3 additions & 2 deletions packages/shared/src/redis-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
* can share a single detection path instead of copy-pasting the same
* three-way dispatch.
*
* Internal to `@exortek/shared` — consumed by `incr-store.js` and
* `record-store.js`, not re-exported to consuming packages.
* Consumed by `incr-store.js` and `record-store.js` here, and imported
* directly by the `oauth2`, `opaque` and `session` stores — so treat this
* module's surface as shared, not private to `@exortek/shared`.
*/

import { isFunction, isString } from './predicates.js';
Expand Down