I’ve been learning event sourcing with Commanded lately and wanted to double-check when to hash a password — I think I already know the answer though!
I have an
Accounts context with
Credential aggregates. The
RegisterUser command is passed the users name, email address and plain-text password. The
User aggregate only handles their name, with email and password heading to the
I had originally planned to hash the password in the
CreateCrendential command, as that seems the most logical place. However, because the
CreateCrendential command is triggered by the
UserRegistered event, that would mean that the unhashed password would get serialised into that event — obviously a big no-no!
That leaves me with two options:
- Hash the password as soon as it hits my system, which at the moment would be in the
- Change the order. The form submission (or endpoint submission) issues the
CreateCredential command, which hashes the password (more logical place for this to take place) and then the
RegisterUser command responds to the
Option 1 feels like a bodge, so I’m inclined to go with option 2. Only issue is that the
User UUID is created/assigned in the
CreateCredential command, rather than the
RegisterUser command. I don’t see this as much of an issue though as both aggregates would store this UUID anyway.
Thoughts? Does changing the order of commands/events make the most sense and avoid storing unhashed passwords in a logical way?
When it comes to passwords and security, you want your passwords to be hashed as soon as possible. Having your UI sending an un-encrypted password to your backend for processing is a big no no, as it makes your system vulnerable to man in the middle attacks.
I would personally go for option 2.
Then you loose any ability to validate your password against length and complexity. That, for me, is a big no no. And please, please don’t respond with “client validation”.
He is right, sending unencrypted passwords is a NONO, no one should use HTTP anymore with letsencrypt beein available for free.
But I do support you, in the regard, that one should not hash client side. This will not only make you loose the validation opportunities as you say, but it will basically leak the hashing secret…
Or even worse, the hash itself becomes volatile as a plain text password, and an attacker that gains access to the database gains access to all user accounts.
Hashing client side means, that the hash is the “new password” and a MITM attack would just work fine. All ya need is the hash then.
Frontend validation also has its issues (I’ll give you that), as it can easily be bypassed. I picked option 2 because from reading the question I understood I could only pick 1 or 2. But it really does depend on the case. If your UI is talking to a public REST API, you may want to have it in both places.
If your app is being used inside a company within their private intranet, UI validation is likely good enough.
It all comes down where you want your validation to happen - sometimes you may even want validation on both sides:
The UI has to communicate somehow with the backend and at some point, something is being sent to the server. If it’s the user-entered password or the generated hash, doesn’t matter since a MITM attack works for both.
I just edited my post to remove that comment. It figures your typing skills would superseed mine xD
I’m actually wondering how this went to a client/server side discussion. The initial poster asked about eventsourcing and how to structure password hashing in an event sources system without storing the pw in an event. There’s no “client” involved at that stage. It’s all server side.
Likely my fault, since I miss understood the question and then directed the topic in the wrong direction from that point forward =(
I will show myself out.
And I got triggered. So, lets agree on 50/50.
I’m glad this thread got some discussion going, but as @LostKobrakai said, I’m surprised we went to client/server so quickly! Whilst not part of my original question, I’ll likely be using plain old form submissions (over HTTPS of course) or possibly Phoenix LiveView. That said, LV would be of less value in this instance as there’s not much I can validate in the registration form beyond unique email addresses which CQRS makes too cumbersome for instant validation to be practical, and password length/unique characters etc.
Anyway, I decided that for the time being I would roll
Credentials into the
User schema. Either of the approaches that I listed above comes with caveats and compromises, mainly due to the password hashing in events (as already explained) and the complexity of unique validation of email addresses (mentioned above).
I suspect that in Commanded parlance I’ll be looking to use a process manager so that I can respond appropriately to errors caused by duplicate emails before any other, related events get saved. At this stage, simply combining the two schemas is the most pragmatic solution until I’m more comfortable with ES/CQRS.
I’m not sure why you dismiss option 1 so fast. A small variation of option 1 might be to hash your password even before it hits the first command. That would mean that in your original design the
RegisterUser command has a user name, email and hashed password`.
In that case you don’t have any risks of saving the plain text password anywhere because it won’t enter you commands/events unhashed.
That’s roughly what I started with, and subsequently ended up with after discarded the additional
My public API in
Accounts has a
register_user function which just takes a map of attributes. These attrs are then piped through various functions which do things like assign a UUID, add a
registration_date, downcase the email address and hash the password before the command is dispatched.
All these functions are scoped to the
RegisterUser module which is why I initially felt option one was undesirable as there was going to be a
CreateCredential module that shared some of the same functions, either duplicating them or referencing another, only semi-related command. It was an issue of semantics and DRY which largely became moot once I ditched the extra event.