Skip to content
All case studies
Payments / Fintech·Senior Full-Stack Developer·2025

Multi-factor auth on a system that had none

The legacy platform had a single password gate and no second factor. I built multi-factor authentication end to end - email and authenticator-app OTP, the enrolment flow, and environment-aware password recovery - on a new stack, as a clean vertical slice from database to route.

2
second factors: email OTP and authenticator app
0 → full
MFA where the legacy system had a bare password gate
Vertical
slice owned end to end: entity, repository, service, handler, route

Stack

PHP 8.4MezzioDoctrine ORMTOTPOpenAPIPest

In one line

I added multi-factor authentication to a payments platform that had only ever had a password, building the whole feature as one clean vertical slice - entity, repository, service, handler, route, email template and config - rather than bolting a second factor onto a login flow that was never designed for one.

The problem

The legacy platform authenticated users with a password and nothing else. For a system sitting in front of payments data, a single factor is the kind of gap that turns one leaked credential into a full account compromise. The new stack needed real multi-factor authentication, and it needed to be something a user could actually turn on and use, not a checkbox.

There was no existing second-factor code to migrate. This was net-new security functionality on a fresh architecture, which is the good news and the responsibility at once: nothing to copy, nothing to blame, and no room to get an auth flow subtly wrong.

What I built

I owned the feature as a vertical slice, top to bottom, so every layer was consistent with the platform's conventions rather than special-cased.

The second factors

Two, so users are not forced onto one channel:

  • Email OTP - a one-time code delivered by email, with its own message template, for users without an authenticator app.
  • Authenticator-app OTP - a time-based one-time password (TOTP) for users who want a proper second factor that does not depend on email delivery.

The enrolment flow

MFA is only useful if turning it on is a real flow, not a database flag someone sets by hand. I built activation via both email and the mobile app, tracked with an isMfaActivated field on the user, so the system knows a user's true MFA state and enforces it on subsequent logins.

The vertical slice

The one feature touched every layer, and I built each to the platform's standard:

  • Entity - the MFA state on the user record.
  • Request / response DTOs - typed contracts for the OTP exchange, carrying the OpenAPI metadata the rest of the platform relies on.
  • Repository and service - the OTP lifecycle, kept behind a service boundary rather than smeared through the handler.
  • Handler and route - the authenticate-mfa and send-code endpoints, named and shaped like every other endpoint on the platform.
  • Email template and config - so the OTP email is a first-class, configurable part of the system.

Environment-aware recovery

Password recovery is the soft underbelly of any auth system. I made the forgot-password flow environment-specific, so a reset link generated in one environment cannot be used to walk into another - a small detail that is exactly the kind of thing that goes wrong when password reset is treated as an afterthought.

Why it mattered

It closed a real security gap on a payments system. Going from a bare password to genuine multi-factor authentication is the difference between one leaked credential being a catastrophe and being an inconvenience.

It was built to the platform's conventions, not around them. Because the feature went through the same DTO, service and OpenAPI patterns as everything else, it did not become a special case that the next person has to learn separately. It reads like the rest of the system.

It gave users a real choice. Two second factors, an actual enrolment flow, and recovery that does not undermine the thing it is recovering - the feature is usable, not just present.

What I would tell someone starting this

Build auth as a vertical slice, not a bolt-on. The failures in authentication systems live in the seams - between the token and the session, between reset and login. Owning every layer of the one feature is how you keep the seams honest.

Password recovery is part of the auth system, not a footnote. The strongest second factor in the world does not help if the reset flow is a side door. Make it environment-aware and treat it with the same care as the login path.

Let’s build something that lasts

Hiring a senior engineer or architect for a remote team? I work across EU and US time zones - tell me what you're building.

Newsletter

I write about software architecture, PHP, Vue, TypeScript, and developer experience. No spam, unsubscribe anytime.

Copyright © 2026. All rights reserved.