DSA Guide
System Design

Security

Authentication versus authorisation, sessions and tokens, what encryption does and does not cover, and the attacks worth designing against

Security is not a component you add at the end. It is a set of decisions spread through the design, and most of them are cheap if made early and expensive afterwards.

Authentication and Authorisation

Two different questions, frequently conflated.

AuthenticationAuthorisation
QuestionWho are you?What may you do?
HappensOnce per sessionOn every request
Failure code401 Unauthorized403 Forbidden

Authorisation must be checked on every request, at the server

The most common serious flaw in real systems is checking permission when rendering the UI and not when serving the data.

GET /api/orders/12345

If the server does not verify that order 12345 belongs to the caller, anyone can read anyone's orders by changing the number — regardless of what the interface shows.

Hiding a button is not authorisation. Every endpoint must independently answer "is this caller allowed to touch this record?"

Sessions or Tokens

Once someone has logged in, how does the next request prove it?

Server sessionSigned token (JWT)
State heldOn the serverIn the token itself
ScalingNeeds shared storageStateless
RevocationImmediate — delete itHard — valid until expiry
SizeSmall idLarger, sent every request
SuitsTraditional appsAPIs, mobile, service-to-service

You cannot revoke a JWT, and people forget this

A signed token is valid until it expires because the server holds no record of it. Logging out, banning an account, or revoking a permission does not stop a token already issued.

Standard mitigations:

  • Short expiry (5–15 minutes) plus a long-lived refresh token that is stored server-side and can be revoked.
  • A denylist of revoked token ids — which reintroduces the state you were avoiding, but only for the few that are revoked.

Say which you are using. "Stateless JWTs" without a revocation answer is an incomplete design.

OAuth in one paragraph

OAuth 2.0 lets a user grant an application limited access to their account elsewhere without sharing their password. The app is redirected to the provider, the user approves, and the app receives a token scoped to specific permissions. The password never reaches the app, and access can be withdrawn without a password change.

OpenID Connect is a thin layer on top that adds "who is this user?" to OAuth's "what may this app do?"

Storing Passwords

Never:      store the password
Never:      encrypt it (encryption is reversible)
Never:      md5 or sha256 alone (far too fast to compute)

Correct:    a slow hash designed for passwords
            bcrypt, scrypt, or Argon2, with a per-user salt

Slowness is the feature

General-purpose hashes are built to be fast, which is exactly wrong here — a modern GPU tries billions of SHA-256 guesses per second.

bcrypt and Argon2 are deliberately slow and memory-hungry. Tuned so one hash takes about 100 ms, a login stays instant while brute-forcing becomes impractical.

The salt — random data per user — ensures two identical passwords hash differently, so one precomputed table cannot crack the whole database.

Encryption

In transitAt rest
Protects againstNetwork eavesdroppingStolen disks and backups
MechanismTLSDisk or field-level encryption
CoversThe connectionThe stored bytes

Encryption at rest protects less than people think

It protects against someone physically taking the disk or a leaked backup file.

It does not protect against a compromised application, SQL injection, or stolen credentials — in all of those the attacker goes through the database, which decrypts transparently for them.

It is worth having and it is not a substitute for access control. Claiming data is safe "because it is encrypted at rest" misstates the threat it addresses.

For genuinely sensitive fields, encrypt at the application level so the database only ever holds ciphertext. Then a database compromise alone is not enough.

The Attacks Worth Designing Against

AttackWhat happensDefence
SQL injectionInput becomes part of a queryParameterised queries — never string concatenation
XSSAttacker's script runs in a victim's browserEscape output; Content-Security-Policy
CSRFA victim's browser makes an authenticated request unknowinglySameSite cookies, anti-CSRF tokens
SSRFThe server is tricked into fetching an internal URLAllowlist outbound destinations
Insecure direct object referenceChanging an id reads someone else's dataCheck ownership per request
Credential stuffingPasswords leaked elsewhere are tried hereRate limiting, MFA, breach-list checks
DDoSTraffic floods the systemCDN absorption, rate limits, load shedding

Two rules that prevent most of the list

Never build a query or a document by concatenating user input. Parameterised queries make injection structurally impossible rather than merely unlikely.

Treat all input as hostile, including from your own services. A compromised internal service is exactly how an attacker moves sideways, and "it came from inside the network" has stopped being a trust signal.

Defence in Depth

Assume every layer will eventually fail, and make sure no single failure is fatal.

Edge         →  DDoS protection, rate limiting, WAF
Gateway      →  authentication, coarse authorisation
Service      →  fine-grained authorisation on every request
Database     →  least-privilege accounts, encryption at rest
Operations   →  audit logs, secret rotation, monitoring

Secrets do not belong in the repository

API keys, database passwords and signing keys committed to a repository stay in the git history forever, even after being deleted in a later commit.

Use a secrets manager, inject at runtime through the environment, and rotate on a schedule. A leaked key you can rotate in minutes is an incident; one embedded in a hundred deployments is an outage.

Privacy Obligations

Regulation aside, these are design constraints and are easier to build in than to retrofit:

RequirementDesign consequence
Deletion on requestYou must be able to find every copy — including caches, backups, analytics, logs
Data minimisationDo not collect what you do not need; every field is a liability
Purpose limitationData gathered for one purpose should not silently power another
Audit trailWho accessed what, and when

'Delete my data' is hard once data is denormalised

A user's name copied into a cache, a feed, a search index, an analytics warehouse and last month's backup must be removed from all of them.

Two approaches: keep personal data in one place and reference it by id everywhere else, or maintain a documented inventory of every location.

The first is far easier, and it is a good argument against copying personal fields across service boundaries even when duplication is otherwise convenient.

On this page