golangdata structures

Google Authentication With Goth in Golang

Tired of building authentication from scratch? Worried about password security, forgotten password flows, and the general headache of managing user accounts? What if you could just… let Google handle it?

Welcome to the wonderful world of OAuth 2.0 (or more specifically, OpenID Connect for authentication), and in the Go ecosystem, a fantastic package called Goth. Goth is your friendly bouncer, making it surprisingly simple to integrate authentication providers like Google, GitHub, Facebook, and many more into your Go applications. It handles the complex dance of redirects, tokens, and user data retrieval, so you don't have to.

In this guide, we'll build a simple Go application that lets users sign in with their Google accounts, manages their sessions, and protects certain routes. It's like giving your users a VIP pass stamped by Google itself!

The Grand Tour: Project Structure

First, let's peek at the blueprint of our digital fortress. This structure keeps things organized and makes our lives easier:

.
├── database
│   └── db.go       // Our simple SQLite database setup
├── db.sqlite       // Where our user data lives (a simple file database)
├── go.mod          // Go modules file
├── go.sum          // Go module checksums
├── handler
│   ├── auth.go     // The entry points for the Google authentication dance
│   └── protected.go// Handlers for pages only VIPs (authenticated users) can see
├── main.go         // The heart of our application: server setup, Goth config
├── model
│   └── user.go     // Our Go representation of a user (what we store in our DB)
├── templates
│   ├── login.html  // The "Sign in with Google" button page
│   └── protected.html// The "Welcome, VIP!" page
└── utils
    └── context.go  // A little helper for securely passing user data through requests

Gathering Our Toolkit: Dependencies

Before we code, let's grab the necessary packages. Think of these as our specialized tools for building this secure application:

go get -u github.com/mattn/go-sqlite3  # For our lightweight SQLite database
go get -u github.com/go-chi/chi/v5     # A slick, minimalistic router for our web server
go get -u github.com/markbates/goth    # The star of the show: our OAuth/OpenID Connect library
go get -u github.com/gorilla/sessions  # For managing user sessions with cookies

(Note: github.com/gorilla/sessions is often a transitive dependency of goth but explicitly including it ensures its availability for session store setup.)

main.go: The Application's Grand Central Station

This is where all the magic starts. main.go handles our server setup, configures Goth, sets up session management, and defines our routes. It also introduces a crucial middleware that acts as our bouncer for protected areas.

// main.go
package main

import (
	"context"       // For passing data through request context
	"encoding/gob"  // To register our User struct for session encoding
	"log"           // For logging any issues
	"net/http"      // Go's standard HTTP library

	"google-auth-tutorial/database" // Our local database package
	"google-auth-tutorial/handler"  // Our local request handlers
	"google-auth-tutorial/model"    // Our local user model
	"google-auth-tutorial/utils"    // Our local context key utility

	"github.com/go-chi/chi/v5"              // Our HTTP router
	"github.com/gorilla/sessions"           // For managing cookies and sessions
	"github.com/markbates/goth"             // The main Goth package
	"github.com/markbates/goth/gothic"      // Goth's HTTP handler utilities
	"github.com/markbates/goth/providers/google" // The Google OAuth provider for Goth
)

// userMiddleware: Our Bouncer at the VIP Section
// This middleware checks if a user is authenticated (has a valid session cookie).
// If not, it politely (or firmly) redirects them to the login page.
func userMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Try to get the session from the cookie store
		s, err := gothic.Store.Get(r, "session")
		// If there's an error, no session, or no 'user' in the session,
		// then this user isn't authenticated. Redirect them!
		if err != nil || s == nil || s.Values["user"] == nil {
			http.Redirect(w, r, "/login", http.StatusSeeOther) // SeeOther means redirect after POST-like action
			return
		}

		// If we found a user in the session, cast it to our User struct
		// (gobs are fun, but sometimes require type assertions!)
		u := s.Values["user"].(model.User)

		// Attach the user to the request context
		// This makes the user data available to downstream handlers without
		// passing it explicitly through function arguments.
		ctx := context.WithValue(r.Context(), utils.CtxKey("user"), u)
		next.ServeHTTP(w, r.WithContext(ctx)) // Hand off the request with the user attached
	})
}

func main() {
	// Initialize our SQLite database. Our user list will live here.
	db := database.InitializeDatabase()

	// --- Goth Configuration: Telling Goth about our Google bestie ---
	// You'll need to create credentials at Google Cloud Console: https://console.cloud.google.com/
	// Make sure to set the Authorized redirect URI to "http://localhost:8080/auth/google/callback"
	goth.UseProviders(
		google.New(
			"YOUR_GOOGLE_CLIENT_ID",     // <-- REPLACE THIS! Get it from Google Cloud Console
			"YOUR_GOOGLE_CLIENT_SECRET", // <-- REPLACE THIS! Get it from Google Cloud Console
			"http://localhost:8080/auth/google/callback", // This must match your Google Cloud Console redirect URI
			"email",   // Requested scope: we want their email address
			"profile", // Requested scope: we want their basic profile info (name, etc.)
		),
	)

	// --- Session Store Setup: Our trusty cookie jar ---
	// This configures how user sessions are stored (in this case, via secure cookies).
	key := "super-secret-session-key-that-you-should-change-in-production" // <-- CHANGE THIS! Make it long and random!
	maxAge := 86400 * 30 // Session cookie valid for 30 days
	store := sessions.NewCookieStore([]byte(key)) // Create a new cookie store with our secret key
	store.MaxAge(maxAge) // Set how long the cookie is valid
	store.Options.Path = "/" // Make the cookie available across the entire site
	store.Options.HttpOnly = true // Prevent JavaScript from accessing the cookie (security!)
	store.Options.Secure = false // Set to 'true' in production (requires HTTPS)

	gothic.Store = store // Tell Goth to use our custom session store

	// --- Gob Registration: Teaching Go how to pack our user for travel ---
	// When you store custom structs in a session (which uses gob encoding),
	// you need to register them first. This tells Go how to serialize/deserialize your User struct.
	gob.Register(model.User{})

	// --- Routing: Setting up the red carpet and guarded paths ---
	r := chi.NewRouter() // Initialize our Chi router

	// Public routes
	r.Get("/login", handler.HandleGetLogIn)   // The page with the "Sign in with Google" button
	r.Get("/logout", handler.HandleGetLogOut) // The logout endpoint

	// Authentication routes managed by Goth
	r.Route("/auth", func(r chi.Router) {
		// This is the callback URI Google redirects to after successful login
		r.Get("/{provider}/callback", handler.HandleGetGoogleAuthCallback(db))
		// This is the initial endpoint a user clicks to start the Google OAuth flow
		r.Get("/", func(w http.ResponseWriter, r *http.Request) {
			// gothic.BeginAuthHandler initiates the redirect to Google's login page
			gothic.BeginAuthHandler(w, r)
		})
	})

	// Protected routes: Only authenticated users allowed!
	r.Route("/protected", func(r chi.Router) {
		r.Use(userMiddleware) // Apply our bouncer middleware here!
		r.Get("/", handler.HandleGetProtected) // The actual protected page
	})

	// Start the server!
	log.Fatal(http.ListenAndServe(":8080", r)) // Listen on port 8080
}

database/db.go: Our Simple Data Vault

We need a place to store our users once Google authenticates them. For simplicity, we're using SQLite, a file-based database.

// database/db.go
package database

import (
	"database/sql" // Go's standard SQL package
	"log"          // For logging fatal errors

	_ "github.com/mattn/go-sqlite3" // The SQLite driver (underscore import for side effects only)
)

// InitializeDatabase: Sets up our SQLite database and creates the user table if it doesn't exist.
func InitializeDatabase() *sql.DB {
	db, err := sql.Open("sqlite3", "file:///db.sqlite") // Open (or create) the SQLite database file
	if err != nil {
		log.Fatal(err) // If we can't open the DB, that's a showstopper!
	}
	// Execute SQL to create our 'user' table.
	// 'id' will be the unique Google User ID, 'name' and 'email' from their profile.
	db.Exec(
		`
		CREATE TABLE IF NOT EXISTS user (
			id TEXT PRIMARY KEY,
			name TEXT,
			email TEXT
		)
		`,
	)
	return db // Return the database connection
}

utils/context.go: The Secret Handshake for Context Keys

When you pass values through Go's context.Context, it's best practice to use custom types for keys to avoid collisions with other packages. This tiny file defines such a type.

// utils/context.go
package utils

// CtxKey: A custom type for context keys.
// Using a distinct type prevents collisions with other keys from other packages.
type CtxKey string

model/user.go: The User's Digital ID Card

This defines our User struct (what a user looks like in our Go app and database) and a handy method to either create a new user or retrieve an existing one from our SQLite database.

// model/user.go
package model

import (
	"database/sql" // For database interactions
)

// User: Our representation of a user in the application.
type User struct {
	ID    string // Google's unique User ID (e.g., from gu.UserID)
	Name  string // User's name from Google profile
	Email string // User's email from Google profile
}

// CreateOrRead: This method tries to find a user by ID.
// If found, it populates the User struct. If not, it creates a new user in the DB.
func (u *User) CreateOrRead(db *sql.DB) error {
	// Step 1: Try to read the user
	s, err := db.Prepare(
		`
		select id, name, email
		from user
		where id = $1
		`,
	)
	if err != nil {
		return err // Error preparing the SQL statement
	}
	// Attempt to scan the row into our User struct fields
	err = s.QueryRow(u.ID).Scan(&u.ID, &u.Name, &u.Email)
	if err == nil {
		return nil // User found! We're done.
	}

	// Step 2: If the user wasn't found (err != nil after Scan, typically sql.ErrNoRows),
	// then we proceed to insert them.
	s, err = db.Prepare(
		`
		insert into user (id, name, email) values($1, $2, $3)
		`,
	)
	if err != nil {
		return err // Error preparing the insert statement
	}

	// Execute the insert statement with our user's data
	_, err = s.Exec(u.ID, u.Name, u.Email)
	return err // Return any error from the insert operation
}

handler/auth.go: The Authentication Dance Moves

These handlers orchestrate the Google authentication flow, from receiving Google's callback to setting up a user's session.

// handler/auth.go
package handler

import (
	"database/sql" // For interacting with our database
	"html/template" // For rendering HTML templates
	"log"           // For logging errors
	"net/http"      // Go's HTTP package

	"google-auth-tutorial/model" // Our User model

	"github.com/markbates/goth/gothic" // Goth's HTTP handler utilities
)

// HandleGetGoogleAuthCallback: This is the critical endpoint Google redirects to
// after a user successfully logs in through their system.
func HandleGetGoogleAuthCallback(db *sql.DB) http.HandlerFunc {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// gothic.CompleteUserAuth does the heavy lifting: exchanges codes for tokens,
		// fetches user info from Google, and gives us a GothUser struct.
		gu, err := gothic.CompleteUserAuth(w, r)
		if err != nil {
			log.Printf("Error completing Google auth: %v", err)
			http.Error(w, "Authentication failed", http.StatusForbidden)
			return
		}

		// Now that Goth has verified the user, let's create (or read) them in our own database.
		// We map GothUser's fields to our simpler User model.
		u := model.User{ID: gu.UserID, Name: gu.Name, Email: gu.Email}
		if err := u.CreateOrRead(db); err != nil {
			log.Printf("Error creating/reading user in DB: %v", err)
			http.Error(w, "Internal server error during user registration", http.StatusInternalServerError)
			return
		}

		// --- Session Creation: Stamping the passport for our newly authenticated user ---
		// Get a new session from our session store.
		s, err := gothic.Store.New(r, "session")
		if err != nil {
			log.Printf("Error getting new session: %v", err)
			http.Error(w, "Session error", http.StatusInternalServerError)
			return
		}
		// Store our authenticated user model in the session.
		s.Values["user"] = u // Store the User struct directly
		err = s.Save(r, w) // Save the session, which writes the cookie to the client
		if err != nil {
			log.Printf("Error saving session: %v", err)
			http.Error(w, "Session save error", http.StatusInternalServerError)
			return
		}

		// Success! Redirect the user to a protected page.
		http.Redirect(w, r, "/protected", http.StatusSeeOther)
	})
}

// HandleGetLogIn: Simply renders our login page with the Google sign-in link.
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.Printf("Error executing login template: %v", err)
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}
}

// HandleGetLogOut: Destroys the user's session, effectively logging them out.
func HandleGetLogOut(w http.ResponseWriter, r *http.Request) {
	// Get the active session.
	session, err := gothic.Store.Get(r, "session")
	if err != nil {
		log.Printf("Error getting session for logout: %v", err)
		http.Error(w, "Logout error", http.StatusInternalServerError)
		return
	}

	// Destroy the session by setting its MaxAge to a negative value
	// and clearing its values. This tells the browser to delete the cookie.
	session.Options.MaxAge = -1
	session.Values = make(map[interface{}]interface{}) // Clear stored values
	err = session.Save(r, w) // Save the modified session to apply changes (delete cookie)
	if err != nil {
		log.Printf("Error saving session during logout: %v", err)
		http.Error(w, "Logout error", http.StatusInternalServerError)
		return
	}
	// Redirect the user back to the login page.
	http.Redirect(w, r, "/login", http.StatusSeeOther)
}

handler/protected.go: The VIP Lounge

This handler is for the page that only authenticated users can access. Notice how it doesn't do any authentication itself – that's the middleware's job! It just assumes a verified User is already in the request context.

// handler/protected.go
package handler

import (
	"log"      // For logging errors
	"net/http" // Go's HTTP package
	"text/template" // For rendering HTML templates (could also use html/template)

	"google-auth-tutorial/model" // Our User model
	"google-auth-tutorial/utils" // Our context key utility
)

// HandleGetProtected: Serves the protected page.
// It assumes the userMiddleware has already run and placed the user in the context.
func HandleGetProtected(w http.ResponseWriter, r *http.Request) {
	// Retrieve the user from the request context.
	// We're confidently asserting the type here because our middleware
	// guarantees it will be a model.User if we reach this point.
	u := r.Context().Value(utils.CtxKey("user")).(model.User)

	// Parse and execute the protected page template, passing the user data.
	tmpl := template.Must(template.ParseFiles("templates/protected.html"))
	if err := tmpl.Execute(w, u); err != nil {
		log.Printf("Error executing protected template: %v", err)
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}
}

The Welcome Mats: HTML Templates

Finally, the simple HTML files that welcome (or redirect) our users.

templates/login.html: Your Gateway to Google

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Login</title>
    <style>body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; background-color: #f0f2f5; } a { padding: 15px 30px; background-color: #4285F4; color: white; text-decoration: none; border-radius: 5px; font-size: 1.2em; display: flex; align-items: center; gap: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.2); } a:hover { background-color: #357ae8; }</style>
  </head>
  <body>
    <a href="/auth?provider=google">
      <img src="https://www.google.com/favicon.ico" alt="Google icon" width="24" height="24">
      Sign in with Google
    </a>
  </body>
</html>

templates/protected.html: The VIP View

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Protected Page</title>
    <style>body { font-family: sans-serif; display: flex; flex-direction: column; justify-content: center; align-items: center; min-height: 100vh; background-color: #e6ffe6; color: #333; } h1 { color: #28a745; } a { margin-top: 20px; padding: 10px 20px; background-color: #dc3545; color: white; text-decoration: none; border-radius: 5px; } a:hover { background-color: #c82333; }</style>
  </head>
  <body>
    <h1>Hello, {{.Name}}!</h1>
    <p>Your email: {{.Email}}</p>
    <p>Welcome to the super-secret protected page!</p>
    <a href="/logout">Log out</a>
  </body>
</html>

How It All Works: The Google Authentication Dance

  1. User clicks "Sign in with Google": They hit /auth?provider=google.
  2. Goth steps in: gothic.BeginAuthHandler takes over, redirects the user to Google's login/consent page.
  3. Google does its thing: The user logs in and grants permission.
  4. Google redirects back: Google sends the user's browser back to http://localhost:8080/auth/google/callback.
  5. Goth completes the handshake: gothic.CompleteUserAuth exchanges tokens with Google, fetches the user's profile (gu).
  6. Your app gets the user: We take gu's data (UserID, Name, Email) and CreateOrRead it in our own db.sqlite.
  7. Session created: We stash our model.User into a gorilla/sessions cookie, encrypted with our secret key.
  8. Redirect to protected page: The user is sent to /protected.
  9. Middleware checks: userMiddleware intercepts /protected, finds the user in the session, puts them in the request context.
  10. Protected handler: HandleGetProtected simply renders the page, accessing user details from the context.
  11. Logout: Clicking "Log out" clears the session cookie and redirects to /login.

This setup provides a robust and secure way to handle user authentication, leveraging Google's infrastructure while giving you control over user data within your own application. It's clean, efficient, and surprisingly easy with Goth!

Go forth and build authenticated apps!