Skip to main content
  1. Languages/
  2. Golang Guides/

Mastering OAuth2 in Go: A Production-Ready Guide to Google and GitHub Logins

Jeff Taakey
Author
Jeff Taakey
21+ Year CTO & Multi-Cloud Architect.

Introduction
#

In the landscape of modern web development, rolling your own authentication system is rarely the right choice. Managing passwords, salts, and encryption at rest is a liability that most businesses should avoid if possible.

By 2025, the industry standard for user authentication remains firmly rooted in OAuth2. It reduces friction for users—who doesn’t have a Google or GitHub account?—and significantly lowers the security surface area for your application.

For Golang developers, implementing OAuth2 can seem daunting due to the intricacies of the handshake protocol, state validation, and provider-specific quirks. However, Go’s standard library and the official x/oauth2 package make this surprisingly robust and efficient.

In this guide, we will build a production-grade authentication service that supports both Google and GitHub logins. We won’t just write “Hello World” code; we will tackle the real-world challenges: CSRF protection, structured configuration, and JSON profile parsing.

Prerequisites
#

Before we dive into the code, ensure your environment is ready. This guide assumes you are working in a professional development environment.

Environment Setup
#

  • Go Version: Go 1.22 or higher (we will leverage recent standard library router enhancements).
  • IDE: VS Code (with Go extension) or Goland.
  • Network: Ability to receive callbacks (localhost is fine for development).

Developer Account Requirements
#

To follow along, you will need to register applications with the providers:

  1. Google Cloud Console: Create a project, setup the OAuth consent screen, and generate a Client ID and Client Secret.
    • Redirect URI: http://localhost:8080/auth/google/callback
  2. GitHub Developer Settings: Create a new OAuth App.
    • Callback URL: http://localhost:8080/auth/github/callback

Understanding the OAuth2 Flow
#

Before writing code, let’s visualize exactly what happens during an OAuth2 Authorization Code flow. This is the most secure flow for server-side applications.

sequenceDiagram autonumber participant U as User participant B as Browser participant S as Go Server participant P as Provider (Google/GitHub) U->>B: Clicks "Login with Google" B->>S: GET /auth/google/login S-->>B: Redirect to Provider (includes State & Scopes) B->>P: Request Authorization Page P-->>U: Prompts for Consent U->>P: Approves Access P-->>B: Redirects to Callback URL with ?code=...&state=... B->>S: GET /auth/google/callback?code=XYZ&state=ABC S->>S: Validate State (CSRF Check) S->>P: Exchange Code for Access Token P-->>S: Returns Access Token S->>P: Request User Profile (using Token) P-->>S: Returns User JSON S-->>B: Sets Session Cookie & Redirects to Dashboard

Step 1: Project Initialization
#

Let’s set up a clean, modular project. We will use godotenv to manage our secrets, keeping them out of our source code—a strict requirement for any serious Go project.

Terminal:

mkdir go-oauth-pro
cd go-oauth-pro
go mod init github.com/yourname/go-oauth-pro
go get golang.org/x/oauth2
go get github.com/joho/godotenv

Create a .env file in your root directory. Do not commit this file to Git.

File: .env

GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
SESSION_SECRET=super-secret-random-string-for-state

Step 2: Configuration and Infrastructure
#

We need a centralized way to manage our OAuth configurations. We’ll stick to the standard library net/http as much as possible to keep dependencies light.

Create a file named config.go. This setup ensures we load our environment variables safely on startup.

File: config.go

package main

import (
	"log"
	"os"

	"github.com/joho/godotenv"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/github"
	"golang.org/x/oauth2/google"
)

type Config struct {
	GoogleLoginConfig *oauth2.Config
	GithubLoginConfig *oauth2.Config
	RandomState       string
}

var AppConfig Config

func InitConfig() {
	// Load .env file
	if err := godotenv.Load(); err != nil {
		log.Println("No .env file found, relying on system env variables")
	}

	AppConfig.GoogleLoginConfig = &oauth2.Config{
		ClientID:     getEnvOrError("GOOGLE_CLIENT_ID"),
		ClientSecret: getEnvOrError("GOOGLE_CLIENT_SECRET"),
		RedirectURL:  "http://localhost:8080/auth/google/callback",
		Scopes: []string{
			"https://www.googleapis.com/auth/userinfo.email",
			"https://www.googleapis.com/auth/userinfo.profile",
		},
		Endpoint: google.Endpoint,
	}

	AppConfig.GithubLoginConfig = &oauth2.Config{
		ClientID:     getEnvOrError("GITHUB_CLIENT_ID"),
		ClientSecret: getEnvOrError("GITHUB_CLIENT_SECRET"),
		RedirectURL:  "http://localhost:8080/auth/github/callback",
		Scopes:       []string{"read:user", "user:email"},
		Endpoint:     github.Endpoint,
	}

	// In production, this should be generated per session, not global
	AppConfig.RandomState = getEnvOrError("SESSION_SECRET")
}

func getEnvOrError(key string) string {
	value := os.Getenv(key)
	if value == "" {
		log.Fatalf("Environment variable %s not set", key)
	}
	return value
}

Step 3: Implementing the Handlers
#

Now comes the core logic. We need two primary handlers for each provider:

  1. Login Handler: Redirects the user to Google/GitHub.
  2. Callback Handler: Receives the code, exchanges it for a token, and fetches user data.

To make our code clean, we will define structs to map the JSON responses from Google and GitHub, as they have different schemas.

File: main.go

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
)

// --- Structs for User Data ---

type GoogleUser struct {
	ID            string `json:"id"`
	Email         string `json:"email"`
	VerifiedEmail bool   `json:"verified_email"`
	Name          string `json:"name"`
	Picture       string `json:"picture"`
}

type GithubUser struct {
	ID        int    `json:"id"`
	Login     string `json:"login"` // Username
	AvatarURL string `json:"avatar_url"`
	Name      string `json:"name"`
	Email     string `json:"email"` // Might be empty if private
}

// --- Main Entry Point ---

func main() {
	InitConfig()

	mux := http.NewServeMux()

	// Google Routes
	mux.HandleFunc("GET /auth/google/login", googleLoginHandler)
	mux.HandleFunc("GET /auth/google/callback", googleCallbackHandler)

	// GitHub Routes
	mux.HandleFunc("GET /auth/github/login", githubLoginHandler)
	mux.HandleFunc("GET /auth/github/callback", githubCallbackHandler)

	// Home
	mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte(`<html><body>
			<h1>Golang OAuth2 Pro</h1>
			<a href="/auth/google/login">Login with Google</a><br>
			<a href="/auth/github/login">Login with GitHub</a>
		</body></html>`))
	})

	log.Println("Server started on :8080")
	if err := http.ListenAndServe(":8080", mux); err != nil {
		log.Fatal(err)
	}
}

// --- Google Handlers ---

func googleLoginHandler(w http.ResponseWriter, r *http.Request) {
	url := AppConfig.GoogleLoginConfig.AuthCodeURL(AppConfig.RandomState)
	http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}

func googleCallbackHandler(w http.ResponseWriter, r *http.Request) {
	// 1. Validate State (CSRF Protection)
	if r.URL.Query().Get("state") != AppConfig.RandomState {
		http.Error(w, "State mismatch. Possible CSRF attack.", http.StatusBadRequest)
		return
	}

	// 2. Exchange Code for Token
	code := r.URL.Query().Get("code")
	token, err := AppConfig.GoogleLoginConfig.Exchange(context.Background(), code)
	if err != nil {
		http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
		return
	}

	// 3. Fetch User Info
	resp, err := http.Get("https://www.googleapis.com/oauth2/v2/userinfo?access_token=" + token.AccessToken)
	if err != nil {
		http.Error(w, "Failed to get user info: "+err.Error(), http.StatusInternalServerError)
		return
	}
	defer resp.Body.Close()

	userData, _ := io.ReadAll(resp.Body)
	
	// Unmarshal for demonstration
	var gUser GoogleUser
	json.Unmarshal(userData, &gUser)

	fmt.Fprintf(w, "Logged in via Google: %+v\n", gUser)
}

// --- GitHub Handlers ---

func githubLoginHandler(w http.ResponseWriter, r *http.Request) {
	url := AppConfig.GithubLoginConfig.AuthCodeURL(AppConfig.RandomState)
	http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}

func githubCallbackHandler(w http.ResponseWriter, r *http.Request) {
	// 1. Validate State
	if r.URL.Query().Get("state") != AppConfig.RandomState {
		http.Error(w, "State mismatch", http.StatusBadRequest)
		return
	}

	// 2. Exchange Code
	code := r.URL.Query().Get("code")
	token, err := AppConfig.GithubLoginConfig.Exchange(context.Background(), code)
	if err != nil {
		http.Error(w, "Code exchange failed: "+err.Error(), http.StatusInternalServerError)
		return
	}

	// 3. Fetch User Info (Requires explicit Auth header for GitHub)
	req, _ := http.NewRequest("GET", "https://api.github.com/user", nil)
	req.Header.Set("Authorization", "Bearer "+token.AccessToken)
	req.Header.Set("Accept", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		http.Error(w, "User info failed: "+err.Error(), http.StatusInternalServerError)
		return
	}
	defer resp.Body.Close()

	userData, _ := io.ReadAll(resp.Body)

	var ghUser GithubUser
	json.Unmarshal(userData, &ghUser)

	fmt.Fprintf(w, "Logged in via GitHub: %+v\n", ghUser)
}

Key Implementation Details
#

  1. State Parameter: In the AuthCodeURL function, we pass AppConfig.RandomState. This string is sent to the provider and returned in the callback. If they don’t match, it means the request didn’t originate from your app (a CSRF attack). In a real production app, this should be unique per user session.
  2. Context: We pass context.Background() to the exchange function. In production, use a context with a timeout (e.g., context.WithTimeout) to prevent your server from hanging if the provider is slow.
  3. Client Headers: Notice the difference in fetching user info. Google accepts the access token as a query parameter (though headers are preferred), while GitHub strictly requires the Authorization: Bearer header.

Provider Comparison: Google vs. GitHub
#

When implementing multiple providers, it helps to understand the subtle differences in their API behavior.

Feature Google OAuth2 GitHub OAuth2
UserInfo Endpoint https://www.googleapis.com/oauth2/v2/userinfo https://api.github.com/user
Email Handling Returns email in profile if scope granted. Email might be private; requires separate API call to /user/emails.
Token Transport Supports Query Param & Header. Strictly Headers (Authorization: Bearer).
Scopes Style Full URLs (e.g., https://www.googleapis.com/...) Short strings (e.g., read:user, repo).
Token Expiry Short-lived (1 hr) + Refresh Token. Traditionally non-expiring (unless configured).

Performance and Security Best Practices
#

To take this from a tutorial script to a “DevPro” level implementation, consider these enhancements:

1. Robust State Validation
#

Using a static string from .env for the state parameter is okay for testing, but weak for production. Better approach: Generate a random string, store it in a secure HTTP-only cookie, and validate it upon callback.

import "crypto/rand"
import "encoding/base64"

func generateRandomState() string {
    b := make([]byte, 32)
    rand.Read(b)
    return base64.URLEncoding.EncodeToString(b)
}

2. Handling Private Emails (GitHub)
#

GitHub users can hide their email addresses. If your application relies on email for unique identification (it should), the /user endpoint might return null for email. Solution: You must perform a secondary request to https://api.github.com/user/emails and look for the entry where primary: true and verified: true.

3. Token Storage
#

Never store Access Tokens in local storage or plain cookies on the client side.

  • Session Approach: Create a session ID on your server, map it to the user’s data, and send the session ID as an HTTP-only, Secure cookie.
  • JWT Approach: Generate your own JWT after the OAuth exchange and send that to the client.

4. Timeouts
#

External API calls fail. Always wrap your HTTP clients in timeouts.

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
token, err := conf.Exchange(ctx, code)

Conclusion
#

Implementing OAuth2 in Golang is a powerful way to secure your application while improving user experience. By leveraging golang.org/x/oauth2, you avoid the complexity of writing the protocol handshake yourself, allowing you to focus on application logic.

The code provided above gives you a functional foundation for both Google and GitHub. However, the journey doesn’t end here. For a 2025-era production environment, your next steps are to implement a proper session management system (like Redis or encrypted cookies) and ensure your state token generation is cryptographically secure per request.

Further Reading
#

Happy coding, and stay secure!