Email and password login.
A returning user enters their email or username and password, gets a session, and lands on the page they were trying to reach.
Requirements exploration.
This design covers the web login screen for a photo-sharing product. The server is treated as a black box, and components are described by what they're responsible for, not by library.
Clarifying questions
Functional requirements
Non-functional requirements
| No. | Area | Requirement | Target / measure |
|---|---|---|---|
| 01 | Performance | The form is visible and usable almost immediately, even on slow networks. | LCP < 1.5sInteractive < 2sRoute JS < 30 KB Mid-range phone on 4G, compressed JS. |
| 02 | Security | No credential exposure, CSRF-safe, session in an HttpOnly cookie. | 0 credentials in URLs, logs, analytics or storage |
| 03 | User experience | Clear loading, error and success states. No double submits. | Every error has text + a recovery action |
| 04 | Accessibility | Fully operable by keyboard and screen reader. | WCAG 2.1 AA |
| 05 | Password managers | Browser and third-party managers fill and save credentials. | Chrome · Safari · Firefox built-insMajor managers |
| 06 | Internationalisation | All strings translated; right-to-left layouts supported. | All supported locales |
Scope
High level design.
Only the Auth Service knows about HTTP and CSRF. The form knows nothing about the network, and the store is the single place other surfaces read login state from.
Component diagram
Click a card to see its block and connections in the diagram.
Sequence diagram
The happy path, from pressing Enter to landing on the next page. Scroll sideways to follow it.
- User → LoginForm: type + Enter
- LoginForm → LoginForm: validate locally → ok
- LoginForm → Auth Store: loginRequested
- Note over Auth Store: status = submitting
- Auth Store → Auth Service: login(creds)
- Auth Service → Server: POST /api/auth/login + X-CSRF-Token
- Note over Server: verify, risk check
- Server → Auth Service (reply): 200 + Set-Cookie
- Auth Service → Auth Store (reply): Success(viewer)
- Note over Auth Store: status = authenticated
- Auth Store → Router: status changed
- Router → User (reply): redirect to next or Home
| Code | Outcome | Kind | What happens | Reacts |
|---|---|---|---|---|
| 401 | invalid_credentials | error | Banner, focus password, keep identifier. | 2LoginForm |
| 200 | two_factor_required | challenge | Route to /accounts/login/two_factor. | 5Session Guard |
| 403 | checkpoint_required | challenge | Route to /challenge. | 5Session Guard |
| 429 | rate_limited | error | Banner with wait time, submit disabled. | 2LoginForm |
| ERR | network error / timeout | error | “Check your connection”, values kept. | 2LoginForm |
Data model.
The password lives in exactly one place, and only briefly. Everything else the app needs after login fits in a small store.
Interface definition.
Login is a single request and response, and nothing needs to be pushed. Plain REST keeps the route bundle small.
Transport choice
Server to client APIs
Sets the csrftoken cookie and returns the token. Called once on page load, or embedded in the server-rendered HTML to save a round trip.
Verifies credentials and, on success, sets the session cookie.
Error cases
Every error comes back in one shape. Auth Service reads the code and the client reacts.
| HTTP | Type | Body code | Client behaviour |
|---|---|---|---|
| 400 | error | invalid_request | Show field errors from the server. Should not happen if client validation matches. |
| 401 | error | invalid_credentials | Banner: “Your email, username or password is incorrect.” Keep identifier, clear password, focus password. |
| 403 | challenge | checkpoint_required | Store the challenge, route to the checkpoint flow. |
| 403 | retry | csrf_failed | Refresh the CSRF token once, then retry the request once automatically. |
| 423 | error | account_disabled | Banner with a link to the help centre. |
| 429 | error | rate_limited+ retry_after_s | Banner: “Please wait a few minutes before you try again.” Disable submit with a countdown. |
| 5xx | network | timeout · offline | Banner: “We couldn’t connect. Check your connection and try again.” Keep both values. |
Client to client communication
Fields talk to LoginForm through props and callbacks. LoginForm and the rest of the app talk through Auth Store actions. Pick a scenario and step through it to see which action fires and what it changes.
UI component API
LoginForm is reused by the login page and by the login modal that appears when a logged-out user tries to like or follow. Click a prop to see what it controls.
- Prop
- defaultIdentifier
- Default
- ""
- Why
- Prefills the field, for example after sign-up or logout.
Optimizations.
Not everything can be optimised at once. A quick rubric picks the areas that define this surface, then each one gets a deep dive.
Priority rubric
Each candidate area is scored 1–3 on three questions. The top three become deep dives; the rest are handled elsewhere or do not apply.
| Rank | Area | Impact if it fails | Users affected | How often it matters | Score | Decision |
|---|---|---|---|---|---|---|
| 1 | Security of credentials and session | 3 of 3 | 3 of 3 | 3 of 3 | 9 | Deep diveA leak compromises the account, and every other surface depends on this session. |
| 2 | Form UX and accessibility | 2 of 3 | 3 of 3 | 3 of 3 | 8 | Deep diveEvery logged-out user passes through this form. Friction or confusing errors lose users. |
| 3 | First-load performance | 2 of 3 | 3 of 3 | 2 of 3 | 7 | Deep diveOften the first page a user sees, frequently on a mobile network. |
| 4 | Internationalisation | 1 of 3 | 2 of 3 | 1 of 3 | 4 | SkippedCopy is short and handled by the app-wide i18n layer. |
| 5 | SEO | 1 of 3 | 1 of 3 | 1 of 3 | 3 | SkippedLogin pages should not be indexed. |
| 6 | Offline support | 1 of 3 | 1 of 3 | 1 of 3 | 3 | SkippedLogging in needs the network by definition. |
Deep dive: security
Every item pairs a concrete attack with the layer that stops it.
| No. | Concern | Risk | Approach | Owned by |
|---|---|---|---|---|
| 01 | Session storage | An injected script steals the session. | HttpOnly, Secure, SameSite=Lax cookie set by the server. JavaScript never sees it. | 6Server |
| 02 | CSRF | Another site submits the form as the user. | Double-submit token from a cookie, sent back in X-CSRF-Token. SameSite=Lax adds a second layer. | 4Auth Service6Server |
| 03 | Credentials in URLs | The password ends up in history, logs or referrers. | Always POST, including the no-JavaScript fallback. | 2LoginForm |
| 04 | Credentials in telemetry | The password leaks into error reports or session replays. | Strip request bodies from errors, log only outcome codes, mask password inputs. | 4Auth Service |
| 05 | Password in memory | The password lingers after it is needed. | Held only in form state. Cleared after a failure and on unmount, never stored. | 2LoginForm |
| 06 | Open redirect | next sends the user to a phishing site. | Accept next only as a same-origin path starting with a single /. Anything else goes Home. | 5Session Guard |
| 07 | Brute force | Attackers guess passwords at scale. | The server rate limits and may challenge. The client honours retry_after_s with a countdown. | 6Server2LoginForm |
| 08 | Clickjacking | The login page is framed to trick clicks. | The server sends headers that forbid framing. | 6Server |
| 09 | Content Security Policy | An injected script reads typed passwords. | A strict policy on this route, with no third-party scripts. | 6Server |
Deep dive: form UX and accessibility
Small details decide whether people get in on the first try, including with a screen reader or password manager.
| No. | Concern | Risk | Approach | Owned by |
|---|---|---|---|---|
| 01 | Labels | Placeholder-only fields lose context once typed. | A real <label> per input. A floating label must stay visible after typing. | 2LoginForm |
| 02 | Password managers | Autofill breaks and users give up. | Correct autocomplete values, a real <form>, stable name attributes, no paste blocking. Read values with FormData at submit. | 2LoginForm |
| 03 | Validation timing | Errors appear while the user is still typing. | Validate on blur and on submit. Once a field shows an error, re-validate on change. | 2LoginForm |
| 04 | Error presentation | Screen reader users miss what went wrong. | aria-describedby and aria-invalid on fields, server errors in a role="alert" banner, focus the first invalid field. | 2LoginForm |
Deep dive: first-load performance
The page has to be usable fast on a mid-range phone, before most of the app has loaded.
| No. | Concern | Risk | Approach | Owned by |
|---|---|---|---|---|
| 01 | Server-render the form | A blank screen until JavaScript loads. | Static HTML with a real form that works before hydration. | 1LoginPage |
| 02 | Route bundle budget | Heavy JavaScript delays the first tap on mobile. | Keep route JS under 30 KB compressed. Load the challenge flow lazily. | 1LoginPage |
| 03 | CSRF token in the HTML | An extra round trip before the first submit. | Embed the token in the server-rendered page instead of calling GET /api/auth/csrf. | 6Server4Auth Service |
| 04 | Fonts and images | Layout shifts and a slow LCP. | A system font stack or a preloaded subset, and no hero image on this route. | 1LoginPage |
Trade-offs.
Each decision lists the options that were on the table. The chosen one is first; the others stay visible so the reasoning can be checked.
- Pro:Not readable by scripts
- Pro:Sent automatically
- Pro:Works with server rendering
- Con:Needs CSRF protection
Any injected script can steal it; Persists indefinitely
More moving parts; Lost on every reload, costing a refresh call
- Pro:No noise while typing
- Pro:Fast feedback once a mistake is known
- Con:Slightly more logic
Shows errors before the user has finished typing
Late feedback
- Pro:Discoverable by screen readers
- Pro:Explains what is missing
- Con:An extra press to learn the rule
Skipped by keyboard and gives no reason; Autofill without input events can leave it wrongly disabled
- Pro:Does not reveal whether an account exists
- Con:Less specific help
Lets attackers enumerate accounts
- Pro:Fastest paint
- Pro:Works without JavaScript
- Con:Needs a server rendering path for this route
Blank until JavaScript loads; Slower first paint on mobile