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>