go-oauth2

authentication-libraryGoOAuth2authorization-serverJWTsecurityAPI-authentication

Authentication Library

go-oauth2

Overview

go-oauth2 is a comprehensive library for building full-featured OAuth 2.0 authorization servers in Go. It provides a complete implementation of OAuth 2.0 that fully complies with RFC 6749, supporting major features including authorization code grant, client authentication, and refresh tokens. While offering simple and intuitive APIs, it has the flexibility and scalability to build enterprise-level OAuth 2.0 providers, allowing implementation of custom token stores, authorization stores, and client stores. It supports JWT, custom token formats, and multiple grant types, making it one of the major libraries in the Go OAuth 2.0 ecosystem that continues to be actively developed as of 2025.

Details

go-oauth2 is a Go library for implementing OAuth 2.0 authorization servers, providing complete implementation of OAuth 2.0 specifications. It offers the following key features:

  • OAuth 2.0 Compliance: Full compliance with RFC 6749 and RFC 6750 (Bearer Token) implementations
  • Grant Type Support: Authorization Code, Client Credentials, Password, Refresh Token, and Implicit grants
  • Flexible Storage: Support for various storage backends including memory, Redis, MongoDB, PostgreSQL, and others
  • JWT Support: Standard support for JWT access token generation and validation functionality
  • Customizability: Complete customization of token generation, client authentication, and user authentication
  • Security Features: Advanced security features including PKCE, State, Scope, and Token Introspection

Pros and Cons

Pros

  • High interoperability and reliability through complete compliance with OAuth 2.0 standards
  • Wide range of use cases supported through rich grant types and security features
  • Various databases and cache systems can be utilized through flexible storage abstraction
  • Stateless token management possible through JWT standard support
  • Provides advanced customizability while maintaining simple APIs
  • Active development community with rich documentation and sample code

Cons

  • High initial learning cost due to OAuth 2.0 complexity
  • Additional security implementation required for full-scale authorization server construction
  • Cannot be used in non-Go language environments
  • May be an excessive feature set for simple authentication requirements
  • Detailed configuration and monitoring required for enterprise-level operations

Reference Pages

Code Examples

Basic OAuth 2.0 Server Setup

package main

import (
	"log"
	"net/http"
	"time"

	"github.com/go-oauth2/oauth2/v4/errors"
	"github.com/go-oauth2/oauth2/v4/manage"
	"github.com/go-oauth2/oauth2/v4/models"
	"github.com/go-oauth2/oauth2/v4/server"
	"github.com/go-oauth2/oauth2/v4/store"
	"github.com/go-oauth2/oauth2/v4/generate"
)

func main() {
	// Create manager
	manager := manage.NewDefaultManager()

	// Token configuration
	manager.SetAuthorizeCodeTokenCfg(&manage.Config{
		AccessTokenExp:    time.Hour * 2,
		RefreshTokenExp:   time.Hour * 24 * 7,
		IsGenerateRefresh: true,
	})

	// JWT access token generator setup
	manager.MapAccessGenerate(generate.NewJWTAccessGenerate("", []byte("your-secret-key"), jwt.SigningMethodHS256))

	// Client store setup
	clientStore := store.NewClientStore()
	clientStore.Set("client-id-123", &models.Client{
		ID:     "client-id-123",
		Secret: "client-secret-456",
		Domain: "http://localhost:3000",
		UserID: "app-user",
	})
	manager.MapClientStorage(clientStore)

	// Token store setup (memory store)
	manager.MapTokenStorage(store.NewMemoryTokenStore())

	// Create OAuth2 server
	srv := server.NewDefaultServer(manager)
	srv.SetAllowGetAccessRequest(true)
	srv.SetClientInfoHandler(server.ClientFormHandler)

	// User authorization handler
	srv.SetUserAuthorizationHandler(userAuthorizeHandler)

	// Authorization scope handling
	srv.SetAuthorizeScopeHandler(func(rw http.ResponseWriter, r *http.Request) (scope string, err error) {
		// Validate requested scope
		requestScope := r.FormValue("scope")
		validScopes := []string{"read", "write", "admin"}
		
		for _, validScope := range validScopes {
			if requestScope == validScope {
				return requestScope, nil
			}
		}
		
		return "read", nil // Default scope
	})

	// Error handler
	srv.SetInternalErrorHandler(func(err error) (re *errors.Response) {
		log.Println("OAuth2 Internal Error:", err.Error())
		return
	})

	// Route setup
	http.HandleFunc("/oauth/authorize", func(w http.ResponseWriter, r *http.Request) {
		err := srv.HandleAuthorizeRequest(w, r)
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
		}
	})

	http.HandleFunc("/oauth/token", func(w http.ResponseWriter, r *http.Request) {
		err := srv.HandleTokenRequest(w, r)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
	})

	// Protected resource endpoint
	http.HandleFunc("/api/profile", func(w http.ResponseWriter, r *http.Request) {
		token, err := srv.ValidationBearerToken(r)
		if err != nil {
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return
		}

		w.Header().Set("Content-Type", "application/json")
		w.Write([]byte(fmt.Sprintf(`{
			"user_id": "%s",
			"client_id": "%s",
			"scope": "%s",
			"expires_at": "%s"
		}`, token.GetUserID(), token.GetClientID(), token.GetScope(), token.GetAccessCreateAt().Add(token.GetAccessExpiresIn()))))
	})

	log.Println("OAuth2 Server starting on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

// User authorization handler
func userAuthorizeHandler(w http.ResponseWriter, r *http.Request) (userID string, err error) {
	// Get user ID from session (simple example)
	username := r.FormValue("username")
	password := r.FormValue("password")

	// In actual implementation, authenticate users with database or LDAP
	if username == "testuser" && password == "testpass" {
		return "user-123", nil
	}

	// Redirect to login page on authentication failure
	w.Header().Set("Location", "/login?redirect="+r.URL.RequestURI())
	w.WriteHeader(http.StatusFound)
	return "", errors.ErrAccessDenied
}

Redis Storage Backend Implementation

package main

import (
	"encoding/json"
	"time"

	"github.com/go-redis/redis/v8"
	"github.com/go-oauth2/oauth2/v4"
	"github.com/go-oauth2/oauth2/v4/models"
)

// Redis token store
type RedisTokenStore struct {
	client *redis.Client
	prefix string
}

func NewRedisTokenStore(client *redis.Client, prefix string) *RedisTokenStore {
	return &RedisTokenStore{
		client: client,
		prefix: prefix,
	}
}

// Implementation of oauth2.TokenStore interface
func (rs *RedisTokenStore) Create(ctx context.Context, info oauth2.TokenInfo) error {
	ct := time.Now()
	code := info.GetCode()
	access := info.GetAccess()
	refresh := info.GetRefresh()

	data, err := json.Marshal(info)
	if err != nil {
		return err
	}

	// Store authorization code
	if code != "" {
		key := rs.prefix + "code:" + code
		expiration := info.GetCodeCreateAt().Add(info.GetCodeExpiresIn()).Sub(ct)
		err = rs.client.Set(ctx, key, data, expiration).Err()
		if err != nil {
			return err
		}
	}

	// Store access token
	if access != "" {
		key := rs.prefix + "access:" + access
		expiration := info.GetAccessCreateAt().Add(info.GetAccessExpiresIn()).Sub(ct)
		err = rs.client.Set(ctx, key, data, expiration).Err()
		if err != nil {
			return err
		}
	}

	// Store refresh token
	if refresh != "" {
		key := rs.prefix + "refresh:" + refresh
		expiration := info.GetRefreshCreateAt().Add(info.GetRefreshExpiresIn()).Sub(ct)
		err = rs.client.Set(ctx, key, data, expiration).Err()
		if err != nil {
			return err
		}
	}

	return nil
}

func (rs *RedisTokenStore) GetByCode(ctx context.Context, code string) (oauth2.TokenInfo, error) {
	key := rs.prefix + "code:" + code
	data, err := rs.client.Get(ctx, key).Result()
	if err != nil {
		return nil, err
	}

	var token models.Token
	err = json.Unmarshal([]byte(data), &token)
	return &token, err
}

func (rs *RedisTokenStore) GetByAccess(ctx context.Context, access string) (oauth2.TokenInfo, error) {
	key := rs.prefix + "access:" + access
	data, err := rs.client.Get(ctx, key).Result()
	if err != nil {
		return nil, err
	}

	var token models.Token
	err = json.Unmarshal([]byte(data), &token)
	return &token, err
}

func (rs *RedisTokenStore) GetByRefresh(ctx context.Context, refresh string) (oauth2.TokenInfo, error) {
	key := rs.prefix + "refresh:" + refresh
	data, err := rs.client.Get(ctx, key).Result()
	if err != nil {
		return nil, err
	}

	var token models.Token
	err = json.Unmarshal([]byte(data), &token)
	return &token, err
}

func (rs *RedisTokenStore) RemoveByCode(ctx context.Context, code string) error {
	key := rs.prefix + "code:" + code
	return rs.client.Del(ctx, key).Err()
}

func (rs *RedisTokenStore) RemoveByAccess(ctx context.Context, access string) error {
	key := rs.prefix + "access:" + access
	return rs.client.Del(ctx, key).Err()
}

func (rs *RedisTokenStore) RemoveByRefresh(ctx context.Context, refresh string) error {
	key := rs.prefix + "refresh:" + refresh
	return rs.client.Del(ctx, key).Err()
}

// Redis store usage example
func setupWithRedis() {
	// Redis client setup
	rdb := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "",
		DB:       0,
	})

	manager := manage.NewDefaultManager()

	// Set Redis token store
	tokenStore := NewRedisTokenStore(rdb, "oauth2:")
	manager.MapTokenStorage(tokenStore)

	// Other configurations...
}

JWT Access Tokens and Custom Claims

package main

import (
	"context"
	"encoding/json"
	"time"

	"github.com/dgrijalva/jwt-go"
	"github.com/go-oauth2/oauth2/v4"
	"github.com/go-oauth2/oauth2/v4/generate"
)

// Custom claims structure
type CustomClaims struct {
	UserID      string   `json:"user_id"`
	ClientID    string   `json:"client_id"`
	Scope       string   `json:"scope"`
	Permissions []string `json:"permissions"`
	Role        string   `json:"role"`
	jwt.StandardClaims
}

// Custom JWT generator
type CustomJWTGenerator struct {
	signingKey    []byte
	signingMethod jwt.SigningMethod
}

func NewCustomJWTGenerator(key []byte, method jwt.SigningMethod) *CustomJWTGenerator {
	return &CustomJWTGenerator{
		signingKey:    key,
		signingMethod: method,
	}
}

// Implementation of oauth2.AccessGenerate interface
func (cjg *CustomJWTGenerator) Token(ctx context.Context, data *oauth2.GenerateBasic, isGenRefresh bool) (string, string, error) {
	// Build custom claims
	claims := CustomClaims{
		UserID:   data.UserID,
		ClientID: data.Client.GetID(),
		Scope:    data.Request.Scope,
		StandardClaims: jwt.StandardClaims{
			Audience:  data.Client.GetID(),
			Subject:   data.UserID,
			Issuer:    "oauth2-server",
			ExpiresAt: time.Now().Add(data.TokenInfo.GetAccessExpiresIn()).Unix(),
			IssuedAt:  time.Now().Unix(),
			NotBefore: time.Now().Unix(),
		},
	}

	// Set permissions based on user information
	claims.Permissions = getUserPermissions(data.UserID)
	claims.Role = getUserRole(data.UserID)

	// Generate JWT token
	token := jwt.NewWithClaims(cjg.signingMethod, claims)
	access, err := token.SignedString(cjg.signingKey)
	if err != nil {
		return "", "", err
	}

	var refresh string
	if isGenRefresh {
		// Generate refresh token
		refreshClaims := jwt.StandardClaims{
			Subject:   data.UserID,
			Issuer:    "oauth2-server",
			ExpiresAt: time.Now().Add(data.TokenInfo.GetRefreshExpiresIn()).Unix(),
			IssuedAt:  time.Now().Unix(),
		}
		refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
		refresh, err = refreshToken.SignedString(cjg.signingKey)
		if err != nil {
			return "", "", err
		}
	}

	return access, refresh, nil
}

// Get user permissions (retrieve from database in practice)
func getUserPermissions(userID string) []string {
	// Retrieve user permissions from database or cache
	userPerms := map[string][]string{
		"user-123": {"read:profile", "write:profile"},
		"admin-456": {"read:profile", "write:profile", "admin:users", "admin:system"},
	}

	if perms, exists := userPerms[userID]; exists {
		return perms
	}
	return []string{"read:profile"}
}

func getUserRole(userID string) string {
	userRoles := map[string]string{
		"user-123":  "user",
		"admin-456": "admin",
	}

	if role, exists := userRoles[userID]; exists {
		return role
	}
	return "user"
}

// JWT validation middleware
func jwtValidationMiddleware(signingKey []byte) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			tokenString := extractTokenFromHeader(r)
			if tokenString == "" {
				http.Error(w, "Missing authorization token", http.StatusUnauthorized)
				return
			}

			token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
				return signingKey, nil
			})

			if err != nil || !token.Valid {
				http.Error(w, "Invalid token", http.StatusUnauthorized)
				return
			}

			claims := token.Claims.(*CustomClaims)
			
			// Set claims information in context
			ctx := context.WithValue(r.Context(), "user_claims", claims)
			next.ServeHTTP(w, r.WithContext(ctx))
		})
	}
}

func extractTokenFromHeader(r *http.Request) string {
	authHeader := r.Header.Get("Authorization")
	if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
		return authHeader[7:]
	}
	return ""
}

PKCE (Proof Key for Code Exchange) Support

package main

import (
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"net/http"

	"github.com/go-oauth2/oauth2/v4/server"
)

// PKCE challenge generation
func generatePKCE() (verifier, challenge string, err error) {
	// Code Verifier generation (43-128 character random string)
	verifierBytes := make([]byte, 32)
	_, err = rand.Read(verifierBytes)
	if err != nil {
		return
	}
	verifier = base64.RawURLEncoding.EncodeToString(verifierBytes)

	// Code Challenge generation (SHA256 hash)
	hash := sha256.Sum256([]byte(verifier))
	challenge = base64.RawURLEncoding.EncodeToString(hash[:])

	return
}

// PKCE validation function
func validatePKCE(codeVerifier, codeChallenge, codeChallengeMethod string) bool {
	if codeChallengeMethod == "S256" {
		hash := sha256.Sum256([]byte(codeVerifier))
		expected := base64.RawURLEncoding.EncodeToString(hash[:])
		return expected == codeChallenge
	} else if codeChallengeMethod == "plain" {
		return codeVerifier == codeChallenge
	}
	return false
}

// PKCE-enabled OAuth2 server setup
func setupPKCEServer() {
	manager := manage.NewDefaultManager()
	srv := server.NewDefaultServer(manager)

	// PKCE validation handler
	srv.SetExtensionFieldsHandler(func(ti oauth2.TokenInfo) map[string]interface{} {
		return map[string]interface{}{
			"pkce_supported": true,
		}
	})

	// PKCE validation in authorization request processing
	srv.SetClientScopeHandler(func(tgr *oauth2.TokenGenerateRequest) (allowed bool, err error) {
		codeChallenge := tgr.Request.Form.Get("code_challenge")
		codeChallengeMethod := tgr.Request.Form.Get("code_challenge_method")

		// PKCE parameter validation
		if codeChallenge != "" {
			if codeChallengeMethod == "" {
				codeChallengeMethod = "plain"
			}
			
			if codeChallengeMethod != "S256" && codeChallengeMethod != "plain" {
				return false, errors.New("unsupported code challenge method")
			}
			
			// Store PKCE parameters (in practice, store in session or DB)
			storePKCEChallenge(tgr.ClientID, codeChallenge, codeChallengeMethod)
		}

		return true, nil
	})

	// PKCE validation during token exchange
	srv.SetRefreshingScopeHandler(func(tgr *oauth2.TokenGenerateRequest, oldScope string) (allowed bool, err error) {
		codeVerifier := tgr.Request.Form.Get("code_verifier")
		
		if codeVerifier != "" {
			// Get stored challenge
			challenge, method := getPKCEChallenge(tgr.ClientID)
			
			if !validatePKCE(codeVerifier, challenge, method) {
				return false, errors.New("invalid code verifier")
			}
			
			// Remove challenge after successful validation
			removePKCEChallenge(tgr.ClientID)
		}

		return true, nil
	})
}

// PKCE challenge storage, retrieval, and removal (use Redis or DB in practice)
var pkceStore = make(map[string]map[string]string)

func storePKCEChallenge(clientID, challenge, method string) {
	if pkceStore[clientID] == nil {
		pkceStore[clientID] = make(map[string]string)
	}
	pkceStore[clientID]["challenge"] = challenge
	pkceStore[clientID]["method"] = method
}

func getPKCEChallenge(clientID string) (challenge, method string) {
	if client, exists := pkceStore[clientID]; exists {
		return client["challenge"], client["method"]
	}
	return "", ""
}

func removePKCEChallenge(clientID string) {
	delete(pkceStore, clientID)
}

OAuth2 Client Implementation

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strings"
)

// OAuth2 client
type OAuth2Client struct {
	ClientID     string
	ClientSecret string
	RedirectURI  string
	AuthURL      string
	TokenURL     string
	Scope        string
}

// Generate authorization URL
func (c *OAuth2Client) GetAuthorizationURL(state string) string {
	params := url.Values{}
	params.Add("client_id", c.ClientID)
	params.Add("redirect_uri", c.RedirectURI)
	params.Add("response_type", "code")
	params.Add("scope", c.Scope)
	params.Add("state", state)

	return c.AuthURL + "?" + params.Encode()
}

// Generate authorization URL with PKCE support
func (c *OAuth2Client) GetAuthorizationURLWithPKCE(state string) (string, string, error) {
	verifier, challenge, err := generatePKCE()
	if err != nil {
		return "", "", err
	}

	params := url.Values{}
	params.Add("client_id", c.ClientID)
	params.Add("redirect_uri", c.RedirectURI)
	params.Add("response_type", "code")
	params.Add("scope", c.Scope)
	params.Add("state", state)
	params.Add("code_challenge", challenge)
	params.Add("code_challenge_method", "S256")

	authURL := c.AuthURL + "?" + params.Encode()
	return authURL, verifier, nil
}

// Token exchange
type TokenResponse struct {
	AccessToken  string `json:"access_token"`
	TokenType    string `json:"token_type"`
	ExpiresIn    int    `json:"expires_in"`
	RefreshToken string `json:"refresh_token"`
	Scope        string `json:"scope"`
}

func (c *OAuth2Client) ExchangeCodeForToken(code, codeVerifier string) (*TokenResponse, error) {
	data := url.Values{}
	data.Set("grant_type", "authorization_code")
	data.Set("client_id", c.ClientID)
	data.Set("client_secret", c.ClientSecret)
	data.Set("code", code)
	data.Set("redirect_uri", c.RedirectURI)
	
	if codeVerifier != "" {
		data.Set("code_verifier", codeVerifier)
	}

	resp, err := http.Post(c.TokenURL, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("token exchange failed: %s", resp.Status)
	}

	var tokenResp TokenResponse
	err = json.NewDecoder(resp.Body).Decode(&tokenResp)
	return &tokenResp, err
}

// Refresh access token with refresh token
func (c *OAuth2Client) RefreshAccessToken(refreshToken string) (*TokenResponse, error) {
	data := url.Values{}
	data.Set("grant_type", "refresh_token")
	data.Set("client_id", c.ClientID)
	data.Set("client_secret", c.ClientSecret)
	data.Set("refresh_token", refreshToken)

	resp, err := http.Post(c.TokenURL, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("token refresh failed: %s", resp.Status)
	}

	var tokenResp TokenResponse
	err = json.NewDecoder(resp.Body).Decode(&tokenResp)
	return &tokenResp, err
}

// Usage example
func clientExample() {
	client := &OAuth2Client{
		ClientID:     "client-id-123",
		ClientSecret: "client-secret-456",
		RedirectURI:  "http://localhost:3000/callback",
		AuthURL:      "http://localhost:8080/oauth/authorize",
		TokenURL:     "http://localhost:8080/oauth/token",
		Scope:        "read write",
	}

	// Authorization flow with PKCE support
	authURL, verifier, err := client.GetAuthorizationURLWithPKCE("random-state-123")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Authorization URL: %s\n", authURL)
	fmt.Printf("Code Verifier (please save): %s\n", verifier)

	// After callback, exchange authorization code and verifier for token
	// tokenResp, err := client.ExchangeCodeForToken("authorization-code", verifier)
}