OAuth2 Authentication with Google and GitHub Providers
OAuth2 helper that handles Google and GitHub provider flows, exchanges codes for tokens, and persists sessions with secure cookies.
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