go-oauth2
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)
}