Thinking about User <> Auth modelling in Go

I’m currently in the midst of writing a webserver using Go for the backend. While I’ve been using Go for a while, it usually involved contributing to a already-setup code base. So, when I started writing my own server from scratch, I started pondering a lot over the architecture and structure of my code.

One of the starting points has been thinking about the User model and how this ties to the Authentication system for the webserver. Our initial requirements for both being:

Authentication System

  • Users should be able to login via email, password
  • Users should be able to reset password, logout
  • Users should also be able to login using OAuth via providers like Google, Facebook, etc.
  • A particular user should be able to use any of the methods and still be able to login to the same user account.

User Model

  • Users should have an email, first, last name
  • Eventually we’ll build a profile around the user which will hold more PII data (like phone number, height etc).

Authentication

Let’s first tackle the authentication model. First let us capture the class structure that both the OAuth and local Email/Password would require.


auth class model

OAuth

From the class diagram we can see what data we require from OAuth providers (we’re ignoring data like access tokens, refresh tokens, which are actually required for the OAuth protocol here and focusing on the user related data). Here the User ID is a unique ID which the provider gives, we can use this to associate to an individual user.

Email/Password

The user provides us with all the data here. The username is something which we use as a fallback in case the user forgets their email.

User Model

Now let’s extract the common data from the above model (data which is PII) and extract out our user model.


user class model

Benefits

  • Clear segregation of User and Authentication system
  • All PII data is held within the User model
  • Multiple Providers and the internal auth system can all be mapped to a single user
  • Potentially allows multiple emails to be linked to the same user
  • Login doesn’t require DB queries to the user model. Nice!

Challenges

  • Data duplication, the email field is duplicated in both the user and email/password models
  • A user with the same email but different OAuth providers will be linked to the same account. Single user is enforced by design.

Go

Now that we have some clarification over what our DB models look like, it’s time to think of a high-level design (obviously with idiomatic Go) for our authentication flow.

Some early decisions:

  1. We’ll be using github.com/markbates/goth to handle OAuth
  2. We’ll be using JWTs instead of server side sessions. This reduces infrastructure dependency and allows stateless propagation of user state.

With this, there are two flows that we primarily need to adhere to:

  • Register
    • This happens when the user is creating his/account for the first time
    • The user model is assumed to be non-existent and hence should be created in this flow
  • Login
    • This is the flow the user follows every other time post register
    • The user model should already exist in this flow

Talking in terms of DDD, let’s say that the User context, provides a simple functionality to create a User. The function creates a new User from the data provided and returns the newly create User’s ID.

func CreateUser(user *User) (int, error)

Then we need all our authentication to satisfy the following interface.

type createUserFn func(user *User) (int, error)

type Auth interface {
    Register(createUser createUserFn) func(w http.ResponseWriter, r *http.Request)
    Login() func(w http.ResponseWriter, r *http.Request) 
}

A sample implementation for the password based auth would be

type PasswordAuth struct{}

func (p *PasswordAuth) Register(createUser createUserFn) func(w http.ResponseWriter, r *http.Request) {
    return func(w http.ResponseWriter, r *http.Request) {
        user, email, password := parseData(r)
        
        userId := createUser(user)
        hashedSaltedPass := saltHashPassword(password)
        p.storeToDB(email, hashedSaltedPass)
        
        // Trigger Email for verification
        go p.sendEmailForVerification(email)
        
        // Get JWT Cookie
        token, expiry := jwt.CreateToken(userId)
        
        // Set JWT Token
        http.SetCookie(w, &http.Cookie{
		    Name:    "token",
		    Value:   token,
		    Expires: expiry,
	    })
    }
}

func (p *PasswordAuth) Login() func(w http.ResponseWriter, r *http.Request) (int, error) {
    return func(w http.ResponseWriter, r *http.Request) {
        _, email, password := parseData(r)
        
        hashedSaltedPass := saltHashPassword(password)
        userId := p.retreive(email, password)
        
        // Get JWT Cookie
        token, expiry := jwt.CreateToken(userId)
        
        // Set JWT Token
        http.SetCookie(w, &http.Cookie{
		    Name:    "token",
		    Value:   token,
		    Expires: expiry,
	    })
    }
}