{"playbook":"Accounts","url":"https://agentwranglers.se/playbook/accounts","updated":"2026-10-21","license":"https://creativecommons.org/licenses/by/4.0/","credit":"Required by the licence: credit Agent Wranglers with a link to the rule or playbook you used.","how":"Each rule's How text is on its own page: add .md to its url.","rules":[{"chapter":"Sign-up","name":"Ask only for what the first task needs","level":"Recommended","applies":["accounts"],"rule":"Ask at sign-up only for the fields the first task needs, usually an email address or a passkey. Ask for anything else when a feature needs it.","check":"A test reads the sign-up form's fields and compares them with the written list. It fails when a field has no feature next to it.","url":"https://agentwranglers.se/playbook/accounts/sign-up/minimal-sign-up","related":["/playbook/website/accessibility/accessible-authentication","/playbook/website/privacy/data-minimization"],"sources":[{"label":"GDPR Article 5(1)(c) on data minimisation","url":"https://eur-lex.europa.eu/eli/reg/2016/679/oj"}]},{"chapter":"Sign-up","name":"Prove the email address before trusting it","level":"Required","risk":"takeover","applies":["accounts"],"rule":"Mark an email address as verified only after someone opens a link or types a code sent to it. Until then, send no account data there and never use it to link accounts.","check":"Sign up with a test address and skip the link. Invite a colleague, export data and link a provider login with the same address. Each is refused. Open the link, and all three work.","url":"https://agentwranglers.se/playbook/accounts/sign-up/email-verification","related":["/playbook/email/deliverability/confirmed-opt-in","/playbook/email/email-security/sign-in-links"],"sources":[]},{"chapter":"Sign-up","name":"Stop strangers from claiming an address before its owner","level":"Required","risk":"takeover","applies":["accounts"],"rule":"Keep an unverified sign-up pending. When the owner proves the address another way, such as through a provider, drop the pending password, passkeys and sessions instead of merging them.","check":"Sign up with a test address and a password, without verifying. Then sign in through a provider that verified the same address. The first password no longer works, and the first browser's session is gone.","url":"https://agentwranglers.se/playbook/accounts/sign-up/pre-hijacking","related":["/playbook/accounts/sign-in-methods/last-sign-in-method","/playbook/adversarial-testing/abuse-and-limits/no-account-enumeration","/playbook/accounts/sign-in-methods/link-provider-login"],"sources":[]},{"chapter":"Sign-up","name":"Compare email addresses and usernames in one canonical form","level":"Required","risk":"takeover","applies":["accounts"],"rule":"Store each email address and username as typed, plus one canonical key. Look up, compare and enforce uniqueness on the key only.","check":"Sign up as `Anna@Example.com`, then as ` anna@EXAMPLE.com`. The second finds the first account. Sign up as `anna+shop@example.com`, and it creates a separate account.","url":"https://agentwranglers.se/playbook/accounts/sign-up/identifier-normalisation","related":["/playbook/email/message-basics/international-addresses"],"sources":[{"label":"Unicode UTS #46 IDNA Compatibility Processing","url":"https://www.unicode.org/reports/tr46/"},{"label":"Unicode UAX #15 Normalization Forms","url":"https://www.unicode.org/reports/tr15/"}]},{"chapter":"Sign-up","name":"Reserve names that look official","level":"Recommended","applies":["accounts"],"rule":"Refuse usernames, organisation names and subdomains that pose as staff or system pages, or that look like another existing name.","check":"Try to register `Admin`, `support`, `ѕupport` with a Cyrillic \"ѕ\", and a look-alike of an existing name. Each is refused.","url":"https://agentwranglers.se/playbook/accounts/sign-up/reserved-names","related":["/playbook/email/unsubscribe-and-bounces/postmaster-abuse","/playbook/accounts/profile-and-account-lifecycle/username-change"],"sources":[{"label":"Unicode UTS #39 Security Mechanisms","url":"https://www.unicode.org/reports/tr39/"}]},{"chapter":"Sign-in methods","name":"Decide whether a provider or a library runs sign-in","level":"Recommended","applies":["accounts"],"rule":"Choose a hosted identity provider or a maintained library on purpose, write down why, and never write your own password, token or protocol code.","check":"Search the code for hand-made password hashing, token signing or SAML parsing, such as `createHmac` or `hashlib.sha256` near the word password. Nothing appears outside the chosen library. The decision log names the choice and the export path.","url":"https://agentwranglers.se/playbook/accounts/sign-in-methods/auth-provider-or-library","related":["/playbook/adversarial-testing/access-and-accounts/password-storage","/playbook/stack/product-services/keycloak-self-hosted","/playbook/stack/product-services/no-own-auth-code","/playbook/stack/product-services/sign-in-library"],"sources":[]},{"chapter":"Sign-in methods","name":"Keep your own user ID","level":"Recommended","applies":["accounts"],"rule":"Give every person your own stable user ID and map each sign-in method to it. Never key records by an email address or a provider's user ID.","check":"Search the schema for foreign keys to an email column or a provider subject. None exist. Change a test user's email, and every record stays linked.","url":"https://agentwranglers.se/playbook/accounts/sign-in-methods/own-user-id","related":["/playbook/data/schema-design/stable-keys","/playbook/accounts/sign-in-methods/provider-sign-in","/playbook/accounts/business-sign-on-and-provisioning/organisation-sso"],"sources":[{"label":"RFC 9562 Universally Unique Identifiers (UUIDs)","url":"https://www.rfc-editor.org/rfc/rfc9562"}]},{"chapter":"Sign-in methods","name":"Offer passkeys first","level":"Recommended","applies":["accounts"],"rule":"Offer passkeys as the main way to sign in, through autofill. Invite people to create one right after they sign in another way.","check":"In a browser test with a virtual authenticator, such as Playwright over the Chrome DevTools Protocol, register a passkey and sign in through autofill. Both succeed. The stored user handle holds no email or name.","url":"https://agentwranglers.se/playbook/accounts/sign-in-methods/passkeys-first","related":["/playbook/website/standard-addresses/passkey-endpoints","/playbook/stack/product-services/passkey-library"],"sources":[{"label":"W3C Web Authentication Level 3","url":"https://www.w3.org/TR/webauthn-3/"},{"label":"HTML Standard on autofill","url":"https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill"}]},{"chapter":"Sign-in methods","name":"Verify every passkey answer on the server","level":"Required","risk":"takeover","applies":["accounts"],"rule":"Accept a passkey sign-in only after the server checks the challenge, origin, relying party ID, user flags and signature with a maintained WebAuthn library.","check":"Tests replay a captured sign-in response, send one with a changed origin and send one signed for another account's credential ID. Each is refused, and a fresh response is accepted once.","url":"https://agentwranglers.se/playbook/accounts/sign-in-methods/passkey-verification","related":["/playbook/website/standard-addresses/webauthn","/playbook/stack/product-services/passkey-library"],"sources":[{"label":"WebAuthn Level 3 on verifying an authentication assertion","url":"https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion"}]},{"chapter":"Sign-in methods","name":"Keep the password manager's passkey list current","level":"Optional","applies":["accounts"],"rule":"Tell the browser when a passkey stops working or a user's name changes, with the WebAuthn signal methods, so people stop picking dead passkeys.","check":"A browser test deletes a passkey in settings and spies on `signalAllAcceptedCredentials`. It is called once, with every remaining ID and without the deleted one.","url":"https://agentwranglers.se/playbook/accounts/sign-in-methods/passkey-signals","related":["/playbook/website/standard-addresses/passkey-endpoints"],"sources":[{"label":"WebAuthn Level 3 on signal methods","url":"https://www.w3.org/TR/webauthn-3/#sctn-signal-methods"}]},{"chapter":"Sign-in methods","name":"Let people sign in with an account they already have","level":"Recommended","applies":["accounts"],"rule":"Run sign-in with Google, Apple, GitHub or Microsoft through OpenID Connect, using the authorization code flow with PKCE, `state` and `nonce`, and a maintained library.","check":"Search the code for `response_type=token` and for hand-made JWT parsing in the sign-in code. Neither appears. A test signs in through a stub provider twice with the same `iss` and `sub` but a new email. Both sign-ins reach the same user ID.","url":"https://agentwranglers.se/playbook/accounts/sign-in-methods/provider-sign-in","related":["/playbook/accounts/sign-in-methods/own-user-id","/playbook/adversarial-testing/access-and-accounts/single-sign-on"],"sources":[{"label":"OpenID Connect Core 1.0 on ID token validation","url":"https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation"},{"label":"RFC 9700 Best Current Practice for OAuth 2.0 Security","url":"https://www.rfc-editor.org/rfc/rfc9700"}]},{"chapter":"Sign-in methods","name":"Link a provider login only to an account its owner controls","level":"Required","risk":"takeover","applies":["accounts"],"rule":"Never join a provider login to an existing account because the email addresses match. Link it after the person signs in to that account, or under the trusted-issuer exception below.","check":"On staging, sign in through a provider not on the trusted list, with the email of an existing account. No link is made and the sign-in page asks for the existing account. Repeat with `email_verified` false from a trusted issuer. No link is made either.","url":"https://agentwranglers.se/playbook/accounts/sign-in-methods/link-provider-login","related":["/playbook/accounts/sign-up/pre-hijacking","/playbook/accounts/sign-in-methods/last-sign-in-method"],"sources":[{"label":"OpenID Connect Core 1.0 on claim stability","url":"https://openid.net/specs/openid-connect-core-1_0.html#ClaimStability"},{"label":"OpenID Connect Core 1.0 on standard claims","url":"https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims"}]},{"chapter":"Sign-in methods","name":"Accept long passwords and refuse breached ones","level":"Recommended","applies":["accounts"],"rule":"When you offer passwords, set no rules on character types and never force changes on a schedule. Allow password managers and paste, and refuse passwords on a blocklist.","check":"A test sets `passwordpassword` and the account's own username as passwords. Both are refused with a reason. A 64-character password with no digits or symbols is accepted. A browser test pastes into the password field, and the value arrives.","url":"https://agentwranglers.se/playbook/accounts/sign-in-methods/password-rules","related":["/playbook/adversarial-testing/access-and-accounts/login-guessing","/playbook/adversarial-testing/access-and-accounts/password-storage"],"sources":[{"label":"NIST SP 800-63B-4 on password verifiers","url":"https://pages.nist.gov/800-63-4/sp800-63b.html#passwordver"},{"label":"OWASP Authentication Cheat Sheet","url":"https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html"}]},{"chapter":"Sign-in methods","name":"Weigh email links before making them the only way in","level":"Recommended","applies":["accounts"],"rule":"Use email links or codes as the only sign-in for low-risk accounts only. Bind each link to the browser that asked for it, and offer a passkey after the first sign-in.","check":"Ask for a link in one browser and open it in another. The second browser is not signed in and shows a code. Typing the code in the first browser signs it in.","url":"https://agentwranglers.se/playbook/accounts/sign-in-methods/email-sign-in-trade-offs","related":["/playbook/email/email-security/sign-in-links"],"sources":[{"label":"NIST SP 800-63B-4 on out-of-band authenticators","url":"https://pages.nist.gov/800-63-4/sp800-63b.html#ooba"}]},{"chapter":"Sign-in methods","name":"Sign in devices without a keyboard through a second screen","level":"Optional","applies":["accounts"],"rule":"For TVs, command-line tools and other devices without a handy browser, use the OAuth 2.0 Device Authorization Grant from RFC 8628. Never ask for a password on the device.","check":"A test starts a device sign-in, polls before the interval and gets `slow_down`. After the person confirms on the second screen, the next poll returns tokens. A wrong user code entered 5 times in a row is blocked.","url":"https://agentwranglers.se/playbook/accounts/sign-in-methods/device-sign-in","related":["/playbook/accounts/sessions-and-security-activity/session-list"],"sources":[{"label":"RFC 8628 OAuth 2.0 Device Authorization Grant","url":"https://www.rfc-editor.org/rfc/rfc8628"}]},{"chapter":"Sign-in methods","name":"Never remove the last way to sign in","level":"Recommended","applies":["accounts"],"rule":"Let people add and remove sign-in methods, but refuse to remove the last one.","check":"On a test account with one passkey only, call the remove endpoint for it. The answer is 409 and the passkey stays.","url":"https://agentwranglers.se/playbook/accounts/sign-in-methods/last-sign-in-method","related":["/playbook/accounts/sign-up/pre-hijacking","/playbook/accounts/sign-in-methods/link-provider-login","/playbook/adversarial-testing/access-and-accounts/reauthenticate-sensitive-changes"],"sources":[]},{"chapter":"Second factors and recovery","name":"Offer second factors from strongest to weakest","level":"Recommended","applies":["accounts"],"rule":"Offer passkeys and security keys first, then authenticator apps. Offer push approval only when the person types a number from the sign-in screen. SMS and email codes are fallbacks, never an admin's only factor.","check":"The settings page lists factor types in the order passkey, security key, authenticator app, then text message. A test admin account with only SMS is held at the setup page. A push prompt without a typed number cannot be approved.","url":"https://agentwranglers.se/playbook/accounts/second-factors-and-recovery/second-factor-choice","related":["/playbook/accounts/second-factors-and-recovery/mfa-enrolment","/playbook/adversarial-testing/access-and-accounts/two-factor-login"],"sources":[{"label":"NIST SP 800-63B-4 on phishing resistance","url":"https://pages.nist.gov/800-63-4/sp800-63b.html#verifimpers"},{"label":"NIST SP 800-63B-4 on restricted authenticators","url":"https://pages.nist.gov/800-63-4/sp800-63b.html#restricted"},{"label":"NIST SP 800-63B-4 on out-of-band authenticators","url":"https://pages.nist.gov/800-63-4/sp800-63b.html#out-of-band"}]},{"chapter":"Second factors and recovery","name":"Confirm a second factor before turning it on","level":"Recommended","applies":["accounts"],"rule":"Turn on a new second factor only after the person proves it works once, and let them give it a name.","check":"Start authenticator setup and submit a wrong code. Two-factor login stays off. Submit the current code, and it turns on under the given name. At sign-in, the same code a second time is refused.","url":"https://agentwranglers.se/playbook/accounts/second-factors-and-recovery/mfa-enrolment","related":["/playbook/accounts/second-factors-and-recovery/second-factor-choice","/playbook/adversarial-testing/access-and-accounts/two-factor-login"],"sources":[{"label":"NIST SP 800-63B-4 on single-factor OTP verifiers","url":"https://pages.nist.gov/800-63-4/sp800-63b.html#single-factor-otp-verifiers"},{"label":"RFC 6238 TOTP Time-Based One-Time Password Algorithm","url":"https://www.rfc-editor.org/rfc/rfc6238"}]},{"chapter":"Second factors and recovery","name":"Give recovery codes with the first second factor","level":"Recommended","applies":["accounts"],"rule":"When someone turns on a second factor, give them 10 single-use recovery codes of at least 64 random bits each, and store only their hashes.","check":"Sign in with one recovery code, then try it again. The second use fails, a new code was shown after the first, and the notice email arrived. No code appears in plain text in the database.","url":"https://agentwranglers.se/playbook/accounts/second-factors-and-recovery/recovery-codes","related":["/playbook/adversarial-testing/access-and-accounts/two-factor-login"],"sources":[{"label":"NIST SP 800-63B-4 on saved recovery codes","url":"https://pages.nist.gov/800-63-4/sp800-63b.html#savedrecovery"}]},{"chapter":"Second factors and recovery","name":"Ask everyone to keep two ways in","level":"Recommended","applies":["accounts"],"rule":"Prompt each person with one second factor to add another, such as a second passkey or a security key.","check":"Sign in as a test user with one factor. Settings shows the reminder. Add a second factor, and the reminder goes away.","url":"https://agentwranglers.se/playbook/accounts/second-factors-and-recovery/two-authenticators","related":["/playbook/adversarial-testing/access-and-accounts/two-factor-login"],"sources":[{"label":"NIST SP 800-63B-4 on binding an additional authenticator","url":"https://pages.nist.gov/800-63-4/sp800-63b.html#bindexisting"}]},{"chapter":"Second factors and recovery","name":"Plan for losing every factor","level":"Recommended","applies":["accounts"],"rule":"Write down one recovery path for people who lost every factor and every recovery code. Make it slower and louder than sign-in.","check":"Run the process on a test account with no factors. Access returns only after the wait. Every verified address got a notice, and its cancel link stops the recovery.","url":"https://agentwranglers.se/playbook/accounts/second-factors-and-recovery/recovery-without-factors","related":["/playbook/adversarial-testing/access-and-accounts/two-factor-login","/playbook/accounts/support-and-staff-access/support-identity-check","/playbook/accounts/second-factors-and-recovery/no-security-questions"],"sources":[{"label":"NIST SP 800-63B-4 on account recovery methods","url":"https://pages.nist.gov/800-63-4/sp800-63b.html#account-recovery-methods"}]},{"chapter":"Second factors and recovery","name":"Tell people about every change to their sign-in","level":"Recommended","applies":["accounts"],"rule":"Notify every verified address within 5 minutes of a security change. That means an added or removed factor, a used recovery code, a new email or password, or a new device.","check":"On staging, add and remove a factor, use a recovery code and sign in from a new browser. Each sends one notice within 5 minutes, and none holds a sign-in link.","url":"https://agentwranglers.se/playbook/accounts/second-factors-and-recovery/security-notifications","related":["/playbook/email/unsubscribe-and-bounces/transactional-only","/playbook/accounts/second-factors-and-recovery/failed-second-factor"],"sources":[{"label":"NIST SP 800-63B-4 on account notifications","url":"https://pages.nist.gov/800-63-4/sp800-63b.html#notification"}]},{"chapter":"Second factors and recovery","name":"Never use security questions","level":"Avoid","applies":["accounts"],"rule":"Never use security questions, or facts such as a birth date or the last digits of a card, to sign in or recover an account.","check":"Search the schema and the code for names such as `security_question`, `secret_answer` or `maiden`. Nothing matches.","url":"https://agentwranglers.se/playbook/accounts/second-factors-and-recovery/no-security-questions","related":["/playbook/accounts/second-factors-and-recovery/recovery-without-factors","/playbook/adversarial-testing/access-and-accounts/password-reset"],"sources":[{"label":"NIST SP 800-63B-4 on password verifiers","url":"https://pages.nist.gov/800-63-4/sp800-63b.html#passwordver"}]},{"chapter":"Second factors and recovery","name":"Let organisations require a second factor","level":"Recommended","applies":["accounts"],"rule":"Let organisation owners require a second factor for every member, and hold members without one at a setup page.","check":"Turn the policy on for a test organisation. After the grace period, a member without a second factor reaches only the setup page. A member with a passkey works as before.","url":"https://agentwranglers.se/playbook/accounts/second-factors-and-recovery/org-mfa-policy","related":["/playbook/adversarial-testing/access-and-accounts/two-factor-login","/playbook/accounts/business-sign-on-and-provisioning/sso-reauthentication"],"sources":[{"label":"RFC 8176 Authentication Method Reference Values","url":"https://www.rfc-editor.org/rfc/rfc8176"}]},{"chapter":"Second factors and recovery","name":"Tell people when their password worked but the second factor failed","level":"Recommended","applies":["accounts"],"rule":"When a sign-in passes the password but fails the second factor, offer another factor and notify the account owner. Show the attempt at their next sign-in too.","check":"On staging, enter a test user's correct password, then three wrong codes, then leave. One notice arrives with the time and browser. The next sign-in shows the failed attempt. The code page links to another factor and to a recovery code.","url":"https://agentwranglers.se/playbook/accounts/second-factors-and-recovery/failed-second-factor","related":["/playbook/accounts/second-factors-and-recovery/security-notifications","/playbook/adversarial-testing/access-and-accounts/two-factor-login"],"sources":[{"label":"OWASP Multifactor Authentication Cheat Sheet on failed login attempts","url":"https://cheatsheetseries.owasp.org/cheatsheets/Multifactor_Authentication_Cheat_Sheet.html#failed-login-attempts"}]},{"chapter":"Second factors and recovery","name":"Let a remembered device skip only the second factor","level":"Recommended","applies":["accounts"],"rule":"A \"remember this device\" choice may skip the second factor at sign-in for at most 30 days. It never skips the first factor, admin sign-in or the check before a sensitive change.","check":"Sign in with the box ticked, sign out and sign in again. The password is asked and the code is not. Change the email, and the code is asked. Move a fake clock 31 days, and the code is asked again. A test admin is always asked.","url":"https://agentwranglers.se/playbook/accounts/second-factors-and-recovery/remembered-device","related":["/playbook/adversarial-testing/access-and-accounts/session-cookies","/playbook/adversarial-testing/access-and-accounts/reauthenticate-sensitive-changes","/playbook/accounts/sessions-and-security-activity/session-list"],"sources":[{"label":"NIST SP 800-63B-4 on browser cookies","url":"https://pages.nist.gov/800-63-4/sp800-63b.html#sesscookies"},{"label":"NIST SP 800-63B-4 on AAL1 reauthentication","url":"https://pages.nist.gov/800-63-4/sp800-63b.html#aal1reauth"},{"label":"OWASP Authentication Cheat Sheet on risk-based authentication","url":"https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#adaptive-or-risk-based-authentication"}]},{"chapter":"Sessions and security activity","name":"List every signed-in device","level":"Recommended","applies":["accounts"],"rule":"Show each person their active sessions, with device, browser, approximate place, sign-in time and last activity, and mark the current one.","check":"Sign in from two browsers. The list in each shows both, with the right browser names, and marks the current one.","url":"https://agentwranglers.se/playbook/accounts/sessions-and-security-activity/session-list","related":["/playbook/accounts/sign-in-methods/device-sign-in","/playbook/accounts/second-factors-and-recovery/remembered-device","/playbook/adversarial-testing/access-and-accounts/session-cookies"],"sources":[{"label":"OWASP Session Management Cheat Sheet","url":"https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html"}]},{"chapter":"Sessions and security activity","name":"Let people sign out of one device or all of them","level":"Required","risk":"takeover","applies":["accounts"],"rule":"Let people end any one session from the list, or every session but the current one. The ended sessions stop working on their next request.","check":"Sign in from two browsers. From the first, end the second session. The second browser's next request answers 401 or shows the sign-in page.","url":"https://agentwranglers.se/playbook/accounts/sessions-and-security-activity/revoke-sessions","related":["/playbook/website/security/clear-site-data","/playbook/adversarial-testing/access-and-accounts/token-checks","/playbook/notifications/push-notifications/unlink-push-on-sign-out","/playbook/accounts/sessions-and-security-activity/security-activity-log","/playbook/accounts/sessions-and-security-activity/sign-out-link"],"sources":[{"label":"OWASP Session Management Cheat Sheet","url":"https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html"},{"label":"RFC 7009 OAuth 2.0 Token Revocation","url":"https://www.rfc-editor.org/rfc/rfc7009"}]},{"chapter":"Sessions and security activity","name":"End sessions when access changes","level":"Required","risk":"takeover","applies":["accounts"],"rule":"End every other session when the password changes or a factor is removed. End every session when the account is disabled, deleted or locked after a \"This wasn't me\" report.","check":"Sign in from two browsers and change the password in the first. The second is signed out and the first stays in. Disable the test account as an admin, and the first is signed out too.","url":"https://agentwranglers.se/playbook/accounts/sessions-and-security-activity/end-sessions-on-change","related":["/playbook/adversarial-testing/access-and-accounts/password-reset","/playbook/accounts/organisations-and-teams/member-removal"],"sources":[{"label":"OWASP Session Management Cheat Sheet","url":"https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html"},{"label":"NIST SP 800-63B-4 on session management","url":"https://pages.nist.gov/800-63-4/sp800-63b.html#sessmgmt"}]},{"chapter":"Sessions and security activity","name":"List connected apps and personal tokens","level":"Recommended","applies":["accounts"],"rule":"Show each person every app, agent and personal token that acts for them, with its scopes and last use. Let them revoke each one.","check":"Grant a test app access and make a personal token. Both appear in settings. Revoke both, and their next calls answer 401.","url":"https://agentwranglers.se/playbook/accounts/sessions-and-security-activity/connected-apps","related":["/playbook/api-design/api-security/api-keys"],"sources":[{"label":"RFC 7009 OAuth 2.0 Token Revocation","url":"https://www.rfc-editor.org/rfc/rfc7009"}]},{"chapter":"Sessions and security activity","name":"Show people their own security log","level":"Recommended","applies":["accounts"],"rule":"Keep a log of each person's security events and show it to them in settings for at least 90 days.","check":"On a test account, sign in, add a passkey and export data. Settings shows three new events with their times. The log holds no token and no IP address.","url":"https://agentwranglers.se/playbook/accounts/sessions-and-security-activity/security-activity-log","related":["/playbook/accounts/sessions-and-security-activity/revoke-sessions","/playbook/website/privacy/data-minimization","/playbook/adversarial-testing/supply-chain-and-setup/security-logging","/playbook/accounts/support-and-staff-access/impersonation"],"sources":[]},{"chapter":"Sessions and security activity","name":"Put sign-out on every signed-in page","level":"Recommended","applies":["accounts"],"rule":"Show a sign-out control in the header or main menu of every signed-in page, at most one click or tap away. Signing out ends the session on the server.","check":"A browser test signs in and opens every signed-in route, at phone and desktop width. Each page has a sign-out control within one click. After using it, the old cookie gets 401.","url":"https://agentwranglers.se/playbook/accounts/sessions-and-security-activity/sign-out-link","related":["/playbook/accounts/sessions-and-security-activity/revoke-sessions","/playbook/website/security/clear-site-data"],"sources":[{"label":"OWASP Session Management Cheat Sheet on the logout button","url":"https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#logout-button"},{"label":"NIST SP 800-63B-4 on session management","url":"https://pages.nist.gov/800-63-4/sp800-63b.html#sessmgmt"}]},{"chapter":"Sessions and security activity","name":"Let organisation owners sign a member out","level":"Recommended","applies":["accounts"],"rule":"Let organisation owners end every session and token a member has in their organisation, for a lost laptop or phone. The member must sign in again before reaching it.","check":"Sign a test member in from two browsers. As the owner, sign the member out. Both browsers get 401 or 403 on their next request to the organisation. The member signs in again and keeps the same role.","url":"https://agentwranglers.se/playbook/accounts/sessions-and-security-activity/owner-ends-sessions","related":["/playbook/notifications/push-notifications/unlink-push-on-sign-out","/playbook/compliance/audit-logs-and-access/audit-log-events","/playbook/accounts/organisations-and-teams/member-removal"],"sources":[{"label":"NIST SP 800-63B-4 on loss, theft, damage and compromise","url":"https://pages.nist.gov/800-63-4/sp800-63b.html#loss-theft-damage-and-compromise"}]},{"chapter":"Roles and permissions","name":"Check permissions instead of role names","level":"Recommended","applies":["accounts"],"rule":"Make code ask whether a user has a named permission, such as `invoice.refund`. Map roles to permissions in one place, and never compare role names in feature code.","check":"Search the code for role comparisons, such as `role === 'admin'` or `role == \"admin\"`. Matches appear only in the policy module. A test confirms `can` answers no for an unknown role.","url":"https://agentwranglers.se/playbook/accounts/roles-and-permissions/permissions-not-roles","related":["/playbook/adversarial-testing/access-and-accounts/function-access","/playbook/adversarial-testing/access-and-accounts/deny-by-default","/playbook/app-ui/permissions-flags-and-failures/capabilities-from-server"],"sources":[{"label":"OWASP Authorization Cheat Sheet","url":"https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html"}]},{"chapter":"Roles and permissions","name":"Start with roles and add record rules only when needed","level":"Recommended","applies":["accounts"],"rule":"Start with a few roles per organisation. Add conditions on the record only for rules that need them, such as \"only the author may edit a draft\".","check":"Each organisation has at most 6 roles, or a written reason for more. A search finds conditions on record fields only inside the policy module.","url":"https://agentwranglers.se/playbook/accounts/roles-and-permissions/rbac-then-abac","related":["/playbook/adversarial-testing/access-and-accounts/object-access"],"sources":[{"label":"NIST SP 800-162 on Attribute Based Access Control","url":"https://csrc.nist.gov/pubs/sp/800/162/upd2/final"}]},{"chapter":"Roles and permissions","name":"Test every role against every action","level":"Recommended","applies":["accounts"],"rule":"Keep a table of every role and every action with the expected answer, and run it as a test against the server on every change.","check":"Add a route without adding it to the table, and the test fails. Change a role's permissions without changing the table, and the test fails too.","url":"https://agentwranglers.se/playbook/accounts/roles-and-permissions/permission-matrix-test","related":["/playbook/ai-assisted-development/apps-built-with-ai-builders/check-auth-flows","/playbook/testing/unit-tests/decision-tables","/playbook/adversarial-testing/access-and-accounts/function-access","/playbook/adversarial-testing/access-and-accounts/object-access","/playbook/compliance/audit-logs-and-access/roles-as-code"],"sources":[]},{"chapter":"Roles and permissions","name":"Give roles inside an organisation","level":"Required","risk":"takeover","applies":["accounts"],"rule":"Store customer roles on the membership between a user and an organisation. Keep staff roles in a separate store that customer code cannot write.","check":"Make a test user owner of organisation A and viewer in B. An owner action in B answers 403, and the same action in A succeeds.","url":"https://agentwranglers.se/playbook/accounts/roles-and-permissions/org-scoped-roles","related":["/playbook/accounts/support-and-staff-access/staff-accounts","/playbook/data/schema-design/tenant-isolation"],"sources":[]},{"chapter":"Roles and permissions","name":"Make role changes count on the next request","level":"Required","risk":"data","applies":["accounts"],"rule":"Apply a removed role or permission within 60 seconds of the change. Never keep roles only inside a long-lived token or cache.","check":"Demote a signed-in test admin to viewer. Within 60 seconds, their next admin request answers 403.","url":"https://agentwranglers.se/playbook/accounts/roles-and-permissions/role-changes-immediate","related":["/playbook/accounts/organisations-and-teams/member-removal","/playbook/adversarial-testing/access-and-accounts/function-access"],"sources":[]},{"chapter":"Roles and permissions","name":"Limit tokens and agents to what their owner may do","level":"Required","risk":"takeover","applies":["accounts"],"rule":"Give an API key, connected app or AI agent at most its creator's permissions. Check the creator's current role on every call.","check":"As a test admin, make a key with an admin scope, then demote the admin to member. The key's next admin call answers 403.","url":"https://agentwranglers.se/playbook/accounts/roles-and-permissions/delegated-permissions","related":["/playbook/website/ready-for-ai-agents/auth-md","/playbook/api-design/api-security/token-scopes"],"sources":[]},{"chapter":"Roles and permissions","name":"Let owners see who can do what","level":"Recommended","applies":["accounts"],"rule":"Give organisation owners one page that lists every member, role, sign-in method and last activity, with pending invitations, API keys and an export.","check":"Open the page as a test owner. Every member, invitation and key appears, and the CSV export has the same rows.","url":"https://agentwranglers.se/playbook/accounts/roles-and-permissions/access-review","related":["/playbook/compliance/audit-logs-and-access/access-reviews"],"sources":[]},{"chapter":"Organisations and teams","name":"Let organisations own the shared data","level":"Recommended","applies":["accounts"],"rule":"Make an organisation own the shared data, and let one person belong to several organisations. Give even a solo user an organisation of their own.","check":"Search the schema for business tables keyed only by a user ID. None exist. Remove the creator from a test organisation, and all its records stay.","url":"https://agentwranglers.se/playbook/accounts/organisations-and-teams/organisation-owns-data","related":["/playbook/data/schema-design/tenant-isolation"],"sources":[]},{"chapter":"Organisations and teams","name":"Make invitations expire and work once","level":"Recommended","applies":["accounts"],"rule":"Send each invitation as a single-use link that expires within 7 days, with one role chosen at sending. Let admins see and cancel pending invitations.","check":"Accept an invitation, then open it again, and it fails. An 8-day-old invitation fails. Accepting while signed in with another email fails. A member inviting someone as owner fails.","url":"https://agentwranglers.se/playbook/accounts/organisations-and-teams/invitations","related":["/playbook/email/email-security/sign-in-links","/playbook/accounts/organisations-and-teams/admin-set-passwords"],"sources":[]},{"chapter":"Organisations and teams","name":"Prove a domain before trusting it","level":"Recommended","applies":["accounts"],"rule":"Let an organisation claim an email domain only after it publishes a DNS TXT record you gave it. Only then may the domain route sign-in or let people join.","check":"Claim a test domain without the record, and verification fails. Add the record, and it passes. A claim on `gmail.com` is refused.","url":"https://agentwranglers.se/playbook/accounts/organisations-and-teams/verified-domains","related":["/playbook/accounts/business-sign-on-and-provisioning/organisation-sso"],"sources":[]},{"chapter":"Organisations and teams","name":"Never leave an organisation without an owner","level":"Recommended","applies":["accounts"],"rule":"Refuse any change that leaves an organisation with no owner, such as removing, demoting or deleting its last owner.","check":"In a test organisation with one owner, try to demote, remove and delete that owner. Each answers 409. With two owners, demote both in parallel. One succeeds and the other answers 409.","url":"https://agentwranglers.se/playbook/accounts/organisations-and-teams/last-owner","related":["/playbook/accounts/organisations-and-teams/member-removal","/playbook/accounts/profile-and-account-lifecycle/account-deletion"],"sources":[]},{"chapter":"Organisations and teams","name":"Transfer ownership only when the new owner accepts","level":"Required","risk":"takeover","applies":["accounts"],"rule":"Transfer ownership of an organisation only after the current owner proves who they are again and the new owner accepts.","check":"Start a transfer without fresh proof, and it is refused. Start it with proof. Roles stay the same until the new owner accepts, and then both people get a notice.","url":"https://agentwranglers.se/playbook/accounts/organisations-and-teams/ownership-transfer","related":["/playbook/adversarial-testing/access-and-accounts/reauthenticate-sensitive-changes"],"sources":[]},{"chapter":"Organisations and teams","name":"Remove access when someone leaves","level":"Recommended","applies":["accounts"],"rule":"When a member leaves or is removed, end their access to that organisation on their next request, and keep the work they made there.","check":"Remove a signed-in test member. Their next request to the organisation answers 403 or 404, their API key gets 401, and their documents are still there.","url":"https://agentwranglers.se/playbook/accounts/organisations-and-teams/member-removal","related":["/playbook/accounts/sessions-and-security-activity/end-sessions-on-change","/playbook/accounts/sessions-and-security-activity/owner-ends-sessions","/playbook/accounts/roles-and-permissions/role-changes-immediate","/playbook/accounts/organisations-and-teams/last-owner","/playbook/compliance/audit-logs-and-access/offboarding-within-a-day","/playbook/accounts/business-sign-on-and-provisioning/deprovision"],"sources":[]},{"chapter":"Organisations and teams","name":"Keep the organisation in the address","level":"Recommended","applies":["accounts"],"rule":"Put the organisation's ID or slug in every address inside it, such as `/acme/invoices/42`, and take the organisation for each request from the address.","check":"Open organisation A in one tab and B in another, then create a record in each. Each record lands in the organisation of its own tab.","url":"https://agentwranglers.se/playbook/accounts/organisations-and-teams/organisation-in-url","related":["/playbook/app-ui/client-and-server-state/state-in-url"],"sources":[]},{"chapter":"Organisations and teams","name":"Never let admins choose or see a member's password","level":"Recommended","applies":["accounts"],"rule":"When an admin adds a member or resets their sign-in, send the member a setup link. The admin never types, sees or receives a password.","check":"The add-member and edit-member forms have no password field. Call the admin reset endpoint for a test member. The response holds no password or link, and the member's inbox gets the reset email.","url":"https://agentwranglers.se/playbook/accounts/organisations-and-teams/admin-set-passwords","related":["/playbook/accounts/organisations-and-teams/invitations","/playbook/adversarial-testing/access-and-accounts/password-reset","/playbook/compliance/audit-logs-and-access/audit-log-events"],"sources":[{"label":"NIST SP 800-63B-4 on password authenticators","url":"https://pages.nist.gov/800-63-4/sp800-63b.html#password-authenticators"}]},{"chapter":"Support and staff access","name":"Use separate staff accounts","level":"Required","risk":"takeover","applies":["accounts"],"rule":"Give staff accounts that are separate from customer accounts, signed in through company single sign-on with a passkey or security key.","check":"Sign in to the admin host with a customer account, and it is refused. Compare the staff accounts with the company directory. Nobody who has left still has one.","url":"https://agentwranglers.se/playbook/accounts/support-and-staff-access/staff-accounts","related":["/playbook/accounts/roles-and-permissions/org-scoped-roles","/playbook/adversarial-testing/access-and-accounts/two-factor-login","/playbook/data/personal-data/access-logging"],"sources":[{"label":"CISA fact sheet on implementing phishing-resistant MFA","url":"https://www.cisa.gov/sites/default/files/publications/fact-sheet-implementing-phishing-resistant-mfa-508c.pdf"}]},{"chapter":"Support and staff access","name":"Check who is asking before support changes an account","level":"Required","risk":"takeover","applies":["accounts"],"rule":"Never change an email, phone number, factor or owner because someone asks by phone, chat or email. Send them to the in-app flow or the recovery process instead.","check":"List the actions in the support tool. None changes an email, phone, factor or owner directly. A test request for an email change by chat ends in the recovery process.","url":"https://agentwranglers.se/playbook/accounts/support-and-staff-access/support-identity-check","related":["/playbook/accounts/second-factors-and-recovery/recovery-without-factors","/playbook/compliance/audit-logs-and-access/audit-log-events"],"sources":[]},{"chapter":"Support and staff access","name":"Impersonate customers only with a reason and a time limit","level":"Recommended","applies":["accounts"],"rule":"Let staff view an account as its customer only with a recorded reason, for at most 60 minutes. Make it read-only by default and log it for the customer.","check":"Start impersonation without a reason, and it is refused. With a reason, try to change the email, and it is refused. After 60 minutes the session ends. The customer's security log shows the visit.","url":"https://agentwranglers.se/playbook/accounts/support-and-staff-access/impersonation","related":["/playbook/data/personal-data/access-logging","/playbook/accounts/sessions-and-security-activity/security-activity-log"],"sources":[{"label":"RFC 8693 OAuth 2.0 Token Exchange","url":"https://www.rfc-editor.org/rfc/rfc8693#section-4.1"}]},{"chapter":"Support and staff access","name":"Ask business customers before support looks in","level":"Recommended","applies":["accounts"],"rule":"Let organisation owners grant support access for a set time, and refuse impersonation of their members without a grant.","check":"Try to impersonate a member of a test organisation without a grant, and it is refused. Grant 72 hours, and it works. End the grant, and the next try is refused.","url":"https://agentwranglers.se/playbook/accounts/support-and-staff-access/support-access-consent","related":["/playbook/compliance/audit-logs-and-access/audit-log-events"],"sources":[]},{"chapter":"Profile and account lifecycle","name":"Keep the old email until the new one is confirmed","level":"Required","risk":"takeover","applies":["accounts"],"rule":"Store a new email address as pending until its owner confirms it. Keep sign-in, resets and notices on the old address until then.","check":"Ask for a change and leave it unconfirmed. A password reset still mails the old address. Confirm, and resets mail the new one. The undo link in the old address's notice restores it.","url":"https://agentwranglers.se/playbook/accounts/profile-and-account-lifecycle/email-change","related":["/playbook/adversarial-testing/access-and-accounts/reauthenticate-sensitive-changes","/playbook/email/email-security/sign-in-links"],"sources":[]},{"chapter":"Profile and account lifecycle","name":"Let usernames change without breaking old links","level":"Recommended","applies":["accounts"],"rule":"When a username or organisation slug changes, redirect the old address to the new one. Keep the old name reserved for at least 90 days.","check":"Rename a test organisation from `acme` to `acme-bakery`. `/acme/invoices` answers 301 to `/acme-bakery/invoices`, and a new sign-up as `acme` is refused.","url":"https://agentwranglers.se/playbook/accounts/profile-and-account-lifecycle/username-change","related":["/playbook/accounts/sign-up/reserved-names","/playbook/website/redirects-and-canonicals/redirect-map"],"sources":[{"label":"RFC 9110 HTTP Semantics on 301 Moved Permanently","url":"https://www.rfc-editor.org/rfc/rfc9110#section-15.4.2"}]},{"chapter":"Profile and account lifecycle","name":"Let people export their data themselves","level":"Recommended","applies":["accounts","eu-users"],"rule":"Offer a download of a person's data from settings, in JSON or CSV, ready within 24 hours.","check":"Ask for an export on a test account with data in every store. A link arrives within 24 hours, it opens only when signed in, and the file holds data from every store.","url":"https://agentwranglers.se/playbook/accounts/profile-and-account-lifecycle/self-service-export","related":["/playbook/website/privacy/data-requests","/playbook/api-design/files-and-uploads/spreadsheet-exports","/playbook/compliance/product-and-sector-laws/data-act-switching","/playbook/compliance/product-and-sector-laws/data-act-connected-products"],"sources":[{"label":"GDPR Articles 15 and 20 on access and portability","url":"https://eur-lex.europa.eu/eli/reg/2016/679/oj"}]},{"chapter":"Profile and account lifecycle","name":"Let people delete their account themselves","level":"Required","risk":"law","applies":["accounts","eu-users"],"rule":"Put account deletion in settings, behind fresh proof of identity, and finish it within one month, as [data requests](https://agentwranglers.se/playbook/website/privacy/data-requests) sets. Say first what happens to the person's data and organisations.","check":"Delete a test account. Its sessions end at once and the email states the date. After the grace period, a search of every store finds no trace except records the law requires.","url":"https://agentwranglers.se/playbook/accounts/profile-and-account-lifecycle/account-deletion","related":["/playbook/website/privacy/data-requests","/playbook/payments/subscriptions/online-cancellation","/playbook/accounts/organisations-and-teams/last-owner"],"sources":[{"label":"GDPR Article 17 on the right to erasure","url":"https://eur-lex.europa.eu/eli/reg/2016/679/oj"},{"label":"GDPR Article 12(3) on the one-month deadline","url":"https://eur-lex.europa.eu/eli/reg/2016/679/oj"}]},{"chapter":"Profile and account lifecycle","name":"Warn before closing inactive accounts","level":"Recommended","applies":["accounts"],"rule":"Set a written inactivity period for free accounts, such as 24 months without sign-in. Warn twice by email, then delete or anonymise the account.","check":"Set a test account's last sign-in 23 months back and run the job. It gets the first warning. Set it past 24 months with both warnings sent, and the job deletes it.","url":"https://agentwranglers.se/playbook/accounts/profile-and-account-lifecycle/inactive-accounts","related":["/playbook/data/personal-data/retention-jobs"],"sources":[{"label":"GDPR Article 5(1)(e) on storage limitation","url":"https://eur-lex.europa.eu/eli/reg/2016/679/oj"}]},{"chapter":"Business sign-on and provisioning","name":"Connect single sign-on per organisation","level":"Recommended","applies":["accounts"],"rule":"Let each business organisation connect its own identity provider through SAML 2.0 or OpenID Connect, and route its users there by verified email domain.","check":"On staging, start sign-in as `ana@acme.example`. The browser goes to Acme's test provider. Start as `bo@other.example`, and the normal sign-in page appears.","url":"https://agentwranglers.se/playbook/accounts/business-sign-on-and-provisioning/organisation-sso","related":["/playbook/adversarial-testing/access-and-accounts/single-sign-on","/playbook/accounts/organisations-and-teams/verified-domains","/playbook/accounts/sign-in-methods/own-user-id"],"sources":[]},{"chapter":"Business sign-on and provisioning","name":"Let organisations require single sign-on","level":"Recommended","applies":["accounts"],"rule":"Let an organisation require its single sign-on for every member with its domain. Keep one owner with a passkey who can sign in without it.","check":"Turn the setting on. A member with the domain cannot sign in with a password. The spare owner can, and the other owners get a notice.","url":"https://agentwranglers.se/playbook/accounts/business-sign-on-and-provisioning/require-sso","related":["/playbook/adversarial-testing/access-and-accounts/single-sign-on"],"sources":[]},{"chapter":"Business sign-on and provisioning","name":"Provision users with SCIM","level":"Recommended","applies":["accounts"],"rule":"Offer SCIM 2.0, as RFC 7643 and RFC 7644 define it, so a customer's directory creates, updates and removes users and groups.","check":"Run a SCIM test suite, such as the one Okta or Microsoft Entra ID provides, against staging. Every test passes. A user created through SCIM can sign in.","url":"https://agentwranglers.se/playbook/accounts/business-sign-on-and-provisioning/scim-provisioning","related":["/playbook/accounts/business-sign-on-and-provisioning/deprovision"],"sources":[{"label":"RFC 7643 SCIM Core Schema","url":"https://www.rfc-editor.org/rfc/rfc7643"},{"label":"RFC 7644 SCIM Protocol","url":"https://www.rfc-editor.org/rfc/rfc7644"}]},{"chapter":"Business sign-on and provisioning","name":"Remove access at once when the directory says so","level":"Required","risk":"data","applies":["accounts"],"rule":"When a customer's directory deactivates or deletes a user, end that user's sessions and tokens in the organisation within 1 minute, and keep their work.","check":"Deactivate a signed-in test user through SCIM, once with a `path` and once with the value object form. Within 1 minute, their next request answers 401 or 403 each time. Their records remain.","url":"https://agentwranglers.se/playbook/accounts/business-sign-on-and-provisioning/deprovision","related":["/playbook/accounts/business-sign-on-and-provisioning/scim-provisioning","/playbook/accounts/organisations-and-teams/member-removal","/playbook/adversarial-testing/access-and-accounts/session-cookies","/playbook/accounts/business-sign-on-and-provisioning/shared-signals"],"sources":[{"label":"RFC 7644 SCIM Protocol, modifying with PATCH","url":"https://www.rfc-editor.org/rfc/rfc7644#section-3.5.2"}]},{"chapter":"Business sign-on and provisioning","name":"Map directory groups to roles","level":"Recommended","applies":["accounts"],"rule":"Let organisation admins map their directory groups to your roles, and apply the mapping on every SCIM change and every sign-in.","check":"Add a test user to a mapped group in the test directory. Their role changes on the next sync. Remove them from the group, and the role goes back.","url":"https://agentwranglers.se/playbook/accounts/business-sign-on-and-provisioning/group-role-mapping","related":["/playbook/compliance/audit-logs-and-access/roles-as-code"],"sources":[]},{"chapter":"Business sign-on and provisioning","name":"Listen for security signals from the customer's provider","level":"Optional","applies":["accounts"],"rule":"Receive OpenID Shared Signals events from customers' identity providers, and end sessions when they report a revoked session or a disabled account.","check":"Send a signed test `session-revoked` event for a signed-in test user. Their next request answers 401. The same event with a bad signature changes nothing.","url":"https://agentwranglers.se/playbook/accounts/business-sign-on-and-provisioning/shared-signals","related":["/playbook/adversarial-testing/access-and-accounts/token-checks","/playbook/accounts/business-sign-on-and-provisioning/deprovision"],"sources":[{"label":"RFC 8417 Security Event Token (SET)","url":"https://www.rfc-editor.org/rfc/rfc8417"},{"label":"RFC 8935 Push-Based SET Delivery Using HTTP","url":"https://www.rfc-editor.org/rfc/rfc8935"},{"label":"RFC 8936 Poll-Based SET Delivery Using HTTP","url":"https://www.rfc-editor.org/rfc/rfc8936"}]},{"chapter":"Business sign-on and provisioning","name":"Ask the provider for a fresh sign-in before sensitive changes","level":"Recommended","applies":["accounts"],"rule":"When a person who signs in through single sign-on makes a sensitive change, send them back to the provider for a fresh sign-in. Check the sign-in time it returns.","check":"Point staging at a test provider that ignores `max_age` and returns an `auth_time` from an hour ago. The sensitive change is refused. With a fresh `auth_time`, it goes through.","url":"https://agentwranglers.se/playbook/accounts/business-sign-on-and-provisioning/sso-reauthentication","related":["/playbook/adversarial-testing/access-and-accounts/reauthenticate-sensitive-changes","/playbook/accounts/second-factors-and-recovery/org-mfa-policy"],"sources":[{"label":"OpenID Connect Core 1.0 authentication request","url":"https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest"},{"label":"OpenID Connect Core 1.0 ID token validation","url":"https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation"},{"label":"NIST SP 800-63B-4 on reauthentication","url":"https://pages.nist.gov/800-63-4/sp800-63b.html#sessionreauthn"}]}]}