mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 22:51:30 -07:00
move to JWT_SECRET
This commit is contained in:
+13
-2
@@ -13,10 +13,21 @@ KV/D1/DO bindings yet.
|
|||||||
| POST | `/connect/token` | OAuth token endpoint, issues a JWT |
|
| POST | `/connect/token` | OAuth token endpoint, issues a JWT |
|
||||||
| GET | `/role/developer/:id` | Developer role lookup (TODO) |
|
| 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
|
## 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
|
- `/eac/challenge` content is inlined in `src/auth.app.ts` (Workers have no
|
||||||
filesystem) — replace `EAC_CHALLENGE` with the real challenge text.
|
filesystem) — replace `EAC_CHALLENGE` with the real challenge text.
|
||||||
- `/cachedlogin/...` and the `RoomInstance` cleanup in `/connect/token` need a DB
|
- `/cachedlogin/...` and the `RoomInstance` cleanup in `/connect/token` need a DB
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ async function placeNewPlayerInOrientation(env: App['Bindings'], accountId: numb
|
|||||||
async function authedId(c: Context<App>): Promise<number | null> {
|
async function authedId(c: Context<App>): Promise<number | null> {
|
||||||
const authHeader = c.req.header('Authorization') ?? ''
|
const authHeader = c.req.header('Authorization') ?? ''
|
||||||
if (!authHeader.toLowerCase().startsWith('bearer ')) return null
|
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
|
const id = sub ? Number.parseInt(sub, 10) : Number.NaN
|
||||||
return Number.isNaN(id) ? null : id
|
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 })
|
const account = await createAccount(c.env.DB, { platforms: platformInt || 0 })
|
||||||
accountId = String(account.accountId)
|
accountId = String(account.accountId)
|
||||||
// Place the new player in Orientation (they don't matchmake into it).
|
// 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 {
|
} else {
|
||||||
const posted = typeof body.account_id === 'string' ? body.account_id.trim() : ''
|
const posted = typeof body.account_id === 'string' ? body.account_id.trim() : ''
|
||||||
if (!/^\d+$/.test(posted)) {
|
if (!/^\d+$/.test(posted)) {
|
||||||
@@ -165,10 +165,7 @@ const app = new Hono<App>()
|
|||||||
accountId = posted
|
accountId = posted
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessToken = await generateToken(accountId, platformId, platform)
|
const accessToken = await generateToken(accountId, platformId, platform, c.env.JWT_SECRET)
|
||||||
|
|
||||||
// TODO: also create the player's dorm on create_account, and remove any
|
|
||||||
// RoomInstance owned by accountId on login.
|
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
access_token: accessToken,
|
access_token: accessToken,
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ export type Env = SharedHonoEnv & {
|
|||||||
// the new player's presence is seeded to the Orientation room so the match
|
// 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.
|
// heartbeat keeps them there instead of bouncing them to the dorm.
|
||||||
RECFLARE_MATCH_PRESENCE: KVNamespace
|
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 */
|
/** Variables can be extended */
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* Minimal HS256 JWT generation.
|
* Minimal HS256 JWT generation.
|
||||||
*
|
*
|
||||||
* No real signing-key binding yet — uses a placeholder dev secret. Swap this for
|
* The signing key is supplied by the caller from the `JWT_SECRET` binding
|
||||||
* a secret binding (e.g. `c.env.JWT_SECRET`) before this is used for anything real.
|
* (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). */
|
/** Token lifetime in seconds (mirrored in the `expires_in` response field). */
|
||||||
export const TOKEN_TTL_SECONDS = 3600
|
export const TOKEN_TTL_SECONDS = 3600
|
||||||
@@ -32,7 +31,7 @@ function base64urlToBytes(input: string): Uint8Array {
|
|||||||
*/
|
*/
|
||||||
export async function validateAndGetAccountId(
|
export async function validateAndGetAccountId(
|
||||||
token: string,
|
token: string,
|
||||||
secret: string = DEV_SECRET
|
secret: string
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const parts = token.split('.')
|
const parts = token.split('.')
|
||||||
if (parts.length !== 3) return null
|
if (parts.length !== 3) return null
|
||||||
@@ -88,7 +87,7 @@ export async function generateToken(
|
|||||||
accountId: string,
|
accountId: string,
|
||||||
platformId: string,
|
platformId: string,
|
||||||
platform: string,
|
platform: string,
|
||||||
secret: string = DEV_SECRET
|
secret: string
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const now = Math.floor(Date.now() / 1000)
|
const now = Math.floor(Date.now() / 1000)
|
||||||
const header = { alg: 'HS256', typ: 'JWT' }
|
const header = { alg: 'HS256', typ: 'JWT' }
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ export default defineConfig({
|
|||||||
miniflare: {
|
miniflare: {
|
||||||
bindings: {
|
bindings: {
|
||||||
ENVIRONMENT: 'VITEST',
|
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',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -26,6 +26,10 @@
|
|||||||
"id": "local"
|
"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,
|
"logpush": false,
|
||||||
"upload_source_maps": true,
|
"upload_source_maps": true,
|
||||||
"observability": {
|
"observability": {
|
||||||
|
|||||||
Reference in New Issue
Block a user