https://github.com/simp-lee/guardian
A comprehensive auth library for Gin with JWT support, RBAC, user-specific permissions, hierarchical resources, multi-DB storage (MySQL/PostgreSQL/SQLite), and high-performance caching.
https://github.com/simp-lee/guardian
jwt rbac rbac-authorization
Last synced: 9 months ago
JSON representation
A comprehensive auth library for Gin with JWT support, RBAC, user-specific permissions, hierarchical resources, multi-DB storage (MySQL/PostgreSQL/SQLite), and high-performance caching.
- Host: GitHub
- URL: https://github.com/simp-lee/guardian
- Owner: simp-lee
- License: mit
- Created: 2025-03-31T07:22:05.000Z (about 1 year ago)
- Default Branch: main
- Last Pushed: 2025-05-12T13:21:55.000Z (about 1 year ago)
- Last Synced: 2025-06-12T17:11:48.065Z (about 1 year ago)
- Topics: jwt, rbac, rbac-authorization
- Language: Go
- Homepage: https://pkg.go.dev/github.com/simp-lee/guardian
- Size: 112 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Guardian: Go Authentication & Authorization Library
**English** | [简体中文](README_ZH.md)
## Introduction
`Guardian` is a comprehensive security library specifically designed for `Gin` framework applications, providing role-based access control (RBAC) with JWT authentication to secure your Go APIs.
## Features
- **Role-Based Access Control (RBAC)**: Define granular permissions through roles
- **JWT Authentication**: Secure token generation, validation, and refreshing
- **OAuth 2.0 Support**: Third-party authentication with WeChat, GitHub, and extensible provider support
- **Gin Middleware Integration**: Drop-in security middleware for Gin web applications
- **Permission Management**: Resource and action-based permission checks with hierarchical support
- **Token Lifecycle Management**: Generate, validate, refresh, and revoke tokens
- **Rate Limiting**: Protection against brute force attacks with configurable limits
- **Flexible Storage**: In-memory storage by default with SQL storage support (MySQL, PostgreSQL, SQLite) and customizable backends
- **Hierarchical Resource Paths**: Support for nested resource permissions with wildcards
- **High-Performance Caching**: Built-in multi-level caching system for improved access speed
- **Auto Refresh**: Automatically refresh tokens before expiration
- **Simple Error Handling**: Clean, idiomatic Go error handling using standard error types
- **Configuration Validation**: Comprehensive validation system to prevent configuration errors
- **Event Callbacks**: Hook into authentication and authorization events
- **Session Management**: Redis-based session storage for OAuth state management in production
## Installation
```bash
go get github.com/simp-lee/guardian
```
## Quick Start
```go
package main
import (
"time"
"github.com/gin-gonic/gin"
"github.com/simp-lee/guardian"
"github.com/simp-lee/guardian/middleware"
)
func main() {
// Initialize Guardian with a secret key
g, err := guardian.New(
guardian.WithSecretKey("your-secure-secret-key"),
)
if err != nil {
panic(err)
}
defer g.Close()
// Create roles
g.CreateRole("admin", "Administrator", "Full system access")
g.CreateRole("user", "Regular User", "Basic access")
// Define permissions
g.AddRolePermission("admin", "users", "*") // Admin can do anything with users
g.AddRolePermission("user", "profile", "read") // Users can read profiles
g.AddRolePermission("user", "profile", "update") // Users can update profiles
// Add user-specific permission (directly to a user, without using roles)
g.AddUserPermission("user456", "articles", "delete") // This user can delete articles
// Assign role to user
g.AddUserRole("user123", "user")
// Set up Gin router
r := gin.Default()
// Add Guardian's error handling middleware for structured error responses
r.Use(guardian.ErrorMiddleware(guardian.DefaultHTTPErrorHandler()))
// Public routes
r.POST("/login", g.RateLimit(), func(c *gin.Context) {
// Validate credentials (implementation not shown)
userID := "user123" // Retrieved from authentication
// Generate JWT token with 24-hour validity
token, err := g.GenerateToken(userID, 24*time.Hour)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to generate token"})
return
}
c.JSON(200, gin.H{"token": token})
})
// Protected routes
protected := r.Group("/api")
protected.Use(g.Auth()) // Apply authentication middleware
protected.GET("/profile", func(c *gin.Context) {
// After authentication, user ID is available in context
userID := c.GetString(middleware.CtxKeyUserID)
c.JSON(200, gin.H{"message": "Profile for user: " + userID})
})
// Route with permission check
protected.POST("/users", g.RequirePermission("users", "create"), func(c *gin.Context) {
c.JSON(200, gin.H{"message": "User created"})
})
// Route with role check
protected.GET("/admin", g.RequireRole([]string{"admin"}), func(c *gin.Context) {
c.JSON(200, gin.H{"message": "Admin area"})
})
r.Run(":8080")
}
```
## Core Concepts
### Authentication
`Guardian` supports two primary authentication methods:
1. **JWT Authentication**: Traditional credential-based authentication with JWT tokens
2. **OAuth 2.0 Authentication**: Third-party authentication via providers like GitHub and WeChat
#### JWT Authentication Workflow
1. **Generate Token**: After verifying user credentials, generate a token containing the user's ID and roles
2. **Validate Token**: Guardian validates tokens during requests using the `Auth()` middleware
3. **Token Refresh**: Optionally refresh tokens before expiration to maintain session
#### OAuth 2.0 Authentication Workflow
1. **OAuth Flow**: Users authenticate via trusted third-party providers
2. **User Mapping**: OAuth users are automatically mapped to Guardian users with default or custom logic
3. **JWT Token**: After successful OAuth authentication, Guardian generates a JWT token for subsequent requests
```go
// Generate token with 1 hour validity
token, err := g.GenerateToken("user123", time.Hour)
// Refresh token before expiration
newToken, err := g.RefreshToken(oldToken)
// Revoke token when needed (logout, etc.)
g.RevokeToken(token)
// Revoke all tokens for a user (password change, security issue)
g.RevokeAllUserTokens("user123")
```
### Token Structure
From the JWT service, parsed tokens provide the following information:
```go
type Token struct {
UserID string // User ID
Roles []string // User's role list
ExpiresAt time.Time // Token expiration time
IssuedAt time.Time // Token issue time
TokenID string // Unique token identifier
}
```
### Roles and Permissions
Guardian's RBAC system consists of:
- **Roles**: Named collections of permissions (e.g., "admin", "editor")
- **Permissions**: Defined as resource + action pairs (e.g., "articles:read")
- **Users**: Assigned roles that grant them permissions
```go
// Create a role
g.CreateRole("editor", "Content Editor", "Can manage content")
// Add permissions to role
g.AddRolePermission("editor", "articles", "create")
g.AddRolePermission("editor", "articles", "update")
g.AddRolePermission("editor", "articles", "delete")
g.AddRolePermission("editor", "articles", "read")
// Add multiple permissions at once
g.AddRolePermissions("editor", "comments", []string{"create", "read", "update"})
// Assign role to user
g.AddUserRole("user123", "editor")
// Check if user has specific permission
hasPermission, _ := g.HasPermission("user123", "articles", "update")
```
### User-Specific Permissions
In addition to role-based permissions, `Guardian` allows assigning permissions directly to specific users without using roles. This is useful for granting temporary access, handling exceptions, or fine-tuning permissions for individual users.
```go
// Add direct permission to a user
g.AddUserPermission("user123", "articles", "delete")
// Add multiple permissions at once
g.AddUserPermissions("user123", "comments", []string{"edit", "delete", "moderate"})
// Check if user has direct permission (excluding role-inherited permissions)
hasDirectPermission, _ := g.HasUserDirectPermission("user123", "articles", "delete")
// Remove specific direct permission
g.RemoveUserPermission("user123", "articles", "delete")
// Remove all direct permissions
g.RemoveAllUserPermissions("user123")
```
When checking permissions with `HasPermission`, Guardian first checks user-specific permissions, then checks role-based permissions. This means a user can get permission from either source:
```go
// Assign a role to user
g.AddUserRole("user123", "editor") // editor role has articles:edit permission
// Add direct permission to same user
g.AddUserPermission("user123", "articles", "delete")
// Check permissions - both return true
hasEditPermission, _ := g.HasPermission("user123", "articles", "edit") // From role
hasDeletePermission, _ := g.HasPermission("user123", "articles", "delete") // Direct permission
```
User-specific permissions are ideal for:
1. **Temporary access**: Grant time-limited access without creating new roles
2. **Exceptions**: Allow specific users to perform actions their roles don't permit
3. **Fine-grained control**: Adjust individual permissions without modifying role definitions
4. **Permission overrides**: Create exceptions for specific users without affecting entire role groups
User-specific permissions support all the same features as role permissions, including hierarchical resources and wildcards:
```go
// Using wildcards and hierarchies
g.AddUserPermission("user123", "reports/*", "read") // User can read any report
g.AddUserPermission("user123", "admin/settings", "*") // User can perform any action on admin settings
```
**Note**: When a user's role is removed, their directly assigned permissions remain effective. Only explicit calls to `RemoveUserPermission` or `RemoveAllUserPermissions` will remove direct permissions.
### Caching System
`Guardian` includes a high-performance multi-level caching system that significantly improves performance for frequently accessed data. The caching system automatically manages role and user permission data, reducing access to the underlying storage.
```go
// Enable caching with custom settings
g, _ := guardian.New(
guardian.WithSecretKey("your-secure-secret-key"),
guardian.WithCache(true), // Enable caching (enabled by default)
guardian.WithRoleCacheTTL(1 * time.Hour), // Time-to-live for role cache
guardian.WithUserRoleCacheTTL(30 * time.Minute), // Time-to-live for user-role relations
guardian.WithCacheCleanupInterval(10 * time.Minute), // Cache cleanup interval
)
// Or use a completely custom cache configuration
cacheConfig := storage.CacheConfig{
EnableRoleCache: true,
RoleCacheTTL: 2 * time.Hour,
EnableUserRoleCache: true,
UserRoleCacheTTL: 1 * time.Hour,
CleanupInterval: 15 * time.Minute,
MaxRoleCacheSize: 2000, // Maximum of 2000 roles in cache
MaxUserRoleCacheSize: 10000, // Maximum of 10000 user-role relations in cache
}
g, _ := guardian.New(
guardian.WithSecretKey("your-secure-secret-key"),
guardian.WithCacheConfig(cacheConfig),
)
```
The caching system provides the following capabilities:
1. **Role Caching**: Caches role definitions and permissions, reducing the overhead of repeatedly fetching the same roles from storage.
2. **User-Role Caching**: Caches user-role relationships to speed up permission checks.
3. **Automatic Invalidation**: Automatically invalidates relevant caches when roles or permissions change.
4. **Periodic Cleanup**: Periodically cleans up expired entries to prevent memory leaks.
5. **Size Limiting**: Configurable maximum cache entries to control memory usage.
6. **Performance Monitoring**: Built-in hit and miss rate statistics.
### Hierarchy Notation
Resources can be organized in hierarchies using the forward slash (`/`) as a separator:
```
articles/draft
articles/published
users/profiles
users/settings
```
### Wildcard Permissions
The wildcard character (`*`) can be used in several ways:
1. **Action wildcard**: `"*"` matches any action on a specific resource
```go
g.AddRolePermission("admin", "articles", "*") // Admin can perform any action on articles
```
2. **Resource wildcard**: `"*"` matches any resource for a specific action
```go
g.AddRolePermission("reviewer", "*", "read") // Reviewer can read any resource
```
3. **Global wildcard**: `"*"` for both resource and action grants full access
```go
g.AddRolePermission("superadmin", "*", "*") // Superadmin has full access
```
4. **Hierarchical wildcard**: `"resource/*"` matches all sub-resources
```go
g.AddRolePermission("editor", "articles/*", "update") // Editor can update any article sub-resource
```
### Hierarchical Permission Resolution
When checking permissions for a hierarchical resource path, `Guardian` uses the following process:
1. First checks for an exact match on the specific resource and action
2. Then checks for wildcard actions (`*`) on the specific resource
3. Then checks for the specific action on wildcard resources (`*`)
4. Then checks for wildcard permissions on wildcard resources (`*`, `*`)
5. Finally traverses up the resource hierarchy, checking parent resources with wildcards
For example, when checking permission for `"articles/draft/section"` with `"edit"` action:
1. Checks `"articles/draft/section"` with `"edit"` permission
2. Checks `"articles/draft/section"` with `"*"` permission
3. Checks `"*"` with `"edit"` permission
4. Checks `"*"` with `"*"` permission
5. Checks hierarchical wildcards:
- `"articles/draft/*"` with `"edit"` permission
- `"articles/draft/*"` with `"*"` permission
- `"articles/*"` with `"edit"` permission
- `"articles/*"` with `"*"` permission
This hierarchical resolution enables efficient permission structures where higher-level permissions automatically apply to sub-resources without requiring explicit definitions.
**Important**: Permissions are inherited ONLY through explicit wildcard patterns. A permission on `"articles/drafts"` (without wildcard) will NOT automatically apply to `"articles/drafts/special"`.
**Examples**:
```go
// Set up resource hierarchy permissions
g.AddRolePermission("contentManager", "content/*", "read") // Can read all content
g.AddRolePermission("articleEditor", "content/articles/*", "edit") // Can edit all articles
g.AddRolePermission("draftEditor", "content/articles/drafts/*", "publish") // Can publish drafts
// These permissions apply automatically:
// - contentManager can read content/articles, content/videos, etc.
// - articleEditor can edit content/articles/published, content/articles/drafts, etc.
// - draftEditor can publish any draft article
// Check permissions
hasPermission, _ := g.HasPermission("user123", "content/articles/drafts/article-1", "read")
// Returns true if user123 has contentManager role (due to content/* permission)
```
This approach significantly reduces the number of permission entries required while maintaining granular control over your resources.
### Middleware
`Guardian` provides middleware functions specifically for the `Gin` framework. When using `Guardian` middleware, the order is important:
1. **g.Auth()** should be applied first to authenticate users
2. **g.RequireRole()** or **g.RequirePermission()** should be applied after Auth
3. **g.RateLimit()** can be applied independently, typically for public endpoints like login
```go
// Correct order
protected := r.Group("/api")
protected.Use(g.Auth()) // Authenticate first
protected.Use(g.RequireRole([]string{"admin"})) // Then check roles
// Apply rate limiting to login endpoint
r.POST("/login", g.RateLimit(), loginHandler)
```
### Context Values
After successful authentication, Guardian's Auth middleware sets the following keys in the `Gin` context:
```go
// Access authenticated user ID
userID := c.GetString(middleware.CtxKeyUserID)
// Access user roles (if any)
roles := c.GetStringSlice(middleware.CtxKeyRoles)
// Access the full parsed token object
token := c.MustGet(middleware.CtxKeyToken).(*jwt.Token)
```
These context values are available in all route handlers that execute after the Auth middleware. This allows your API handlers to access user information without having to parse the token again.
Example protected route handler:
```go
protected.GET("/profile", func(c *gin.Context) {
// User ID is automatically available from context
userID := c.GetString(middleware.CtxKeyUserID)
// You can use the user ID to fetch user data
userData, err := userService.GetUserProfile(userID)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to get user data"})
return
}
c.JSON(200, userData)
})
```
Note that these context keys are only set after the Auth middleware has processed a request with a valid token. They will not be available in routes that don't use the Auth middleware (like your login endpoint).
## OAuth 2.0 Authentication
Guardian provides built-in support for OAuth 2.0 authentication with popular providers like WeChat and GitHub. OAuth allows users to authenticate using their existing accounts from third-party services, eliminating the need for users to create separate accounts.
### Supported Providers
- **WeChat**: WeChat Open Platform OAuth 2.0 for web applications
- **GitHub**: GitHub OAuth Apps with configurable scopes
- **Extensible**: Add custom providers by implementing the `Provider` interface
### Quick OAuth Setup
```go
package main
import (
"github.com/gin-gonic/gin"
"github.com/simp-lee/guardian"
"github.com/simp-lee/guardian/middleware"
"github.com/simp-lee/guardian/oauth"
)
func main() {
// Initialize Guardian with OAuth configuration
g, err := guardian.New(
guardian.WithSecretKey("your-secure-secret-key"),
guardian.WithOAuth(&oauth.Config{
BaseURL: "http://localhost:8080",
CallbackPath: "/auth/callback",
StateTimeout: 10 * time.Minute, // OAuth state parameter timeout
Providers: map[string]oauth.ProviderConfig{
"github": {
ClientID: "your-github-client-id",
ClientSecret: "your-github-client-secret",
Scopes: []string{"user:email"},
},
"wechat": {
ClientID: "your-wechat-appid",
ClientSecret: "your-wechat-secret",
Scopes: []string{"snsapi_login"},
Extra: map[string]string{
"language": "zh_CN", // WeChat interface language
},
},
},
}),
)
if err != nil {
panic(err)
}
defer g.Close()
r := gin.Default()
// OAuth routes
auth := r.Group("/auth")
{
auth.GET("/:provider", g.OAuth()) // Start OAuth flow
auth.GET("/callback/:provider", g.OAuthCallback()) // Handle OAuth callback
}
// Protected routes
api := r.Group("/api")
api.Use(g.Auth())
{
api.GET("/profile", func(c *gin.Context) {
userID := c.GetString(middleware.CtxKeyUserID)
roles := c.GetStringSlice(middleware.CtxKeyRoles)
c.JSON(200, gin.H{
"user_id": userID,
"roles": roles,
"message": "Welcome!",
})
})
}
r.Run(":8080")
}
```
### OAuth Flow
The OAuth authentication process follows these steps:
1. **Initiate OAuth**: User visits `/auth/github` or `/auth/wechat` to start OAuth flow
2. **Authorization**: User is redirected to the provider (GitHub/WeChat) for authorization
3. **Callback**: Provider redirects back to `/auth/callback/{provider}` with authorization code
4. **Token Generation**: Guardian processes the callback and generates a JWT token
5. **Authentication**: User can now access protected routes using the JWT token
### Provider Configuration
#### GitHub OAuth Setup
1. Create a GitHub OAuth App in your GitHub Developer Settings
2. Set Authorization callback URL to: `http://your-domain.com/auth/callback/github`
3. Configure in Guardian:
```go
"github": {
ClientID: "your-github-client-id",
ClientSecret: "your-github-client-secret",
Scopes: []string{"user:email"}, // Request email access
}
```
#### WeChat OAuth Setup
1. Register an application on WeChat Open Platform
2. Set redirect URI to: `http://your-domain.com/auth/callback/wechat`
3. Configure in Guardian:
```go
"wechat": {
ClientID: "your-wechat-appid",
ClientSecret: "your-wechat-secret",
Scopes: []string{"snsapi_login"}, // For web login
Extra: map[string]string{
"language": "zh_CN", // Interface language: zh_CN or en
},
}
```
### Production Configuration with Redis
For production environments, use Redis for OAuth session storage to support multiple server instances and ensure session persistence:
```go
import (
"github.com/redis/go-redis/v9"
"github.com/simp-lee/guardian/oauth/sessionstore"
)
func main() {
// Redis client for OAuth session storage
redisClient := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // Set password if required
DB: 0, // Default DB
})
redisStore := sessionstore.NewRedis(redisClient)
g, _ := guardian.New(
guardian.WithSecretKey("your-secure-secret-key"),
guardian.WithOAuth(&oauth.Config{
BaseURL: "https://your-app.com",
CallbackPath: "/auth/callback",
StateTimeout: 10 * time.Minute, // OAuth state timeout
Providers: map[string]oauth.ProviderConfig{
"github": {
ClientID: "your-github-client-id",
ClientSecret: "your-github-client-secret",
Scopes: []string{"user:email"},
},
"wechat": {
ClientID: "your-wechat-appid",
ClientSecret: "your-wechat-secret",
Scopes: []string{"snsapi_login"},
Extra: map[string]string{
"language": "zh_CN",
},
},
},
}, redisStore), // Pass Redis store as second parameter
)
// Setup routes...
}
```
### User Mapping and Customization
OAuth users are automatically mapped to Guardian users with the following pattern:
```go
// Default user ID format: "{provider}_{oauth_user_id}"
userID := "github_12345" // For GitHub user with ID 12345
userID := "wechat_abc123" // For WeChat user with unionid abc123
// Default role assignment
roles := []string{"user"} // OAuth users get "user" role by default
```
You can customize user mapping by providing a custom OAuth user processor:
```go
import "github.com/simp-lee/guardian/oauth"
// Custom user processor
g.SetOAuthUserProcessor(func(oauthUser *oauth.User) (userID string, roles []string, userData map[string]any, err error) {
// Custom logic to map OAuth user to your system
userID = fmt.Sprintf("oauth_%s_%s", oauthUser.Provider, oauthUser.UserID)
// Assign roles based on email domain or other criteria
if strings.HasSuffix(oauthUser.Email, "@yourcompany.com") {
roles = []string{"admin", "user"}
} else {
roles = []string{"user"}
}
// Include comprehensive user data from OAuth profile
userData = map[string]any{
"email": oauthUser.Email,
"name": oauthUser.Name,
"first_name": oauthUser.FirstName,
"last_name": oauthUser.LastName,
"nickname": oauthUser.NickName,
"avatar": oauthUser.AvatarURL,
"location": oauthUser.Location,
"provider": oauthUser.Provider,
"access_token": oauthUser.AccessToken, // Store if needed for API calls
"expires_at": oauthUser.ExpiresAt,
"raw_data": oauthUser.RawData, // Provider-specific additional data
}
return userID, roles, userData, nil
})
```
Available OAuth user information:
```go
import "time"
type oauth.User struct {
// Provider information
Provider string `json:"provider"`
UserID string `json:"user_id"` // Provider's user ID
Email string `json:"email"`
// User profile
Name string `json:"name"`
FirstName string `json:"first_name,omitzero"`
LastName string `json:"last_name,omitzero"`
NickName string `json:"nickname,omitzero"`
AvatarURL string `json:"avatar_url,omitzero"`
Location string `json:"location,omitzero"`
// OAuth tokens
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitzero"`
ExpiresAt time.Time `json:"expires_at,omitzero"`
// Additional data
RawData map[string]any `json:"raw_data,omitzero"`
}
type oauth.Token struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
Scope string `json:"scope"`
}
```
### Custom OAuth Providers
Guardian supports adding custom OAuth providers by implementing the `Provider` interface. Built-in providers (GitHub, WeChat) are automatically available, but you can add additional providers.
#### Provider Interface
```go
type Provider interface {
Name() string // Provider name (e.g., "google", "facebook")
BeginAuth(state string) (Session, error) // Start OAuth flow, return authorization URL
FetchUser(session Session) (*User, error) // Exchange code for user info
RefreshToken(refreshToken string) (*Token, error) // Refresh access token using refresh token
SetCallbackURL(url string) // Set OAuth callback URL
UnmarshalSession(data string) (Session, error) // Create session from marshaled data
}
type Session interface {
GetAuthURL() (string, error) // Get authorization URL for redirect
Authorize(provider Provider, params url.Values) (string, error) // Exchange authorization code for access token
Marshal() string // Serialize session to string
}
```
#### Provider Factory Registration
For fully custom providers, you may need to register them in the provider factory. See `oauth/provider.go` for the provider factory implementation.
## Advanced Configuration
```go
import "github.com/simp-lee/guardian/oauth"
g, _ := guardian.New(
// Required: Secret key for JWT signing
guardian.WithSecretKey("your-secure-secret-key"),
// Optional: Custom storage implementation
guardian.WithStorage(myCustomStorage),
// Optional: Enable auto-refresh when tokens are near expiration
guardian.WithAutoRefresh(30 * time.Minute),
// Optional: Custom token header name
guardian.WithHeaderName("X-Auth-Token"),
// Optional: Set the cleanup interval for expired tokens
guardian.WithCleanupInterval(2 * time.Hour),
// Optional: Enable role and permission caching
guardian.WithCache(true),
// Optional: Set role cache expiration time
guardian.WithRoleCacheTTL(1 * time.Hour),
// Optional: Set user-role cache expiration time
guardian.WithUserRoleCacheTTL(30 * time.Minute),
// Optional: Set cache cleanup interval
guardian.WithCacheCleanupInterval(10 * time.Minute),
// Optional: Configure OAuth 2.0 authentication
guardian.WithOAuth(&oauth.Config{
BaseURL: "http://localhost:8080",
CallbackPath: "/auth/callback",
StateTimeout: 10 * time.Minute,
Providers: map[string]oauth.ProviderConfig{
"github": {
ClientID: "your-github-client-id",
ClientSecret: "your-github-client-secret",
Scopes: []string{"user:email"},
},
},
}),
)
```
### Authentication Middleware Options
```go
g.Auth(
// Custom authentication header name
guardian.WithAuthHeaderName("X-API-Token"),
// Custom token type (default is "Bearer")
guardian.WithAuthTokenType("Custom"),
// Callback for successful authentication
guardian.OnAuthSuccess(func(c *gin.Context, token *jwt.Token) {
// Custom handling logic
}),
// Callback for authentication failure
guardian.OnAuthFailure(func(c *gin.Context, err error) {
// Custom handling logic
}),
// Custom error handler
guardian.WithAuthErrorHandler(func(c *gin.Context, err error) {
c.JSON(401, gin.H{"custom_error": err.Error()})
c.Abort()
}),
)
```
### Auto Refresh Options
If you've enabled auto-refresh in the main configuration:
```go
// Enable auto-refresh in the Guardian constructor
g, _ := guardian.New(
guardian.WithSecretKey("your-secret"),
// Enable global auto-refresh and set the default threshold
// The token will be refreshed if its remaining validity is less than 15 minutes
guardian.WithAutoRefresh(15 * time.Minute),
)
```
When you apply the Auth middleware and auto-refresh is enabled, the refresh functionality will be automatically included:
```go
// Apply standard authentication middleware
router.Use(g.Auth())
```
For more control over the refresh behavior, you can manually configure the auto-refresh middleware:
```go
import (
"github.com/simp-lee/guardian"
"github.com/simp-lee/guardian/middleware"
)
// Apply the auto-refresh middleware with custom options
router.Use(middleware.AutoRefresh(
// Callback when a token is refreshed
guardian.OnTokenRefresh(func(c *gin.Context, oldToken, newToken string) {
// Log refresh event or perform other actions
log.Printf("Token refreshed: %s -> %s", oldToken, newToken)
}),
// Custom refresh threshold for this specific middleware instance
// Overrides the global setting from Guardian constructor
guardian.WithRefreshThreshold(10 * time.Minute),
))
```
When a token is automatically refreshed, the new token is returned in the HTTP response `X-New-Token` header. Clients should check for this header and use the new token in subsequent requests.
### Permission Middleware Options
```go
g.RequirePermission("articles", "edit",
// Callback when permission is granted
guardian.OnPermissionGranted(func(c *gin.Context, userID, resource, action string) {
// Log access, etc.
}),
// Callback when permission is denied
guardian.OnPermissionDenied(func(c *gin.Context, userID, resource, action string) {
// Log denial, etc.
}),
// Custom error handler
guardian.WithPermissionErrorHandler(func(c *gin.Context, err error) {
c.JSON(403, gin.H{"error": "No permission to access this resource"})
c.Abort()
}),
)
```
### Role Middleware Options
```go
g.RequireRole([]string{"admin", "editor"},
// Callback when role check passes
guardian.OnRoleGranted(func(c *gin.Context, userID string, roles []string) {
// Log access, etc.
}),
// Callback when role check fails
guardian.OnRoleDenied(func(c *gin.Context, userID string, roles []string) {
// Log denial, etc.
}),
// Custom error handler
guardian.WithRoleErrorHandler(func(c *gin.Context, err error) {
c.JSON(403, gin.H{"error": "Higher privileges required"})
c.Abort()
}),
)
```
### Rate Limit Middleware Options
```go
g.RateLimit(
// Set the requests per minute limit
guardian.WithRateLimitRequestsPerMinute(60),
// Set the burst limit for concurrent requests
guardian.WithRateLimitBurst(10),
// Set the cleanup interval for rate limit entries
guardian.WithRateLimitCleanupInterval(5 * time.Minute),
// Set the expiration time for rate limit entries
guardian.WithRateLimitExpirationTime(10 * time.Minute),
// Custom error handler
guardian.WithRateLimitErrorHandler(func(c *gin.Context, err error) {
c.JSON(429, gin.H{"error": "Too many requests, please try again later"})
c.Abort()
}),
// Custom key extractor (for identifying request source)
guardian.WithRateLimitKeyExtractor(func(c *gin.Context) string {
// For example, extract user ID from custom header or JWT token
return c.GetHeader("X-User-ID")
}),
// Callback when rate limit is triggered
guardian.OnRateLimited(func(key string, c *gin.Context) {
// Log rate-limited request
}),
// Callback for normal request (includes tokens remaining)
guardian.OnRateLimitRequest(func(key string, remaining int, c *gin.Context) {
// Can be used for monitoring or adding custom response headers
}),
)
```
The rate limit middleware adds the following headers to responses:
- `X-RateLimit-Limit`: Indicates the rate limit ceiling
- `X-RateLimit-Remaining`: Indicates the number of tokens remaining
## Storage Options
`Guardian` supports multiple storage backends for roles, permissions and user-role mappings:
### Memory Storage (Default)
By default, `Guardian` uses an in-memory storage implementation that stores all data in Go maps:
```go
// Using the default memory storage
g, _ := guardian.New(
guardian.WithSecretKey("your-secret-key"),
// No storage option specified - memory storage will be used
)
```
Memory storage is suitable for testing or applications where persistence is not required. However, all data will be lost when the application restarts.
### SQL Storage
For production systems, `Guardian` provides SQL-based storage implementations that persist role and permission data to a database:
```go
import (
"github.com/simp-lee/guardian"
"github.com/simp-lee/guardian/storage"
_ "github.com/go-sql-driver/mysql" // Import MySQL driver
)
// Create MySQL storage
sqlStorage, err := storage.CreateMySQLStorage(
"user:password@tcp(127.0.0.1:3306)/guardian_db?parseTime=true"
)
if err != nil {
log.Fatalf("Failed to create SQL storage: %v", err)
}
// Create Guardian instance with SQL storage
g, err := guardian.New(
guardian.WithSecretKey("your-secure-secret-key"),
guardian.WithStorage(sqlStorage),
)
if err != nil {
log.Fatalf("Failed to create Guardian: %v", err)
}
defer g.Close()
```
#### Supported Database Systems
Guardian currently supports the following database systems:
1. **MySQL/MariaDB**:
```go
storage, err := storage.CreateMySQLStorage(
"user:password@tcp(127.0.0.1:3306)/dbname?parseTime=true"
)
```
2. **PostgreSQL**:
```go
storage, err := storage.CreatePostgresStorage(
"host=localhost port=5432 user=postgres password=secret dbname=guardian_db sslmode=disable"
)
```
3. **SQLite**:
```go
storage, err := storage.CreateSQLiteStorage("path/to/database.db")
// Or use in-memory SQLite database
storage, err := storage.CreateSQLiteStorage(":memory:")
```
#### Using with GORM
If your application already uses `GORM` for database access, you can integrate `Guardian` with your existing database connection:
```go
import (
"github.com/simp-lee/guardian"
"github.com/simp-lee/guardian/storage"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// Create GORM connection
dsn := "user:password@tcp(127.0.0.1:3306)/database?charset=utf8mb4&parseTime=True&loc=Local"
gormDB, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
// Get the underlying *sql.DB instance
sqlDB, err := gormDB.DB()
if err != nil {
log.Fatalf("Failed to get underlying sql.DB: %v", err)
}
// Create Guardian storage using the existing connection
guardianStorage, err := storage.NewSQLStorage(sqlDB)
if err != nil {
log.Fatalf("Failed to create Guardian storage: %v", err)
}
// Create Guardian instance
g, err := guardian.New(
guardian.WithSecretKey("your-secure-key"),
guardian.WithStorage(guardianStorage),
)
```
#### Database Schema
When using SQL storage, `Guardian` automatically creates the following tables:
- `guardian_roles`: Stores role definitions and permissions
- `guardian_user_roles`: Stores user-role associations
The tables are created with `IF NOT EXISTS` clauses, so they won't conflict with existing tables.
### Database Driver Installation
Depending on your chosen database system, you'll need to install the appropriate driver:
```bash
# MySQL driver
go get github.com/go-sql-driver/mysql
# PostgreSQL driver
go get github.com/lib/pq
# SQLite driver (CGO required)
go get github.com/mattn/go-sqlite3
# SQLite driver (Pure Go, no CGO)
go get modernc.org/sqlite
```
### Custom Storage Implementation
You can implement your own storage backend by implementing the Storage interface:
```go
type Storage interface {
// Role management
CreateRole(role *Role) error
GetRole(roleID string) (*Role, error)
UpdateRole(role *Role) error
DeleteRole(roleID string) error
ListRoles() ([]*Role, error)
// User-Role management
AddUserRole(userID, roleID string) error
RemoveUserRole(userID, roleID string) error
GetUserRoles(userID string) ([]string, error)
// Permission management
AddRolePermission(roleID, resource, action string) error
RemoveRolePermission(roleID, resource string) error
HasPermission(roleID, resource, action string) (bool, error)
// Resource management
Close() error
}
```
Then use your custom implementation with Guardian:
```go
customStorage := NewMyCustomStorage(...)
g, _ := guardian.New(
guardian.WithSecretKey("your-secret-key"),
guardian.WithStorage(customStorage),
)
```
## Cache Management
`Guardian` provides complete control over the caching system, allowing you to fine-tune it according to application needs:
```go
// Get cache statistics
roleHits, roleMisses, userRoleHits, userRoleMisses := g.GetCacheStats()
fmt.Printf("Role Cache: Hit Rate %.2f%%\n",
float64(roleHits)/float64(roleHits+roleMisses)*100)
fmt.Printf("User Role Cache: Hit Rate %.2f%%\n",
float64(userRoleHits)/float64(userRoleHits+userRoleMisses)*100)
// Get current cache size
roleCacheSize, userRoleCacheSize := g.GetCacheSize()
fmt.Printf("Cache Size: %d roles, %d user role entries\n",
roleCacheSize, userRoleCacheSize)
// Clear all caches (e.g., after a bulk permission change)
g.ClearCache()
```
Adjust cache configuration to balance memory usage and performance:
```go
// Custom cache configuration suitable for large applications with many users but fewer roles
config := storage.CacheConfig{
EnableRoleCache: true,
RoleCacheTTL: 4 * time.Hour, // Roles do not change often, can be cached longer
EnableUserRoleCache: true,
UserRoleCacheTTL: 30 * time.Minute,
CleanupInterval: 10 * time.Minute,
MaxRoleCacheSize: 500, // System has fewer roles, appropriate limit
MaxUserRoleCacheSize: 100000, // Large number of users, requires larger cache
}
g, _ := guardian.New(
guardian.WithSecretKey("your-secret-key"),
guardian.WithCacheConfig(config),
)
```
## Configuration Validation
Guardian includes a comprehensive configuration validation system that helps prevent common configuration errors and security issues.
### Configuration Validation Features
The validation system checks for:
**Security Configuration:**
- **Secret Key Strength**: Ensures JWT secret keys meet minimum length and entropy requirements
- **Token Timing**: Validates RefreshThreshold and cleanup interval settings
- **OAuth Security**: Validates OAuth provider configurations and callback URLs
**Performance Configuration:**
- **Cache Settings**: Validates cache TTL values and cleanup intervals
- **Rate Limiting**: Ensures rate limit values are within reasonable bounds
- **Storage Configuration**: Validates storage connection parameters
**Correctness Validation:**
- **Required Fields**: Ensures all mandatory configuration fields are provided
- **Value Ranges**: Validates that numeric values are within acceptable ranges
- **Dependencies**: Checks that dependent features are properly configured
### Example: Production vs Development Configuration
```go
// Production configuration
prodGuardian, err := guardian.New(
guardian.WithSecretKey("very-long-secure-production-secret-key-with-high-entropy-2024"),
guardian.WithRefreshThreshold(15 * time.Minute), // Refresh before expiration
guardian.WithCache(true), // Enable caching
)
// Development configuration
devGuardian, err := guardian.New(
guardian.WithSecretKey("dev-secret-key"),
)
```
### Validation Error Handling
Configuration validation errors provide detailed information about what needs to be fixed:
```go
g, err := guardian.New(
guardian.WithSecretKey("short"), // Too short
)
if err != nil {
// err will contain detailed validation failure information
fmt.Printf("Configuration validation failed: %v", err)
}
```
### Custom Validation Rules
You can also manually validate configuration (using exported helpers) before creating Guardian, but in most cases simply calling `guardian.New(...)` is sufficient—invalid settings will be reported with actionable error messages. Advanced per-mode validation helpers are not exposed.
## Error Handling
`Guardian` follows Go's idiomatic error handling principles using standard error types and the `errors` package. All errors are simple, clear, and consistent.
### Simple Error Handling
Guardian uses standard Go error handling with these features:
### Standard Error Definitions
Canonical error variables are defined in their respective packages to avoid duplication:
```go
import (
"errors"
"github.com/simp-lee/guardian"
"github.com/simp-lee/guardian/jwt"
"github.com/simp-lee/guardian/rbac"
"github.com/simp-lee/guardian/storage"
)
// Authentication errors come from the jwt package
if errors.Is(err, jwt.ErrInvalidToken) {
// handle invalid token
}
if errors.Is(err, jwt.ErrExpiredToken) {
// handle expired token
}
// Authorization and input validation errors come from rbac
if errors.Is(err, rbac.ErrInvalidResource) || errors.Is(err, rbac.ErrInvalidAction) {
// handle bad input
}
// Storage-level conflicts/not-found come from storage
if errors.Is(err, storage.ErrRoleNotFound) {
// handle missing role
}
if errors.Is(err, storage.ErrRoleAlreadyExists) {
// handle duplicate role
}
// Guardian package keeps a few generic errors for HTTP layer convenience
if errors.Is(err, guardian.ErrPermissionDenied) {
// handle permission denied
}
```
### HTTP Error Responses
All errors are automatically converted to JSON responses:
```json
{
"error": "invalid token",
"success": false
}
```
Status codes are automatically mapped:
- `401` - Authentication errors (invalid/expired tokens)
- `403` - Authorization errors (insufficient permissions)
- `404` - Resource not found errors
- `409` - Conflict errors (duplicate resources)
- `429` - Rate limit exceeded
- `500` - Internal server errors
### Error Middleware
```go
import "github.com/simp-lee/guardian"
r := gin.Default()
// Add the error handling middleware
r.Use(guardian.ErrorMiddleware(guardian.DefaultHTTPErrorHandler()))
// Your routes...
```
### Custom Error Handling
You can customize error handling using the provided utilities:
```go
// In your middleware or handlers
if err != nil {
guardian.AbortWithError(c, err)
return
}
// Or create contextual errors
err := guardian.NewAuthenticationError("token validation failed", originalErr)
guardian.AbortWithError(c, err)
```
## Security Best Practices
1. **Use strong secret keys** and manage them securely (environment variables, secret management)
2. **Keep token lifetimes short** - hours rather than days when possible
3. **Revoke tokens** when users change passwords or during security incidents
4. **Apply rate limiting** to authentication endpoints
5. **Use HTTPS** for all production deployments
6. **Follow least privilege principle** when assigning permissions
7. **Regularly audit** role assignments and permissions
8. **Handle token refreshes** - Clients should monitor the X-New-Token header and update their stored token
9. **OAuth Security**:
- Store OAuth client secrets securely
- Use HTTPS for OAuth callback URLs
- Validate OAuth state parameters to prevent CSRF attacks
- Use Redis or persistent storage for OAuth sessions in production
- Regularly rotate OAuth client credentials
## Complete API Reference
For the complete API reference, see the [godoc documentation](https://pkg.go.dev/github.com/simp-lee/guardian).
## License
MIT License