diff --git a/.changeset/phishing-resistant-only.md b/.changeset/phishing-resistant-only.md new file mode 100644 index 0000000..b2bd0d8 --- /dev/null +++ b/.changeset/phishing-resistant-only.md @@ -0,0 +1,5 @@ +--- +'@seamless-auth/types': minor +--- + +Add `phishing_resistant_only` to `SystemConfigSchema` (default `false`) and `SystemConfigPatchSchema`. When it is on, the API starts a session only from a passkey, and refuses email and phone codes, magic links, TOTP and OAuth whatever `login_methods` says. A code is still accepted once, to verify a new account's address before its first passkey is enrolled. diff --git a/package-lock.json b/package-lock.json index d11f214..bf2f0b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@seamless-auth/types", - "version": "0.22.0", + "version": "0.26.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@seamless-auth/types", - "version": "0.22.0", + "version": "0.26.0", "license": "Apache-2.0", "dependencies": { "zod": "^4.3.6" diff --git a/src/schemas/systemConfig/schema.test.ts b/src/schemas/systemConfig/schema.test.ts index d9e1694..0c12e57 100644 --- a/src/schemas/systemConfig/schema.test.ts +++ b/src/schemas/systemConfig/schema.test.ts @@ -181,6 +181,18 @@ describe('SystemConfigSchema', () => { ).toBe(true); }); + it('leaves phishing-resistant-only mode off so existing deployments are unaffected', () => { + expect(SystemConfigSchema.parse(baseConfig).phishing_resistant_only).toBe(false); + expect( + SystemConfigSchema.parse({ ...baseConfig, phishing_resistant_only: true }) + .phishing_resistant_only, + ).toBe(true); + expect(SystemConfigPatchSchema.safeParse({ phishing_resistant_only: true }).success).toBe(true); + expect(SystemConfigPatchSchema.safeParse({ phishing_resistant_only: 'yes' }).success).toBe( + false, + ); + }); + it('applies the default lockout policy', () => { const parsed = SystemConfigSchema.parse(baseConfig); diff --git a/src/schemas/systemConfig/schema.ts b/src/schemas/systemConfig/schema.ts index 02fd1b6..9eb991e 100644 --- a/src/schemas/systemConfig/schema.ts +++ b/src/schemas/systemConfig/schema.ts @@ -290,6 +290,14 @@ export const SystemConfigSchema = z.object({ * providers have their own `promptPasskeyEnrollment`. */ prompt_passkey_enrollment: z.boolean().default(false), + /** + * Phishing-resistant authentication only. Sign-in accepts a passkey and nothing + * else: email and phone codes, magic links, TOTP and OAuth are refused as ways to + * start a session, whatever `login_methods` and `passkey_login_fallback_enabled` + * say. A code is still accepted once, to verify the address of a new account before + * its first passkey is enrolled. + */ + phishing_resistant_only: z.boolean().default(false), oauth_providers: z.array(OAuthProviderConfigSchema).default([]), lockout_policy: LockoutPolicySchema.default(DefaultLockoutPolicy), // Derived from the field defaults rather than restated, so the two cannot drift. @@ -365,6 +373,7 @@ export const SystemConfigPatchSchema = z passkey_login_fallback_enabled: SystemConfigSchema.shape.passkey_login_fallback_enabled.optional(), prompt_passkey_enrollment: z.boolean().optional(), + phishing_resistant_only: z.boolean().optional(), oauth_providers: z.array(OAuthProviderConfigSchema).optional(), lockout_policy: LockoutPolicySchema.optional(), authenticator_policy: AuthenticatorPolicySchema.optional(),