Building Authentication: Part 1

Building Authentication: Part 1

This post, I'll be going through how I designed the authentication parts of Reread.to. I decided to use Google, and Github as identity providers, to not having to deal with passwords. This article is the first part of a sub-series since it ended up being a bit too wordy and confusing. I decided to cut it into parts so I can focus on different aspects in each article. This part will deal with introducing OAuth, setting up the code layout, and starting the first part of the OAuth flow.

Why OAuth though? To be perfectly honest, I usually shy away from using OAuth in my apps because I personally don't use them when signing up for other services. I prefer using email addresses. Since I also don't like giving away my actual email, I use a tiny service to generate random email addresses for me, but that's a story for another time. That is not to say that others don't find it convenient though. So, for Reread, I decided to go for it because a) it was faster, b) I didn't have to store passwords, and c) I think few people have qualms about using third party OAuth providers. That being said, I also want to add a email signup option soon. One of those click to receive a sign-in link to reread options. I'll do that once I figure out how to send transactional emails.

User Management

Before we get into Authentication, I'll quickly go over the concept of a User within Reread. The structure right now is very simple, and I intend to keep it like this for the most part. The migration for creating the user table looks like this:

CREATE TABLE users (
  id serial PRIMARY KEY,
  username varchar(50) UNIQUE NOT NULL,
  fullname varchar,
  email varchar UNIQUE NOT NULL,
  auth_token varchar,
  auth_provider_name varchar,
  created_at timestamptz NOT NULL,
  updated_at timestamptz NOT NULL
);

The important thing here is the auth_token and the auth_provider_name fields. Since I want to be able to authenticate with multiple providers down the line, I want to keep this a bit opaque within the database. The authentication system should be able to deal with the per-provider specifics without having to pollute the database too much.

Since I discussed setting up the database in my previous post, I'll pick this up from where I left off.

The architecture I'm leaning towards is to create a bespoke models packages that will contain APIs to work with specific database tasks. I'm not going to be very strict with it, but it nicely wraps around the database functionality so my entire codebase isn't littered with database related code.

The User model's struct then looks something like this:

const UserTableName = "users"

type User struct {
    Username         string    `db:"username" json:"username"`
    Fullname         string    `db:"fullname" json:"fullname"`
    Email            string    `db:"email" json:"email"`
    AuthToken        string    `db:"auth_token" json:"-"`
    AuthProviderName string    `db:"auth_provider_name" json:"-"`
    CreatedAt        time.Time `db:"created_at" json:"-"`
    UpdatedAt        time.Time `db:"updated_at" json:"-"`
}

and I've so far created the following APIs on it:

func CreateUser(ctx echo.Context, user User) error
func GetUserFromUsername(ctx echo.Context, username string) (User, error)
func GetUser(ctx echo.Context, id int) (User, error)

I won't dive too much into the implementation details for these, and their names are pretty descriptive already. Also, I didn't write all CRUD operations yet, since I didn't need them yet.

OAuth Providers

OAuth is an authorization protocol that allows services to communicate with each other on a specific user's behalf. In order to explain this better, I'll walk through a contrived example.

I want my internet provider to fix an issue with my internet router. In order to do that, they would have to come to my house. To let them in, I have the following options: be present, and allow them in, give them the house keys, or ask someone I trust to let them in.

Giving them the house keys is the least secure method, followed by someone I trust let them in, followed by me allowing them in myself. Now, consider Reread to be the router repairperson, and your house to be Google. Reread could technically ask for my Google password and then check within Google what my name and email is, but that's like giving Reread -- a service I don't trust -- my keys to Google, which contains my information.

The alternative is to let Reread view my information contained in Google by a trusted third party. This can be done by letting Reread talk to a personal server I own, which then talks to Google on my behalf. The last one, personally handling data is not yet possible until we become internet native beings.

The solution that a lot of services have agreed on, is like a 'secret handshake' kind of protocol. There are three parties in this conversation: the User, the Requester, and the Provider. The User is the person who would like to sign in, the Requester is Reread, the service that requires the User's data, and the Provider is the service that contains the User's data, like Google. The secret handshake has three steps:

  • The User clicks 'Sign up' or 'Sign in' via Provider on the Requester site, and the Requester redirects them to the Provider's authorization page.
  • The Provider asks the User if they want to give Requester access to their data.
  • If the user agrees to sharing their data, the Provider calls back the Requester and gives them a temporary set of keys that allows the Requester to read the User's data from the Provider.

This is over-simplifying the process, of course, but for the purposes of getting started, that's enough.

The OAuth process will require changes to the API server mostly, but there will be a bit some changes to the NextJS frontend app too. To start off, I'll create a basic 'Sign In' page.

Reread sign in page

The relevant markup from that page is like so:

<li>
    <Button href="/api/auth/google">
        <>
            <Image .../>
            <span>Sign in with Google</span>
        </>
    </Button>
</li>
<li>
    <Button href="/api/auth/github">
        <>
            <Image ... />
            <span>Sign in with Github</span>
        </>
    </Button>
</li>

Each button will redirect the user to /api/auth/<provider name>. As mentioned earlier, I've told NextJS to rewrite any calls to /api to point to localhost:8080 which is the Go API server. If a user then clicks on 'Sign in with Google', they will be redirected to http://localhost:8080/auth/google. I will now create these URLs in the API server.

Authentication Routes

Within the Go server, I created the following routes:

    authRoute := e.Group("/auth")
    authRoute.GET("/:provider", handler.authSetupHandler)
    authRoute.GET("/:provider/callback", handler.authCallbackHandler)
    authRoute.GET("/user", handler.getUserHandler)
    authRoute.GET("/logout", handler.logout)

Where e represents the Echo server object. In the above snippet, I create a group of routes which will all automatically get the prefix /auth, and add a route for when the user starts to authenticate with a specific provider, a route for when the Provider calls us back on completion of the authorization process, a route for getting the current user, and a route for logging out the user. I'll go into more detail with these routes, but that's all I need to do user sign in, check authentication status, and sign them out.

A curious thing in the above snippet is the :provider. The :<name> tells the router to treat this part of the URL as a variable and give it a name. The variable can be accessed by the route handler by using the context, like so: context.GetParam("provider"). This way, I can make dynamic routes. For example, if someone visits http://localhost:8080/auth/google, the provider value will be google, and if someone visits http://localhost:8080/auth/github the provider value will be github.

Registering Reread with the Providers

In order to use an OAuth Provider, the Provider needs to know about the Requester. This is required by the Provider for safety reasons. It allows the Provider to know which Requester is doing what, and what kinds of privileges the Requester can ask of the Users that sign in.

Each Provider has a different way to set it up, but each Provider documents it well. In the end, what needs to be done is the following:

  • Register the app (Requester) with the Provider
  • Get a pair of secrets from the Provider
  • Make them available to the app in a secure fashion

The secure fashion part is important as leaking the secrets will allow others to impersonate the Requester, and potentially leak users' information and break their trust. While this is important, it's less important for local development. I'll cover securing secrets in a future article when I address deploying Reread.

Setting up the OAuth providers and routes

In order to make working with OAuth easy, I'm using Go's oauth2 library. Since I already know I will be using multiple OAuth providers, I'll organize the code in layers, with each layer abstracting away certain parts of the process. The layer setup looks like so:

- Server
  +-> /auth/:provider route handler (Redirection, Error Handling)
   +-> auth package (JWT, Cookies, and User Database)
    +-> providers package (OAuth for any provider)
     +-> Google Provider (OAuth for Google)
     +-> Github Provider (OAuth for Github)

The Route handler deals with the overall responses the server sends whenever the route is visited. It's very lightweight and shouldn't be doing much else than calling the auth package. The auth package has the most context about the sign in flow and coordinate with the providers package, create JWT tokens and cookies, and also do user database operations. The providers package only cares about OAuth and is a thin layer for abstracting away specific providers -- like Google and Github -- within a single interface.

Now that I have the basic code layout, the first thing to deal with is the lowest level: provider setup. This happens on server startup. The google provider setup looks like so:

type googleProvider struct {
    config                *oauth2.Config
    userProfileRequestUrl string
}

func initGoogle(config *config.Config, callbackPath string) *googleProvider {
    return &googleProvider{
        config: &oauth2.Config{
            ClientID:     config.GoogleClientId,
            ClientSecret: config.GoogleClientSecret,
            Endpoint:     google.Endpoint,
            RedirectURL:  fmt.Sprintf("%s%s", config.BaseUrl, callbackPath),
            Scopes: []string{
                "email",
                "profile",
            },
        },
        userProfileRequestUrl: "https://www.googleapis.com/oauth2/v3/userinfo",
    }
}

I'm using a struct googleProvider to contain both the oauth2.Config and any other API urls I might need to get the user's information. In this case, Google gives me everything I need within a single API and I store that within the userProfileRequestUrl field. I'll be talking about how to use that API later in this series, but for now it's ok to ignore.

Regarding the oauth2 configuration, there are some fields containing the secrets that I obtained by registering Reread with google OAuth, and endpoints that tell the oauth2 library the URLs to call for doing the OAuth process. The RedirectURL is important because it tells the Provider which URL to call us back on when the authorization is done (or fails). Lastly, the scopes is the kind of data reread's interested in reading. For now, all I'm interested in is requesting the user name and email.

The initGoogle function has a similar counterpart called initGithub which sets up the config for Github. With these two in place, I can start creating the upper layer, which is the providers public interface.

func InitProviders(conf *config.Config) {
    providers = map[string]Provider{
        "google": initGoogle(conf, "/auth/google/callback"),
        "github": initGithub(conf, "/auth/github/callback"),
    }
}

This method will initialize both providers and put them in a map so I can reference each based on their names.

In order to reference all providers uniformly, I need to create a generic interface for providers, which I call Provider. The interface looks like this:

type Provider interface {
    AuthCodeURL(string) string
    Exchange(context.Context, string) (*oauth2.Token, error)
}

I'll explain what each method does in the next part, but it's important to note that the two functions are virtually identical to the one oauth2 library exposes. I didn't technically need to wrap them, but providers may require additional work down the line. I don't want to refactor my code then, so this is a low cost investment up front.

In order to extract the provider via the provider name from the request, I have a helper function:

func providerFromContext(ctx echo.Context) (Provider, error) {
  // Get the provider from url
    providerName := ctx.Param("provider")
    provider, found := providers[providerName]

    if !found {
        // If it's an unknown provider, log it and throw an error
        err := fmt.Errorf("unsupported provider: %s", providerName)
        ctx.Logger().Error("providerFromContext: ", err.Error())
        return nil, err
    }

    return provider, nil
}

Each request now has to just call this function, and get the right provider.

Kicking off OAuth: Flow step 1

When the user clicks the Sign in button with a specific provider, the reread app redirects them to our /:provider route. As mentioned before, the :provider is a fancy way of telling Echo that this route is dynamic and whatever value matches :provider should be accessible to the route handler.

Since we're using the handy oauth2 library and we've setup our providers, the route doesn't need to do much.

func BeginAuthHandler(ctx echo.Context) error {
    state, err := states.getState(ctx)
    if err != nil {
        errorResponse := responsetypes.Error{Error: err.Error()}
        return ctx.JSON(http.StatusInternalServerError, &errorResponse)
    }

    url, err := providers.GetAuthUrl(ctx, state)

    if err != nil {
        logger.Error("BeginAuthHandler: ", err)
    }

    return ctx.Redirect(http.StatusTemporaryRedirect, url)
}

Before I talk about what the route handler is doing, it's important to explain states.getState(ctx). OAuth allows a state field to be sent with an authorization request. As the word hints, the encoded string could be anything and might contain state that's important for the Requester. In Reread's case, we don't need any information to be encoded as all we need we can get from the API call to the provider.

While containing encoded data is a use-case for state, the primary purpose of the state field is as what is called a CSRF Token, or Cross Site Request Forgery (Prevention) Token. Since the OAuth flow has multiple steps, the flow is broken up from the perspective of the Requester.

In part 1, where I am now, the Requester will just redirect the User to the Provider. However, to complete the flow, the Provider has to call the Requester back after the User allows it to give us access. Within the callback, how can the Requester be sure the service calling it back is the Provider. The way OAuth does it is to have the Requester pass along a secret (state) that only the Provider and Requester would know and the Provider attaches it back when calling back the Requester. Since the callback contains the shared secret, the Requester can be confident the callback is valid.

It is important to note that I am not using the same string over and over again, instead generating and storing a new secret string each time. This too is important to prevent someone from guessing our secret and then impersonating the Provider.

Other than the state, the process is pretty simple. All this does is it creates a URL that the User is redirected to. The URL is made up of various parts, but can be broken down into:

  • The authorization url of the Provider (https://accounts.google.com/o/oauth2/auth)
  • Scopes (&scope=email)
  • ClientId (&client_id=one part of the secret pair)
  • RedirectUri (&redirect_uri=http://localhost:8080/auth/google/callback)
  • The state (&state=random string)
  • Response Type (&response_type=code)

While this isn’t an exact list, these parameters vary from Provider to Provider at times. Collectively, these parameters provide the authorization page with enough information about the Requester.

Now that the User has landed to the authorization page for a provider, I’ll stop this article here. Next up, I’ll be picking up the OAuth flow with the callback and then talk about how I store the access tokens, and convert them into a Reread token.