Stage 7 · Master
Authentication & Authorization
Password Hashing & Issuing JWTs
Store credentials as slow hashes, then trade them for short-lived gateway tokens instead of long-lived session state.
Why Passwords and Tokens Are Separate Problems
The first mistake I wanted to avoid in Fieldwork was treating authentication as one blob called auth. Storing a password safely and proving a caller is authenticated on every request are related, but they are not the same design problem. Password storage is about surviving a database leak. Access tokens are about keeping request-time checks cheap enough that the gateway can do them for every call without turning the login table into a hot path.
That distinction mattered because Fieldwork is a multi-tenant workspace and task-tracking API. A successful login is not the end of the story; every later request still needs a tenant-aware identity that the gateway can validate before proxying to tasks, projects, or workspace services. If we tied request authentication directly to the users table on every call, we would turn a normal dashboard refresh into repeated credential lookups for no gain.
So the decision split cleanly: bcrypt for stored credentials, and short-lived signed JWT access tokens issued by the gateway after a successful login. I did look at Argon2id for password hashes, because on paper its memory hardness is attractive. I did not pick it here because the concrete cost in this system was operational unevenness at the gateway login path: memory-heavy verification during traffic spikes, mixed library support across tooling, and more migration complexity for a course codebase where the point is to make trade-offs legible. bcrypt's CPU-bound cost model is easier to budget and still far better than any fast hash.
The normal user only feels the hash cost at login or password change. The attacker feels it on every guessed password. That is the asymmetry you are buying when you choose a deliberately slow password hash.
Hashing Passwords for a Real Login Path
Fieldwork never stores a password, only a bcrypt digest. The user record carries email, display name, tenant membership metadata, and the password hash. That sounds obvious, but the implementation detail that mattered was where hashing lives. I kept it in the authentication service rather than on the DTO or repository layer, because the cost factor is policy, not persistence. Repositories should save bytes; services should decide how those bytes get produced.
The registration flow also normalizes email before hashing and saving. That prevents the subtle bug where two user records differ only by casing and later authenticate inconsistently. I rejected the idea of letting the database or ORM helper magically do this work, because password policy, normalization, and future rehash logic belong in one obvious place that can be reviewed together.
| Decision point | Rejected approach | Fieldwork approach |
|---|---|---|
| Password algorithm | Fast hash like SHA-256 with a salt | bcrypt with a tuned cost so guessing stays expensive |
| Where hashing happens | Repository hook or model helper | Auth service so policy lives next to validation |
| Stored data | Password plus transform metadata spread across columns | Single bcrypt digest string on the user row |
Verifying Credentials Without Leaking State
Login verification is deliberately boring: load the user by normalized email, compare the submitted password against the stored digest, and return the same failure envelope whether the email was wrong or the password was wrong. The boring part matters. In a multi-tenant product, an account enumeration bug leaks more than a login detail; it leaks who exists inside a customer workspace.
I also avoided baking tenant selection into the password verification step. A user can belong to multiple tenants through memberships, so the credential check answers only one question: is this user who they claim to be? Tenant context comes next, when the gateway decides which tenant the session is operating in and what memberships the subject has there. Mixing those concerns into password verification would create edge cases the first time a user belongs to two customer accounts.
Once the bcrypt comparison is done, the hash stops being useful. Return a stripped authenticated subject, not a full user model that can wander into logs, traces, or JSON by accident.
Issuing Short-Lived Access Tokens at the Gateway
After credentials succeed, the gateway issues the access token. That was a deliberate boundary choice. Internal services do not need to know how passwords are checked, how refresh tokens rotate, or which signing key is active this week. They need a validated subject with tenant, membership, and permission claims that already passed through one trusted choke point.
I rejected opaque session IDs backed by a database lookup on every request. That would simplify revocation, but it would also force the public entrypoint to hit storage before forwarding every call to projects or tasks. Fieldwork's gateway already has to parse rate limits, tenant headers, and RBAC requirements. Adding a database round trip to every authenticated request would make the gateway slower and more fragile right where fan-out begins. A signed JWT lets the gateway validate locally and attach the resulting identity to downstream calls.
- Keep access tokens short-lived so leaked bearer tokens expire quickly without an immediate database hit.
- Put tenant and permission claims in the token because Fieldwork routes and authorizes at the gateway before reaching internal services.
- Treat JWT issuance as a gateway responsibility, not something every service reimplements with its own signing rules.
What We Decided for Fieldwork
Fieldwork stores user credentials as bcrypt hashes with a cost we can afford on the login path and raise over time. Authentication verifies only identity, not tenant selection or authorization. Once that succeeds, the API gateway issues a short-lived JWT carrying the tenant and permission context needed for normal request handling.
That gives us the shape we wanted: slow, defensive password verification at the edge of login; fast, local token validation at the edge of every request. The rejected alternatives were not wrong in the abstract. Argon2id may be a future upgrade, and opaque sessions are useful in systems that prioritize central revocation over stateless verification. But for Fieldwork's multi-tenant gateway, bcrypt plus short-lived JWTs was the cleaner operational fit, and more importantly, it keeps the design legible as the course moves into refresh token rotation and RBAC.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.