[{"data":1,"prerenderedAt":210},["ShallowReactive",2],{"case-study:multi-factor-auth-from-scratch":3},{"id":4,"title":5,"body":6,"description":175,"draft":176,"extension":177,"industry":178,"meta":179,"metrics":180,"navigation":190,"order":191,"path":192,"role":193,"seo":194,"stack":195,"stem":202,"tags":203,"year":208,"__hash__":209},"caseStudies\u002Fcase-studies\u002Fmulti-factor-auth-from-scratch.md","Multi-factor auth on a system that had none",{"type":7,"value":8,"toc":160},"minimark",[9,14,18,22,25,28,32,35,40,43,60,64,72,76,79,115,119,122,126,132,138,144,148,154],[10,11,13],"h2",{"id":12},"in-one-line","In one line",[15,16,17],"p",{},"I added multi-factor authentication to a payments platform that had only ever had\na password, building the whole feature as one clean vertical slice - entity,\nrepository, service, handler, route, email template and config - rather than\nbolting a second factor onto a login flow that was never designed for one.",[10,19,21],{"id":20},"the-problem","The problem",[15,23,24],{},"The legacy platform authenticated users with a password and nothing else. For a\nsystem sitting in front of payments data, a single factor is the kind of gap that\nturns one leaked credential into a full account compromise. The new stack needed\nreal multi-factor authentication, and it needed to be something a user could\nactually turn on and use, not a checkbox.",[15,26,27],{},"There was no existing second-factor code to migrate. This was net-new security\nfunctionality on a fresh architecture, which is the good news and the\nresponsibility at once: nothing to copy, nothing to blame, and no room to get an\nauth flow subtly wrong.",[10,29,31],{"id":30},"what-i-built","What I built",[15,33,34],{},"I owned the feature as a vertical slice, top to bottom, so every layer was\nconsistent with the platform's conventions rather than special-cased.",[36,37,39],"h3",{"id":38},"the-second-factors","The second factors",[15,41,42],{},"Two, so users are not forced onto one channel:",[44,45,46,54],"ul",{},[47,48,49,53],"li",{},[50,51,52],"strong",{},"Email OTP"," - a one-time code delivered by email, with its own message\ntemplate, for users without an authenticator app.",[47,55,56,59],{},[50,57,58],{},"Authenticator-app OTP"," - a time-based one-time password (TOTP) for users who\nwant a proper second factor that does not depend on email delivery.",[36,61,63],{"id":62},"the-enrolment-flow","The enrolment flow",[15,65,66,67,71],{},"MFA is only useful if turning it on is a real flow, not a database flag someone\nsets by hand. I built activation via both email and the mobile app, tracked with\nan ",[68,69,70],"code",{},"isMfaActivated"," field on the user, so the system knows a user's true MFA state\nand enforces it on subsequent logins.",[36,73,75],{"id":74},"the-vertical-slice","The vertical slice",[15,77,78],{},"The one feature touched every layer, and I built each to the platform's standard:",[44,80,81,87,93,99,109],{},[47,82,83,86],{},[50,84,85],{},"Entity"," - the MFA state on the user record.",[47,88,89,92],{},[50,90,91],{},"Request \u002F response DTOs"," - typed contracts for the OTP exchange, carrying the\nOpenAPI metadata the rest of the platform relies on.",[47,94,95,98],{},[50,96,97],{},"Repository and service"," - the OTP lifecycle, kept behind a service boundary\nrather than smeared through the handler.",[47,100,101,104,105,108],{},[50,102,103],{},"Handler and route"," - the ",[68,106,107],{},"authenticate-mfa"," and send-code endpoints, named\nand shaped like every other endpoint on the platform.",[47,110,111,114],{},[50,112,113],{},"Email template and config"," - so the OTP email is a first-class, configurable\npart of the system.",[36,116,118],{"id":117},"environment-aware-recovery","Environment-aware recovery",[15,120,121],{},"Password recovery is the soft underbelly of any auth system. I made the\nforgot-password flow environment-specific, so a reset link generated in one\nenvironment cannot be used to walk into another - a small detail that is exactly\nthe kind of thing that goes wrong when password reset is treated as an\nafterthought.",[10,123,125],{"id":124},"why-it-mattered","Why it mattered",[15,127,128,131],{},[50,129,130],{},"It closed a real security gap on a payments system."," Going from a bare password\nto genuine multi-factor authentication is the difference between one leaked\ncredential being a catastrophe and being an inconvenience.",[15,133,134,137],{},[50,135,136],{},"It was built to the platform's conventions, not around them."," Because the\nfeature went through the same DTO, service and OpenAPI patterns as everything\nelse, it did not become a special case that the next person has to learn\nseparately. It reads like the rest of the system.",[15,139,140,143],{},[50,141,142],{},"It gave users a real choice."," Two second factors, an actual enrolment flow, and\nrecovery that does not undermine the thing it is recovering - the feature is\nusable, not just present.",[10,145,147],{"id":146},"what-i-would-tell-someone-starting-this","What I would tell someone starting this",[15,149,150,153],{},[50,151,152],{},"Build auth as a vertical slice, not a bolt-on."," The failures in\nauthentication systems live in the seams - between the token and the session,\nbetween reset and login. Owning every layer of the one feature is how you keep\nthe seams honest.",[15,155,156,159],{},[50,157,158],{},"Password recovery is part of the auth system, not a footnote."," The strongest\nsecond factor in the world does not help if the reset flow is a side door. Make it\nenvironment-aware and treat it with the same care as the login path.",{"title":161,"searchDepth":162,"depth":162,"links":163},"",2,[164,165,166,173,174],{"id":12,"depth":162,"text":13},{"id":20,"depth":162,"text":21},{"id":30,"depth":162,"text":31,"children":167},[168,170,171,172],{"id":38,"depth":169,"text":39},3,{"id":62,"depth":169,"text":63},{"id":74,"depth":169,"text":75},{"id":117,"depth":169,"text":118},{"id":124,"depth":162,"text":125},{"id":146,"depth":162,"text":147},"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.",false,"md","Payments \u002F Fintech",{},[181,184,187],{"value":182,"label":183},"2","second factors: email OTP and authenticator app",{"value":185,"label":186},"0 → full","MFA where the legacy system had a bare password gate",{"value":188,"label":189},"Vertical","slice owned end to end: entity, repository, service, handler, route",true,7,"\u002Fcase-studies\u002Fmulti-factor-auth-from-scratch","Senior Full-Stack Developer",{"title":5,"description":175},[196,197,198,199,200,201],"PHP 8.4","Mezzio","Doctrine ORM","TOTP","OpenAPI","Pest","case-studies\u002Fmulti-factor-auth-from-scratch",[204,205,206,207],"Security","Authentication","Backend","API Design","2025","TkVoDNwzWp7dR5AWF7emPpmgXUQrDF_IBe1ZL5i5G84",1787792667894]