Google Authentication with Goth in Golang

Setup

Create a new project

This is the structure of our application:

.
 database
    db.go  // database initialization
 db.sqlite
 go.mod
 go.sum
 handler
    auth.go  // authentication endpoints
    protected.go  // protected endpoints
 main.go  // application entrypoint
 model
    user.go  // user model
 templates
    login.html  // login page template
    protected.html  // protected page template
 utils
     context.go  // request context key utility

Download requirements

go get -u github.com/mattn/go-sqlite3
go get -u github.com/go-chi/chi/v5
go get -u github.com/markbates/goth

Create our entrypoint

Here is the entrypoint to our application. In addition to the main function, a middleware function is also defined which checks if session cookie is present. If a session cookie is found, the middleware adds the user to the request context, otherwise the request is redirected to the login page.

main.go
package main

import (
	"context"
	"encoding/gob"
	"google-auth-tutorial/database"
	"google-auth-tutorial/handler"
	"google-auth-tutorial/model"
	"google-auth-tutorial/utils"
	"log"
	"net/http"

	"github.com/go-chi/chi"
	"github.com/gorilla/sessions"
	"github.com/markbates/goth"
	"github.com/markbates/goth/gothic"
	"github.com/markbates/goth/providers/google"
)

// middleware to check session cookie
// this is used for protected routes to redirect the user to the login page,
// if no session cookie is found
func userMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		s, err := gothic.Store.Get(r, "session")
		if err != nil || s == nil || s.Values["user"] == nil {
			http.Redirect(w, r, "/login", http.StatusSeeOther)
			return
		}

		u := s.Values["user"].(model.User)

		ctx := context.WithValue(r.Context(), utils.CtxKey("user"), u)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

func main() {
	db := database.InitializeDatabase()

	// get client ID and secret by creating credentials
	// at https://console.cloud.google.com/
	goth.UseProviders(
		google.New(
			"<google client id>",
			"<google client secret>",
			"http://localhost:8080/auth/google/callback",
			"email",   // requested scope 'email'
			"profile", // requested scope 'profile'
		),
	)
	key := "secret-session-key"
	maxAge := 86400 * 30 // 30 days
	store := sessions.NewCookieStore([]byte(key))
	store.MaxAge(maxAge)
	store.Options.Path = "/"
	store.Options.HttpOnly = true
	store.Options.Secure = false // set to true in production

	gothic.Store = store

	// register our user model so session store
	// is able to encode our user model to the session cookie
	gob.Register(model.User{})

	r := chi.NewRouter()

	r.Get("/login", handler.HandleGetLogIn)
	r.Get("/logout", handler.HandleGetLogOut)

	r.Route("/auth", func(r chi.Router) {
		r.Get("/{provider}/callback", handler.HandleGetGoogleAuthCallback(db))
		r.Get("/", func(w http.ResponseWriter, r *http.Request) {
			gothic.BeginAuthHandler(w, r)
		})
	})

	r.Route("/protected", func(r chi.Router) {
		r.Use(userMiddleware)
		r.Get("/", handler.HandleGetProtected)
	})

	log.Fatal(http.ListenAndServe(":8080", r))
}

And here is the database initialization:

database/db.go
package database

import (
	"database/sql"
	"log"

	_ "github.com/mattn/go-sqlite3"
)

func InitializeDatabase() *sql.DB {
	db, err := sql.Open("sqlite3", "file:///db.sqlite")
	if err != nil {
		log.Fatal(err)
	}
	db.Exec(
		`
		CREATE TABLE IF NOT EXISTS user (
			id TEXT PRIMARY KEY,
			name TEXT,
			email TEXT
		)
		`,
	)
	return db
}

Our user middleware is using a simple utility to use as the context key (since basic datatypes should not be used as context keys):

utils/context.go
package utils

type CtxKey string

And our model with a method for reading or creating it in the database is defined as follows.

model/user.go
package model

import (
	"database/sql"
)

type User struct {
	ID    string
	Name  string
	Email string
}

func (u *User) CreateOrRead(db *sql.DB) error {
	s, err := db.Prepare(
		`
		select id, name, email
		from user
		where id = $1
		`,
	)
	if err != nil {
		return err
	}
	err = s.QueryRow(u.ID).Scan(&u.ID, &u.Name, &u.Email)
	if err == nil {
		return nil
	}

	s, err = db.Prepare(
		`
		insert into user (id, name, email) values($1, $2, $3)
		`,
	)
	if err != nil {
		return err
	}

	_, err = s.Exec(u.ID, u.Name, u.Email)
	return err
}

Next, let’s add our authentication endpoints. We will have a handler for Google authentication callback, which retrieves user information from Google. We can then store the user in our own database with, e.g., their name and email. We will also have simple handlers getting the login page, and an endpoint for logging the user out.

handler/auth.go
func HandleGetGoogleAuthCallback(db *sql.DB) http.HandlerFunc {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// complete user authentication with gothic
		gu, err := gothic.CompleteUserAuth(w, r)
		if err != nil {
			w.WriteHeader(http.StatusForbidden)
			return
		}

        // create a user in our own database
		u := model.User{ID: gu.UserID, Name: gu.Name, Email: gu.Email}
		if err := u.CreateOrRead(db); err != nil {
			log.Println(err)
			w.WriteHeader(http.StatusInternalServerError)
			return
		}

        // create a session cookie for the user
		s, err := gothic.Store.New(r, "session")
		s.Values["user"] = &u
		s.Save(r, w)

		http.Redirect(w, r, "/protected", http.StatusSeeOther)
	})
}

func HandleGetLogIn(w http.ResponseWriter, r *http.Request) {
	tmpl := template.Must(template.ParseFiles("templates/login.html"))
	if err := tmpl.Execute(w, nil); err != nil {
		log.Println(err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
}

func HandleGetLogOut(w http.ResponseWriter, r *http.Request) {
    // get the stored session
	session, err := gothic.Store.Get(r, "session")
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

    // set session values so that it will be destroyed, i.e. set negative maxAge
    // and remove values map
	session.Options.MaxAge = -1
	session.Values = make(map[interface{}]interface{})
	err = session.Save(r, w)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	http.Redirect(w, r, "/login", http.StatusSeeOther)
}

Then we’ll also add a handler for our protected routes in a separate file. This is just to see that our authentication works as expected: only users that have been authenticated can enter the protected page, and unauthenticated users will be redirected to the login page. The handler for the protected endpoint itself does nothing to verify authentication; that is handled by the middleware used in the /protected router (see the router definition in main.go).

handler/protected.go
package handler

import (
	"google-auth-tutorial/model"
	"google-auth-tutorial/utils"
	"log"
	"net/http"
	"text/template"
)

func HandleGetProtected(w http.ResponseWriter, r *http.Request) {
	u := r.Context().Value(utils.CtxKey("user")).(model.User)
	tmpl := template.Must(template.ParseFiles("templates/protected.html"))
	if err := tmpl.Execute(w, u); err != nil {
		log.Println(err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
}

Then let’s add the simple html templates we used in the endpoints. Our log in page:

templates/login.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <a href="/auth?provider=google">Sign in with Google</a>
  </body>
</html>

And our protected page:

templates/protected.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    Hello {{.Name}} ({{.Email}})
    <a href="/logout">Log out</a>
  </body>
</html>

Sign up or log in to start commenting