From 2641c6d91895487d421d0fb76712a76e1f908378 Mon Sep 17 00:00:00 2001
From: Marco Pivetta
+ Yesterday, I had a
+ quite chatty discussion
+ about how to approach server-side validation in HTTP-based applications.
+
+ DISCLAIMER:
+ this article is in no way suggesting to remove server-side validation.
+ If your customers can open a debugger and modify information that is directly used by your core
+ processes, you already lose. There is no such thing as "validating in the client".
+
+ For the sake of simplicity, we will design the familiar authentication domain in our examples:
+
+ In such a domain, we will likely design our core logic with following
+ repository and aggregate:
+
+ The focus of the discussion was around SPA design,
+ but this article will try to keep things even more simplified.
+
+
+
+
+ This is the kernel of our extremely simplistic domain. + Still, some concepts are under-specified: +
+ +Johann Gambolputty de von Ausfern- schplenden- schlitter- crasscrenbon- fried- digger- dingle- dangle- dongle- dungle- burstein- von- knacker- thrasher- apple- banger- horowitz- ticolensic- grander- knotty- spelltinkle- grandlich- grumblemeyer- spelterwasser- kurstlich- himbleeisen- bahnwagen- gutenabend- bitte- ein- nürnburger- bratwustle- gerspurten- mitz- weimache- luber- hundsfut- gumberaber- shönedanker- kalbsfleisch- mittler- aucher von Hautkopft of Ulm? a valid username?+ The answers to these questions is quite obvious to developers that implemented this same + logic multiple times over the years, but they are still assumptions in our mental model. + + Let's instead write them down: +
+ +~~~php + + That's better! We should also define some invariants: + + +~~~php + 200) { + throw EmailAddressTooLong::fromString($email); + } + + $instance = new self(); + + $instance->email = $email; + + return $instance; + } +} +~~~ + +
+ That solved a few problems, as we now know that we only accept sensible
+ email addresses, and we also made it clear that in our system, the concept of username
+ and email somehow overlap.
+
+ Our system now rejects short passwords completely: that is a security constraint that + we really need to define to prevent empty strings flying around and causing unexpected + chaos. +
+ +
+ WARNING:
+ please do not add silly password policies other than a minimum length: it will lead
+ simply lead to people typing in horrors abc123!!.
+
+ WARNING: + do NOT add the password to the thrown exception details, as exceptions + are usually to be logged. +
+ +
+ We can now adapt our User aggregate to rely on these invariants:
+
User instances with the same Username
+ exist within our system? Absolutely not! We can fix this by combining a read model
+ with our aggregate named constructor:
+
+
+~~~php
+username = $username;
+ $instance->passwordHash = $hashPassword($password);
+
+ return $instance;
+ }
+
+ public function authenticate(
+ PlainTextPassword $password,
+ VerifyPassword $verifyPassword
+ ) : bool {
+ return $verifyPassword($password, $this->passwordHash);
+ }
+}
+~~~
+
++ That's it: that's our simplistic authentication system, and you can use it in a CLI + or HTTP application without any particular added validation needed. We will implement + a naive application through PSR-15 request handlers. +
+ ++ Registration is as simple as this: +
+ +~~~php +users = $users; + $this->userExists = $userExists; + $this->hashPassword = $hashPassword; + } + + public function handle(ServerRequestInterface $request) : ResponseInterface + { + Assert::postRequest($request); + + $postData = $request->getParsedBody(); + + $this->users->store(User::register( + Username::fromEmailAddress($postData['email']), + PlainTextPassword::fromPlainText($postData['password']), + $this->hashPassword, + $this->userExists + )); + + return new TextResponse('Registered! We sent you some spam, and subscribed you to our 10000 SEM campaigns'); + } +} +~~~ + ++ Login is also straightforward: +
+ +~~~php +users = $users; + $this->userExists = $userExists; + $this->verifyPassword = $verifyPassword; + $this->sessionHelper = $sessionHelper; + } + + public function handle(ServerRequestInterface $request) : ResponseInterface + { + Assert::postRequest($request); + + $postData = $request->getParsedBody(); + + $username = Username::fromEmailAddress($postData['email']), + + if (! $this->userExists($username)) { + return new RedirectResponse('/login?failed=true', 401); + } + + $user = $this->users->get($username); + + if (! $user->authenticate( + PlainTextPassword::fromPlainText($postData['password']), + $this->verifyPassword + )) { + return new RedirectResponse('/login?failed=true', 401); + } + + return $this->sessionHelper->addIdentityTo(new RedirectResponse('/dashboard', 200), $username); + } +} +~~~ + ++ The point I am trying to make here is that we wrote an entire authentication + component with no validation components involved. +
+ ++ Try logging in with an invalid email: you will get a 500 error. +
+ ++ Try logging in with an invalid password: you will get a 500 error. +
+ ++ Try registering with an already existing user: you will get a 500 error. +
+ ++ Is this always desirable? Of course not, but for simple value validation, plain HTML + is more than sufficient: +
+ +~~~php + +