move to JWT_SECRET

This commit is contained in:
Devin Zuczek
2026-07-06 16:20:05 -04:00
parent 631fefcfda
commit fbc1aedfcd
6 changed files with 32 additions and 13 deletions
+13 -2
View File
@@ -13,10 +13,21 @@ KV/D1/DO bindings yet.
| POST | `/connect/token` | OAuth token endpoint, issues a JWT |
| GET | `/role/developer/:id` | Developer role lookup (TODO) |
## Signing key
Tokens are signed HS256 with the `JWT_SECRET` binding (see `src/jwt.ts`). It's a
Cloudflare secret in deployed environments and read from `.dev.vars` locally
(gitignored) — never committed. `"keep_vars": true` in `wrangler.jsonc` keeps
deploys from clearing it.
Set the deployed secret once (persists across deploys):
```sh
bunx wrangler secret put JWT_SECRET
```
## Notes / TODO
- `JWT` is signed with a placeholder dev secret in `src/jwt.ts`. Move to a secret
binding before real use.
- `/eac/challenge` content is inlined in `src/auth.app.ts` (Workers have no
filesystem) — replace `EAC_CHALLENGE` with the real challenge text.
- `/cachedlogin/...` and the `RoomInstance` cleanup in `/connect/token` need a DB
+3 -6
View File
@@ -97,7 +97,7 @@ async function placeNewPlayerInOrientation(env: App['Bindings'], accountId: numb
async function authedId(c: Context<App>): Promise<number | null> {
const authHeader = c.req.header('Authorization') ?? ''
if (!authHeader.toLowerCase().startsWith('bearer ')) return null
const sub = await validateAndGetAccountId(authHeader.slice('Bearer '.length))
const sub = await validateAndGetAccountId(authHeader.slice('Bearer '.length), c.env.JWT_SECRET)
const id = sub ? Number.parseInt(sub, 10) : Number.NaN
return Number.isNaN(id) ? null : id
}
@@ -153,7 +153,7 @@ const app = new Hono<App>()
const account = await createAccount(c.env.DB, { platforms: platformInt || 0 })
accountId = String(account.accountId)
// Place the new player in Orientation (they don't matchmake into it).
await placeNewPlayerInOrientation(c.env, account.accountId)
//await placeNewPlayerInOrientation(c.env, account.accountId)
} else {
const posted = typeof body.account_id === 'string' ? body.account_id.trim() : ''
if (!/^\d+$/.test(posted)) {
@@ -165,10 +165,7 @@ const app = new Hono<App>()
accountId = posted
}
const accessToken = await generateToken(accountId, platformId, platform)
// TODO: also create the player's dorm on create_account, and remove any
// RoomInstance owned by accountId on login.
const accessToken = await generateToken(accountId, platformId, platform, c.env.JWT_SECRET)
return c.json({
access_token: accessToken,
+5
View File
@@ -9,6 +9,11 @@ export type Env = SharedHonoEnv & {
// the new player's presence is seeded to the Orientation room so the match
// heartbeat keeps them there instead of bouncing them to the dorm.
RECFLARE_MATCH_PRESENCE: KVNamespace
// HS256 signing key for issued access tokens. Set as a Cloudflare secret
// (`wrangler secret put JWT_SECRET`) in deployed environments and via `.dev.vars`
// locally — never committed. `keep_vars` in wrangler.jsonc stops deploys from
// clearing it.
JWT_SECRET: string
}
/** Variables can be extended */
+4 -5
View File
@@ -1,10 +1,9 @@
/**
* Minimal HS256 JWT generation.
*
* No real signing-key binding yet — uses a placeholder dev secret. Swap this for
* a secret binding (e.g. `c.env.JWT_SECRET`) before this is used for anything real.
* The signing key is supplied by the caller from the `JWT_SECRET` binding
* (a Cloudflare secret in deployed envs, `.dev.vars` locally) — see context.ts.
*/
const DEV_SECRET = 'dev-insecure-signing-key-change-me'
/** Token lifetime in seconds (mirrored in the `expires_in` response field). */
export const TOKEN_TTL_SECONDS = 3600
@@ -32,7 +31,7 @@ function base64urlToBytes(input: string): Uint8Array {
*/
export async function validateAndGetAccountId(
token: string,
secret: string = DEV_SECRET
secret: string
): Promise<string | null> {
const parts = token.split('.')
if (parts.length !== 3) return null
@@ -88,7 +87,7 @@ export async function generateToken(
accountId: string,
platformId: string,
platform: string,
secret: string = DEV_SECRET
secret: string
): Promise<string> {
const now = Math.floor(Date.now() / 1000)
const header = { alg: 'HS256', typ: 'JWT' }
+3
View File
@@ -8,6 +8,9 @@ export default defineConfig({
miniflare: {
bindings: {
ENVIRONMENT: 'VITEST',
// `.dev.vars` is gitignored, so provide a deterministic signing key
// for tests (and CI, which has no `.dev.vars`).
JWT_SECRET: 'test-signing-key',
},
},
}),
+4
View File
@@ -26,6 +26,10 @@
"id": "local"
}
],
// Preserve environment variables and secrets already set in Cloudflare (e.g.
// JWT_SECRET, managed via `wrangler secret put`) instead of clearing them on
// deploy — keeps the signing key out of source.
"keep_vars": true,
"logpush": false,
"upload_source_maps": true,
"observability": {