Strict types and DDD, from a security perspective.

Squeeds Julkalender | 2022-12-09 | Eric Andresen
Today we're going to talk about how with strict types on variables and DDD (Domain Driven Design) you can secure your code and make sure you only expose the information you intended, as well as create variables in your code that you can always trust. In many applications we must handle users, there first names, last names, emails and sometimes social security numbers, as well as user-rights. Let's look at how we can do this securely and efficiently.

Strict typing

What does a user object usually look like?

type User struct {
   FirstName    string
   LastName    string
   Email          string
   PIN             string (string to handle some special charters).

Let's look at the types first, email, is it really appropriate to save it as a string? It's an email we're going to store. Let's then create a type for that.

In this class we can have a validation that keeps track of the fact that it is actually a genuine email stored and nothing else. We can have a check so that it follows the patterns that a real email has. We can verify that it is a domain that exists. And we can easily write a method to extract just that part of an email.

    User.Email.domain() =

Often, you also want some form of validation that a person has confirmed that they own the email they entered when they registered. Then we can create a type that is for that particular use case, ValidEmail. If you make this field static and just let the ValideEmail type handle the crating and storing of the variable, then we know that we can trust this variable throughout the program.

The same applies to social security numbers. This also needs to be handled in many applications and we must be able to trust that it is correct, and follows the rules and formats that a certain country has. Is String a particularly good type for this then? I have come across several programs where 'User.PIN.IsValid()' is used in several places to constantly check that we have a genuine social security number. But if you, like with the email address above, store social security numbers in a strict type for this very purpose, then we know that we can always trust the social security number that comes with the user object.

So now we have something like this instead:

type User struct {
   Firstname    string
   Lastname    string
   Email          Email
   PIN             PIN
   AuthLevel    AuthLevel


Domain objects

When an object is crated of a certain type we can verify that this object contains correct and proper data as it should, before we even get down into our program. With this method, we avoid verifying in several places and know that all the data we receive follows the patterns we want, and we can trust that it is correct.

So now we've created a nice user object where we know we can trust all the information and fields in it, we know all the data doesn't change after it's created, and we have a good idea of exactly where it's being created. All of this is very good for internal use, but let's say a user wants to update their email and sees that the model sent down to our backend looks like this.

  "Firstname":   "John",
  "Lastname":    "Doe",
  "Email":          "",
  "PIN":             "700101-1010",
  "Auth_Level":  "User"

Creating a user object in the backend that we send to the frontend is a smooth thing because we know that all the data is exactly as it was when we read it in from the database. We can trust the information and that nothing has changed along the way. But we may not want to expose to the user where and how we store user rights or expose our data models for that matter. Also, we might now have the best way in handling how we receive data and just translate the data to a model structure that we have, so there is nothing preventing the user from setting "Auth" to "Admin" at the same time as he updates his email. This is not entirely unusual for us to share too much information up to the user that we think the front end will filter out. But we all know how easy it is to check what both calls and responses look like in plain text.

How do we solve that?

DTO (Data transfer object), we create an object specifically to share the information that we want to show to the user, or for the information that we want to receive from the user. In the case above, we have a user object (UserDTO) that only holds the variables that the customer can see or change.

type UserDTO struct {
   Firstname    string
   Lastname    string
   Email          Email

Then we have a method that we wrote that translates DTO objects to the internal objects we take care of with full control of all values and types. So, then it doesn't matter if a sneaky user sends more information because that field is not in our DTO object and is therefore not mapped into our internal user object.

So, for each new type that we know should belong to a specific purpose, it is convenient to collect all this in a separate package/class where you can have all the validation, write unique "equals" for comparison and much more that would be nice for this specific entity.


What can we take away from this?

In my opinion, having proper types and domain objects is super important. It will take a little longer to get started, but if you want to build code that is reliable, easily tested and easy to maintain, I can promise you that this structure will save you a lot of time in the long run.

Above all, you get a much more secure application, where you are in control of the information and you know exactly what is exposed to our users.

Best regards,

Eric Andresen