main.go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
package main

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

	"github.com/google/uuid"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/github"
	"golang.org/x/oauth2/google")

// OAuthProvider represents an OAuth2 identity provider
type OAuthProvider struct {
	Name          string
	Config        *oauth2.Config
	ProfileURL    string
	ProfileParser func([]byte) (*UserProfile, error)
}

// UserProfile holds user information retrieved from OAuth provider
type UserProfile struct {
	ID        string `json:"id"`
	Name      string `json:"name"`
	Email     string `json:"email"`
	AvatarURL string `json:"avatar_url"`
	Provider  string `json:"provider"`
}

// OAuthService manages multiple OAuth2 providers and user sessions
type OAuthService struct {
	providers map[string]*OAuthProvider
	// In production, use a secure session store (e.g., Redis) instead of map
	sessions map[string]*UserProfile
	mu       sync.RWMutex
	cookieName string
	cookieSecret string
}

// NewOAuthService creates a new service with configured providers
func NewOAuthService(cookieSecret string) *OAuthService {
	return &OAuthService{
		providers: make(map[string]*OAuthProvider),
		sessions: make(map[string]*UserProfile),
		cookieName: "oauth_session",
		cookieSecret: cookieSecret,
	}
}

// RegisterProvider adds a new OAuth2 provider to the service
func (s *OAuthService) RegisterProvider(provider *OAuthProvider) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.providers[provider.Name] = provider
}

// getProvider retrieves a provider by name (thread-safe)
func (s *OAuthService) getProvider(name string) (*OAuthProvider, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	p, exists := s.providers[name]
	return p, exists
}

// loginHandler redirects user to OAuth provider's authorization URL
func (s *OAuthService) loginHandler(w http.ResponseWriter, r *http.Request) {
	providerName := r.URL.Query().Get("provider")
	if providerName == "" {
		http.Error(w, "provider query parameter required", http.StatusBadRequest)
		return
	}

	provider, exists := s.getProvider(providerName)
	if !exists {
		http.Error(w, fmt.Sprintf("provider %s not supported", providerName), http.StatusBadRequest)
		return
	}

	// Generate random state to prevent CSRF attacks
	state := uuid.NewString()
	// In production, store state in secure cookie or session
	http.SetCookie(w, &http.Cookie{
		Name:     "oauth_state",
		Value:    state,
		Path:     "/",
		MaxAge:   300, // 5 minutes
		HttpOnly: true,
		Secure:   true, // Enable in HTTPS environments
		SameSite: http.SameSiteLaxMode,
	})

	// Redirect to provider's authorization URL
	authURL := provider.Config.AuthCodeURL(state, oauth2.AccessTypeOnline)
	http.Redirect(w, r, authURL, http.StatusTemporaryRedirect)
}

// callbackHandler handles the OAuth provider's callback and retrieves user profile
func (s *OAuthService) callbackHandler(w http.ResponseWriter, r *http.Request) {
	providerName := r.URL.Query().Get("provider")
	if providerName == "" {
		http.Error(w, "provider query parameter required", http.StatusBadRequest)
		return
	}

	provider, exists := s.getProvider(providerName)
	if !exists {
		http.Error(w, fmt.Sprintf("provider %s not supported", providerName), http.StatusBadRequest)
		return
	}

	// Verify CSRF state
	stateCookie, err := r.Cookie("oauth_state")
	if err != nil || stateCookie.Value != r.URL.Query().Get("state") {
		http.Error(w, "invalid or missing CSRF state", http.StatusForbidden)
		return
	}

	// Exchange authorization code for access token
	code := r.URL.Query().Get("code")
	token, err := provider.Config.Exchange(context.Background(), code)
	if err != nil {
		http.Error(w, fmt.Sprintf("failed to exchange code: %v", err), http.StatusInternalServerError)
		return
	}

	// Retrieve user profile from provider's API
	client := provider.Config.Client(context.Background(), token)
	resp, err := client.Get(provider.ProfileURL)
	if err != nil {
		http.Error(w, fmt.Sprintf("failed to get user profile: %v", err), http.StatusInternalServerError)
		return
	}
	defer resp.Body.Close()

	// Parse profile response
	profileData, err := io.ReadAll(resp.Body)
	if err != nil {
		http.Error(w, fmt.Sprintf("failed to read profile data: %v", err), http.StatusInternalServerError)
		return
	}
	profile, err := provider.ProfileParser(profileData)
	if err != nil {
		http.Error(w, fmt.Sprintf("failed to parse profile: %v", err), http.StatusInternalServerError)
		return
	}
	profile.Provider = providerName

	// Create session (in production, use Redis or database)
	sessionID := uuid.NewString()
	s.mu.Lock()
	s.sessions[sessionID] = profile
	s.mu.Unlock()

	// Set session cookie
	http.SetCookie(w, &http.Cookie{
		Name:     s.cookieName,
		Value:    sessionID,
		Path:     "/",
		MaxAge:   86400, // 24 hours
		HttpOnly: true,
		Secure:   true,
		SameSite: http.SameSiteLaxMode,
	})

	// Redirect to user profile page
	http.Redirect(w, r, "/profile", http.StatusTemporaryRedirect)
}

// profileHandler displays the authenticated user's profile
func (s *OAuthService) profileHandler(w http.ResponseWriter, r *http.Request) {
	// Get session cookie
	cookie, err := r.Cookie(s.cookieName)
	if err != nil {
		http.Redirect(w, r, "/login?provider=google", http.StatusTemporaryRedirect)
		return
	}

	// Retrieve user profile from session store
	s.mu.RLock()
	profile, exists := s.sessions[cookie.Value]
	s.mu.RUnlock()
	if !exists {
		http.Redirect(w, r, "/login?provider=google", http.StatusTemporaryRedirect)
		return
	}

	// Display profile as HTML
	w.Header().Set("Content-Type", "text/html")
	fmt.Fprintf(w, `<html>
	<head><title>User Profile</title></head>
	<body>
		<h1>Welcome, %s!</h1>
		<p>ID: %s</p>
		<p>Email: %s</p>
		<p>Provider: %s</p>
		<img src="%s" alt="Avatar" width="100">
	</body>
</html>`,
		profile.Name, profile.ID, profile.Email, profile.Provider, profile.AvatarURL)
}

// Example profile parsers
func googleProfileParser(data []byte) (*UserProfile, error) {
	var resp struct {
		ID        string `json:"id"`
		Name      string `json:"name"`
		Email     string `json:"email"`
		Picture   string `json:"picture"`
	}
	if err := json.Unmarshal(data, &resp); err != nil {
		return nil, err
	}
	return &UserProfile{
		ID:        resp.ID,
		Name:      resp.Name,
		Email:     resp.Email,
		AvatarURL: resp.Picture,
	}, nil
}

func githubProfileParser(data []byte) (*UserProfile, error) {
	var resp struct {
		ID        int    `json:"id"`
		Login     string `json:"login"`
		Name      string `json:"name"`
		Email     string `json:"email"`
		AvatarURL string `json:"avatar_url"`
	}
	if err := json.Unmarshal(data, &resp); err != nil {
		return nil, err
	}
	return &UserProfile{
		ID:        fmt.Sprintf("%d", resp.ID),
		Name:      resp.Name,
		Email:     resp.Email,
		AvatarURL: resp.AvatarURL,
	}, nil
}

// Example Usage
func main() {
	// Load OAuth credentials from environment variables (never hardcode!)
	googleClientID := os.Getenv("GOOGLE_CLIENT_ID")
	googleClientSecret := os.Getenv("GOOGLE_CLIENT_SECRET")
	githubClientID := os.Getenv("GITHUB_CLIENT_ID")
	githubClientSecret := os.Getenv("GITHUB_CLIENT_SECRET")
	cookieSecret := os.Getenv("COOKIE_SECRET")
	if cookieSecret == "" {
		cookieSecret = uuid.NewString() // For demo only; use a fixed secret in production
	}

	// Create OAuth service
	service := NewOAuthService(cookieSecret)

	// Register Google provider
	service.RegisterProvider(&OAuthProvider{
		Name: "google",
		Config: &oauth2.Config{
			ClientID:     googleClientID,
			ClientSecret: googleClientSecret,
			RedirectURL:  "http://localhost:8080/auth/callback?provider=google",
			Scopes:       []string{"openid", "email", "profile"},
			Endpoint:     google.Endpoint,
		},
		ProfileURL:    "https://www.googleapis.com/oauth2/v3/userinfo",
		ProfileParser: googleProfileParser,
	})

	// Register GitHub provider
	service.RegisterProvider(&OAuthProvider{
		Name: "github",
		Config: &oauth2.Config{
			ClientID:     githubClientID,
			ClientSecret: githubClientSecret,
			RedirectURL:  "http://localhost:8080/auth/callback?provider=github",
			Scopes:       []string{"user:email"},
			Endpoint:     github.Endpoint,
		},
		ProfileURL:    "https://api.github.com/user",
		ProfileParser: githubProfileParser,
	})

	// Register HTTP handlers
	http.HandleFunc("/auth/login", service.loginHandler)
	http.HandleFunc("/auth/callback", service.callbackHandler)
	http.HandleFunc("/profile", service.profileHandler)

	// Start HTTP server
	log.Println("OAuth2 authentication service starting on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

How It Works

OAuth2 helper that handles Google and GitHub provider flows, exchanges codes for tokens, and persists sessions with secure cookies.

Uses an oauth2.Config per provider, builds authorization URLs with state, handles callbacks to exchange codes for tokens, fetches user profiles, stores session info in signed cookies, and includes logout handlers.

Key Concepts

  • 1Supports multiple providers behind a common interface.
  • 2Uses state parameters and secure cookies to prevent CSRF and session hijack.
  • 3Separates auth redirect, callback handling, and profile fetch logic.

When to Use This Pattern

  • Adding social login to web applications quickly.
  • Bootstrapping identity brokering for internal tools.
  • Prototyping multi-provider OAuth flows locally.

Best Practices

  • Store client secrets outside source control and rotate them.
  • Validate the state parameter to block CSRF.
  • Request only the minimal scopes needed for your app.
Go Version1.18
Difficultyadvanced
Production ReadyYes
Lines of Code295