mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 23:18:51 -07:00
[www] flag to disable websignups
This commit is contained in:
@@ -25,6 +25,7 @@ import {
|
||||
writeAuditLog,
|
||||
} from '@repo/domain'
|
||||
import {
|
||||
flagVar,
|
||||
intVar,
|
||||
logger,
|
||||
withCleanSpec,
|
||||
@@ -144,6 +145,20 @@ const DEFAULT_MAX_ACCOUNTS_PER_PLATFORM_ID = 3
|
||||
const DEFAULT_MAX_ACCOUNTS_PER_IP = 3
|
||||
const DEFAULT_MAX_ACCOUNTS_PER_DEVICE_ID = 3
|
||||
|
||||
/**
|
||||
* Whether a `create_account` with no platform — a password account, which is what the
|
||||
* website mints — is accepted at all (`PASSWORD_SIGNUP`; see context.ts). Off by default:
|
||||
* such an account has no identity but the address it came from, so the caps above are
|
||||
* the only thing limiting it, and a VPN or Tor exit resets the IP arm every N accounts.
|
||||
* That is how one player made 150 accounts in a weekend. A game-client signup always
|
||||
* carries a Steam or Meta identity and is untouched by this switch.
|
||||
*
|
||||
* Refused as `invalid_grant` with this description, which `www` translates for the form
|
||||
* (see its auth-messages.ts) — keep the two in step.
|
||||
*/
|
||||
const DEFAULT_PASSWORD_SIGNUP = false
|
||||
const PASSWORD_SIGNUP_DISABLED_DESCRIPTION = 'password signup is disabled'
|
||||
|
||||
/** New players start in the Orientation room (RoomId 13) — the new-user flow. */
|
||||
const ORIENTATION_ROOM_ID = 13
|
||||
/**
|
||||
@@ -617,7 +632,10 @@ const app = new Hono<App>()
|
||||
'',
|
||||
'**`create_account`** — mints a new account with an auto-assigned random username',
|
||||
'(players do not pick one initially) and places it in the Orientation room. A posted',
|
||||
'`password` becomes the login credential. Subject to three independent signup caps,',
|
||||
'`password` becomes the login credential. Without a `platform` this is a PASSWORD',
|
||||
'account (what the website makes), accepted only while `PASSWORD_SIGNUP` is on —',
|
||||
'off by default, refused as `invalid_grant` "password signup is disabled". Subject',
|
||||
'to three independent signup caps,',
|
||||
'per verified platform id, per signup IP and per client `device_id`',
|
||||
'(`MAX_ACCOUNTS_PER_PLATFORM_ID` / `MAX_ACCOUNTS_PER_IP` / `MAX_ACCOUNTS_PER_DEVICE_ID`;',
|
||||
'any disabled by setting it to 0). If it asserts a',
|
||||
@@ -762,7 +780,7 @@ const app = new Hono<App>()
|
||||
// - cached_login authenticates purely by platform identity.
|
||||
// - create_account that asserts a platform: we won't bind an identity we can't
|
||||
// prove. (create_account with NO platform is the password-account path —
|
||||
// allowed, but binds no platformId.)
|
||||
// binds no platformId, and is open only while PASSWORD_SIGNUP says so.)
|
||||
//
|
||||
// A password grant is NOT gated: the password already proved who it is. It posts
|
||||
// its platform proof too, and if that verifies we LINK the identity to the account
|
||||
@@ -851,6 +869,17 @@ const app = new Hono<App>()
|
||||
// via create_account or /account/me/changepassword.
|
||||
let accountId: string
|
||||
if (grantType === 'create_account') {
|
||||
// The password-account path (no platform) is a switch, off by default: see
|
||||
// DEFAULT_PASSWORD_SIGNUP. Checked first, before anything is read or minted —
|
||||
// a closed door costs no D1 round trip and burns no cap slot.
|
||||
if (!platformAsserted && !flagVar(c.env.PASSWORD_SIGNUP, DEFAULT_PASSWORD_SIGNUP)) {
|
||||
logger.info('signup refused: password signup is disabled', { ip: clientIp })
|
||||
return c.json(
|
||||
{ error: 'invalid_grant', error_description: PASSWORD_SIGNUP_DISABLED_DESCRIPTION },
|
||||
400
|
||||
)
|
||||
}
|
||||
|
||||
// A banned player's next move is a new account, so the evasion arms are checked
|
||||
// BEFORE one is minted — against the only identity a signup has, the IP it came
|
||||
// from and the platform identity it just proved. Refusing after the fact (as the
|
||||
|
||||
@@ -27,6 +27,16 @@ export type Env = SharedHonoEnv & {
|
||||
MAX_ACCOUNTS_PER_PLATFORM_ID?: string | number
|
||||
MAX_ACCOUNTS_PER_IP?: string | number
|
||||
MAX_ACCOUNTS_PER_DEVICE_ID?: string | number
|
||||
/**
|
||||
* Whether `create_account` accepts a signup with NO platform identity — the password
|
||||
* account the website makes (`www` `/api/signup`), or anything else that posts the
|
||||
* grant without a `platform`. `on`/`off` (or true/false, 1/0); unset means OFF, so a
|
||||
* server opens web signup deliberately. Read through `flagVar`. The `www` worker reads
|
||||
* the same knob to hide its form and refuse its endpoint, so the two agree for free.
|
||||
* Platform signups (Steam, Meta) are never governed by this — they always have an
|
||||
* identity to authenticate with later, and the caps above are what limit them.
|
||||
*/
|
||||
PASSWORD_SIGNUP?: string | number | boolean
|
||||
/**
|
||||
* Which linked arms a ban is enforced through, as a comma-separated list out of `ip`
|
||||
* and `platform` — or `off` for neither. Unset means BOTH: a ban reaches the accounts
|
||||
|
||||
@@ -1076,6 +1076,59 @@ describe('auth worker routes', () => {
|
||||
expect(login.status).toBe(200)
|
||||
})
|
||||
|
||||
test('PASSWORD_SIGNUP=off refuses a no-platform create_account and mints nothing', async () => {
|
||||
// The switch the website's signup hangs off. Every other create_account test here
|
||||
// runs with it ON (see vitest.config.ts); this one is the closed door.
|
||||
const original = env.PASSWORD_SIGNUP
|
||||
try {
|
||||
env.PASSWORD_SIGNUP = 'off'
|
||||
const before = await env.DB.prepare('SELECT COUNT(*) AS n FROM account').first<{
|
||||
n: number
|
||||
}>()
|
||||
const refused = await postToken(
|
||||
'grant_type=create_account&password=hunter2&device_id=dev-web&device_class=2',
|
||||
'198.51.100.60'
|
||||
)
|
||||
expect(refused.status).toBe(400)
|
||||
expect(refused.json.error).toBe('invalid_grant')
|
||||
expect(refused.json.error_description).toBe('password signup is disabled')
|
||||
const after = await env.DB.prepare('SELECT COUNT(*) AS n FROM account').first<{
|
||||
n: number
|
||||
}>()
|
||||
expect(after?.n).toBe(before?.n)
|
||||
|
||||
// Logging in is not what the switch gates: an existing password account still gets
|
||||
// its token, whatever the switch says.
|
||||
const login = await postToken(
|
||||
`grant_type=password&username=Player42&password=${LOGIN_PASSWORD}`,
|
||||
'198.51.100.60'
|
||||
)
|
||||
expect(login.status).toBe(200)
|
||||
|
||||
// A platform signup is a different door: it still reaches platform verification
|
||||
// (and fails THERE, on the missing ticket) rather than being refused by the switch.
|
||||
const steam = await postToken(
|
||||
'grant_type=create_account&platform=0&platform_id=76561198000000001',
|
||||
'198.51.100.60'
|
||||
)
|
||||
expect(steam.status).toBe(400)
|
||||
expect(steam.json.error_description).not.toBe('password signup is disabled')
|
||||
|
||||
// Unset is off too — the default an operator has to opt out of.
|
||||
delete env.PASSWORD_SIGNUP
|
||||
const unset = await postToken('grant_type=create_account&password=hunter2', '198.51.100.61')
|
||||
expect(unset.status).toBe(400)
|
||||
expect(unset.json.error_description).toBe('password signup is disabled')
|
||||
|
||||
// And `on` reopens it.
|
||||
env.PASSWORD_SIGNUP = 'on'
|
||||
const open = await postToken('grant_type=create_account&password=hunter2', '198.51.100.62')
|
||||
expect(open.status).toBe(200)
|
||||
} finally {
|
||||
env.PASSWORD_SIGNUP = original
|
||||
}
|
||||
})
|
||||
|
||||
test('the signup caps come from vars, and 0 disables an arm', async () => {
|
||||
// The cap an operator actually runs is the `MAX_ACCOUNTS_PER_IP` var; the constant in
|
||||
// auth.app.ts is only the fallback. `env` is shared by every test in this file, so the
|
||||
|
||||
@@ -8,6 +8,10 @@ export default defineConfig({
|
||||
miniflare: {
|
||||
bindings: {
|
||||
ENVIRONMENT: 'VITEST',
|
||||
// Password (no-platform) signup is OFF by default, and most create_account tests
|
||||
// post no `platform` — they'd all test the closed door. The switch itself is
|
||||
// covered by a test that flips it off and back.
|
||||
PASSWORD_SIGNUP: 'on',
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user