ARTDANTECH
Security design docSecure front-end engineeringLet's Talk

Argent Bank — Phase 1: Authentication

A security design document on building the authentication foundation for a neobank front-end — the threat model, the defensive decisions, and the parts we explicitly left out of scope.

Overview

Banking front-ends fail in ways that look small from the outside and catastrophic from the inside: a token that outlives a logout, a protected route that flashes its content for 200ms before the guard kicks in, an error message that leaks whether an email is registered. Each of these is a real-world trust breach. This is the design document for how we built against them — including the parts we deliberately didn't cover.

The engagement

Phase 1 of a neobank front-end: complete authentication flow, protected routes, editable user profile, centralized Redux state, and a network layer designed to be the single chokepoint for every authenticated request.

The threat posture

Built as if every trust boundary would be tested. Not 'does it work when the user behaves,' but 'does it fail safely when a token expires mid-request, when someone navigates directly to a protected URL, when the network drops mid-login.'

The discipline

Every defensive choice documented with its tradeoff. Every out-of-scope item listed explicitly. A banking foundation that earns trust through transparency about what it does and doesn't do.

The outcome

An authenticated SPA that holds up against common front-end auth failure modes, integrates cleanly with the provided REST API, and ships as a foundation Phase 2 can build on without touching Phase 1's security assumptions. Live URL above.

Quick disclosure, consistent with earlier case studies in this collection: Argent Bank is a recognizable training brief. We're publishing it because the interesting work on a build like this isn't completing it — it's the security reasoning a senior engineer brings that a junior implementation misses. The rest of this document is that reasoning.


The threat model

Before writing a component, we wrote the list of things that could go wrong. Every defensive decision in the build traces back to one of these threats. Listing them explicitly is how a serious security posture starts — and it's the step most front-end engagements skip.

1. Token leakage via storage choice

Threat. JWTs stored in localStorage are readable by any script running on the origin, which means any XSS vulnerability — however small, however transient — becomes a full session compromise. Storing tokens in sessionStorage has the same problem with a shorter lifetime.

Defense. The token is stored in Redux state and in memory only, not in localStorage. Page refresh loses the session — which is a deliberate tradeoff, not a bug. The correct production solution for a real neobank would be an httpOnly cookie set by the back-end, which this Phase 1 build documents as the Phase 2 migration path. Making this tradeoff visible is more important than hiding it behind a working demo.

2. Route-guard flash of protected content

Threat. The naive implementation of a protected route renders the protected component, then checks auth in a useEffect, then redirects if the check fails. This produces a visible flash of sensitive content before the redirect happens — sometimes only for 50ms, but enough to leak data, confuse the user, and look broken under slow network.

Defense. Route guards check auth state synchronously at the guard level, before the protected component mounts. No flash, no leak, no useEffect race. The guard returns either the protected component or a <Navigate /> redirect — never both, never in sequence.

3. Logout that doesn't actually log out

Threat. A logout handler that clears the token but leaves user data in the Redux store leaves identifying information accessible to any component that reads from the store. A subsequent login as a different user can show stale data from the previous session for a split second.

Defense. Logout dispatches a root-level reset action that wipes all auth-related slices synchronously. The next render sees a clean slate, not the previous user's residue. This is also the defense against the "shared computer" scenario where one user logs out and another immediately logs in.

4. Error messages that leak account existence

Threat. A login form that says "user not found" for unknown emails and "wrong password" for known emails is an account enumeration oracle — an attacker can probe which email addresses have accounts without any valid credentials. This is a known OWASP issue that most banking onboarding flows still get wrong.

Defense. Every failed login produces the same generic error: "Invalid credentials." The back-end returns whatever it returns; the front-end normalizes to a single message. This is a one-line UX decision with real security consequences.

5. Network layer scattered across components

Threat. When components call fetch directly, token handling gets duplicated, error handling becomes inconsistent, and updating the auth scheme requires touching every call site. More subtly, it becomes impossible to add global defenses — request logging, token refresh, rate limit handling — because there's no single point to add them.

Defense. A single network wrapper is the only module that touches the back-end. Every authenticated request goes through it. Adding token refresh in Phase 2 is a one-file change. Adding request retry on transient failures is a one-file change. Adding request timing attack protection is a one-file change. This is the most boring decision in the build and the most important.


What's explicitly out of scope

A security posture that doesn't list its limits is not a security posture — it's a marketing document. Here's what this Phase 1 does not protect against, stated explicitly so nobody confuses the scope of the work with the scope of the threat surface.

  • XSS defense at the content level. React's JSX escaping handles the common cases, but this build doesn't include a Content Security Policy, Trusted Types, or DOMPurify for any user-generated content that would arrive in Phase 2. Real production banking needs all three.
  • CSRF protection. The current token scheme uses Bearer headers, which is not CSRF-vulnerable by design. The moment the Phase 2 migration to httpOnly cookies happens, CSRF tokens become mandatory and are not yet implemented.
  • Rate limiting on login attempts. Front-end rate limiting is theater — a real attacker bypasses the client entirely. This has to be enforced at the API layer, which is outside this engagement's scope. The front-end does not pretend to defend against credential stuffing.
  • Multi-factor authentication. Phase 1 is single-factor. Any real neobank needs MFA before production. The Redux slice structure is designed to accommodate an MFA step between credential submission and session establishment, but the step itself is not built.
  • Session timeout and idle lockout. The current session lasts as long as the token is valid per the back-end. A real banking front-end needs client-side idle detection and proactive lockout. This is a clearly-scoped Phase 2 addition.
  • Audit logging from the client. Tracking what an authenticated user does on the front-end is a compliance requirement most banking engagements need. Not implemented here. Designed to be added at the network wrapper layer where every action already passes through.

Listing these isn't an apology. It's the inventory a senior engineer hands to the next engineer on the project, so nobody inherits false assumptions about what's already defended.


Key architectural decisions

Two-slice Redux store

Auth and user are separate slices. Auth holds session tokens and session state. User holds profile and editable fields. The split is defensive — compromising one slice can't leak data from the other, and the reset boundaries are clean.

Guard at route, not component

React Router guards check auth before rendering protected components, eliminating the flash-of-content class of bug and producing predictable redirect behavior on direct URL entry.

Single network chokepoint

Every authenticated API call routes through one wrapper module. Token injection, error normalization and future refresh logic all live in one place that every component depends on implicitly.

Optimistic profile updates with rollback

Profile edits update the Redux store immediately and roll back visibly on API failure. Users see responsive UI; any network or validation error produces a clean revert instead of a confusing half-state.


Tech stack

  • React 18 (function components, hooks)
  • React Router 6 (guard-based route protection)
  • Redux Toolkit (slices, typed actions, RTK Query-ready)
  • Centralized network wrapper (fetch-based, single chokepoint)
  • Vite (dev server and build)
  • Jest + React Testing Library (unit and integration tests)
  • MSW (network mocking for realistic test coverage)
  • ESLint, Prettier (enforced via pre-commit)
  • Git / GitHub

Architectural decisions we'd revisit with a real client

The honest section we've been running on every case study in this collection. For a real neobank engagement with a real budget and a real compliance perimeter, here's what would change.

  • Token storage moves to httpOnly cookies. Non-negotiable for production. The current in-memory approach is a deliberate Phase 1 tradeoff, not a recommendation.
  • Add CSRF tokens in lockstep with the cookie migration. The two changes happen together or neither happens — changing one without the other creates a larger vulnerability than leaving both alone.
  • Introduce a refresh token flow. Short-lived access token, longer-lived refresh token, silent refresh via the network wrapper. The architecture is already shaped for this; the implementation is Phase 2 work.
  • Adopt TypeScript from day zero. The current build is JavaScript because the brief specified it. For a real client, the amount of state shape, action typing, and API contract enforcement in a banking front-end makes TypeScript effectively mandatory.
  • Integration tests against a staging environment, not just MSW. MSW is excellent for unit and component tests. Real integration tests against a real back-end catch the contract drift that mocks hide — and contract drift is where banking front-ends break in production.
  • Accessibility audit by an actual audit firm. The build passes automated axe checks but banking accessibility compliance (particularly for ADA and EAA requirements) benefits from a human audit with actual assistive technology users. Not something we'd skip on a production engagement.

What this engagement reveals

Most front-end security work is done by people who treat "security" as a checklist applied at the end, and most of those checklists are theatrical — they make the engagement look careful without actually changing the threat surface. Real front-end security is architectural: decisions made in the first week about where tokens live, where state resets happen, where the network chokepoint sits, and what's deliberately out of scope. The checklist-at-the-end approach can't fix those decisions later. We make them first.

If you're building an application where user trust is load-bearing — neobank, fintech, healthtech, legal, any product where a security incident is also a brand incident — this is the engagement we're built for. The first call is a 30-minute working session where we look at your threat model, your compliance perimeter, and your current architecture, and tell you honestly whether the gaps we'd find are closeable in the engagement window you have. If the right answer is "you need a security firm, not a front-end studio," we'll say so in the call. Straight answers only.

See if your security posture is where you think it is.