Enhance cursor rules and add user management functionality
- Updated cursor rules to emphasize the importance of dependency injection and logging practices. - Introduced a new user management domain, including entities, services, handlers, and validation. - Implemented user authentication features such as login, registration, and role-based access control. - Added Redis integration for token management and caching. - Enhanced API documentation with Swagger for user-related endpoints. - Updated configuration to support new JWT settings and Redis connection parameters.
This commit is contained in:
@@ -0,0 +1,353 @@
|
||||
# Authorization Package
|
||||
|
||||
This package provides JWT-based authorization services for the Tender Management system, following Clean Architecture principles and best practices.
|
||||
|
||||
## Features
|
||||
|
||||
- **JWT Token Management**: Generate and validate access and refresh tokens
|
||||
- **Middleware Support**: Ready-to-use Echo middleware for authentication and authorization
|
||||
- **Role-Based Access Control**: Built-in role validation middleware
|
||||
- **Company Scoping**: Company-level access control middleware
|
||||
- **Token Blacklisting**: Support for token invalidation (Redis-ready)
|
||||
- **Injectable Configuration**: Dependency injection for flexible configuration management
|
||||
- **Configuration Providers**: Multiple ways to create configurations (environment, custom, hybrid)
|
||||
- **Security Best Practices**: Secure token generation and validation
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Basic Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"tm/pkg/authorization"
|
||||
"tm/pkg/logger"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create logger
|
||||
logger := logger.NewLogger()
|
||||
|
||||
// Create configuration provider (environment-based)
|
||||
configProvider := authorization.NewEnvConfigProvider()
|
||||
config := configProvider.CreateConfig()
|
||||
|
||||
// Create authorization service
|
||||
authService := authorization.NewAuthorizationService(config, logger)
|
||||
|
||||
// Generate tokens
|
||||
tokenPair, err := authService.GenerateTokenPair(
|
||||
"user123",
|
||||
"user@example.com",
|
||||
"admin",
|
||||
"company456",
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Validate tokens
|
||||
result, err := authService.ValidateAccessToken(tokenPair.AccessToken)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if result.Valid {
|
||||
fmt.Printf("User ID: %s, Role: %s\n", result.Claims.UserID, result.Claims.Role)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Using with Echo Framework
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"tm/pkg/authorization"
|
||||
"tm/pkg/logger"
|
||||
)
|
||||
|
||||
func main() {
|
||||
e := echo.New()
|
||||
|
||||
// Setup authorization
|
||||
logger := logger.NewLogger()
|
||||
configProvider := authorization.NewEnvConfigProvider()
|
||||
config := configProvider.CreateConfig()
|
||||
authService := authorization.NewAuthorizationService(config, logger)
|
||||
|
||||
// Public routes
|
||||
e.POST("/login", loginHandler)
|
||||
|
||||
// Protected routes
|
||||
protected := e.Group("/api")
|
||||
protected.Use(authorization.AuthMiddleware(authService))
|
||||
{
|
||||
protected.GET("/profile", getProfileHandler)
|
||||
protected.PUT("/profile", updateProfileHandler)
|
||||
}
|
||||
|
||||
// Admin routes
|
||||
admin := protected.Group("/admin")
|
||||
admin.Use(authorization.AdminMiddleware(authService))
|
||||
{
|
||||
admin.POST("/users", createUserHandler)
|
||||
admin.GET("/users", listUsersHandler)
|
||||
}
|
||||
|
||||
// Company-scoped routes
|
||||
company := protected.Group("/company/:company_id")
|
||||
company.Use(authorization.CompanyMiddleware(authService))
|
||||
{
|
||||
company.GET("/tenders", getCompanyTendersHandler)
|
||||
}
|
||||
|
||||
e.Start(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Configuration Management
|
||||
|
||||
The authorization package uses dependency injection for configuration. You can create configurations using different providers:
|
||||
|
||||
#### Configuration Providers
|
||||
|
||||
The package provides several configuration providers that implement the `ConfigProvider` interface:
|
||||
|
||||
```go
|
||||
type ConfigProvider interface {
|
||||
CreateConfig() *AuthorizationConfig
|
||||
}
|
||||
```
|
||||
|
||||
#### Environment-based Configuration
|
||||
|
||||
```go
|
||||
// Create configuration from environment variables
|
||||
configProvider := authorization.NewEnvConfigProvider()
|
||||
config := configProvider.CreateConfig()
|
||||
```
|
||||
|
||||
#### Custom Configuration
|
||||
|
||||
```go
|
||||
// Create custom configuration
|
||||
customConfig := &authorization.AuthorizationConfig{
|
||||
AccessTokenSecret: "my-secret-key",
|
||||
RefreshTokenSecret: "my-refresh-secret",
|
||||
AccessTokenExpiry: 30 * time.Minute,
|
||||
RefreshTokenExpiry: 14 * 24 * time.Hour,
|
||||
Issuer: "my-app",
|
||||
Audience: "my-api",
|
||||
}
|
||||
|
||||
configProvider := authorization.NewCustomConfigProvider(customConfig)
|
||||
config := configProvider.CreateConfig()
|
||||
```
|
||||
|
||||
#### Hybrid Configuration
|
||||
|
||||
```go
|
||||
// Combine multiple configuration sources with priority
|
||||
envProvider := authorization.NewEnvConfigProvider()
|
||||
customProvider := authorization.NewCustomConfigProvider(customConfig)
|
||||
hybridProvider := authorization.NewHybridConfigProvider(envProvider, customProvider)
|
||||
config := hybridProvider.CreateConfig()
|
||||
|
||||
// The hybrid provider applies configurations in order:
|
||||
// 1. Environment variables (base)
|
||||
// 2. Custom overrides (priority)
|
||||
```
|
||||
|
||||
#### File-based Configuration (Future)
|
||||
|
||||
```go
|
||||
// File-based configuration (placeholder for future implementation)
|
||||
fileProvider := authorization.NewFileConfigProvider("config.yaml")
|
||||
config := fileProvider.CreateConfig()
|
||||
```
|
||||
|
||||
#### Environment Variables
|
||||
|
||||
```bash
|
||||
# Required
|
||||
export TM_ACCESS_TOKEN_SECRET="your-super-secret-access-key-here"
|
||||
export TM_REFRESH_TOKEN_SECRET="your-super-secret-refresh-key-here"
|
||||
|
||||
# Optional (with defaults)
|
||||
export TM_ACCESS_TOKEN_EXPIRY="15m"
|
||||
export TM_REFRESH_TOKEN_EXPIRY="168h" # 7 days
|
||||
export TM_JWT_ISSUER="tender-management"
|
||||
export TM_JWT_AUDIENCE="tender-management-api"
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### AuthorizationService Interface
|
||||
|
||||
```go
|
||||
type AuthorizationService interface {
|
||||
// Generate tokens
|
||||
GenerateTokenPair(userID, email, role, companyID string) (*TokenPair, error)
|
||||
GenerateAccessToken(userID, email, role, companyID string) (string, error)
|
||||
GenerateRefreshToken(userID, email, role, companyID string) (string, error)
|
||||
|
||||
// Validate tokens
|
||||
ValidateToken(tokenString string) (*TokenValidationResult, error)
|
||||
ValidateAccessToken(tokenString string) (*TokenValidationResult, error)
|
||||
ValidateRefreshToken(tokenString string) (*TokenValidationResult, error)
|
||||
|
||||
// Token management
|
||||
RefreshAccessToken(refreshToken string) (string, error)
|
||||
InvalidateToken(tokenString string) error
|
||||
IsTokenBlacklisted(tokenString string) (bool, error)
|
||||
}
|
||||
```
|
||||
|
||||
### Middleware Functions
|
||||
|
||||
```go
|
||||
// Authentication middleware
|
||||
AuthMiddleware(authService AuthorizationService) echo.MiddlewareFunc
|
||||
|
||||
// Role-based access control
|
||||
RoleMiddleware(authService AuthorizationService, requiredRoles ...string) echo.MiddlewareFunc
|
||||
|
||||
// Admin-only access
|
||||
AdminMiddleware(authService AuthorizationService) echo.MiddlewareFunc
|
||||
|
||||
// Company-scoped access
|
||||
CompanyMiddleware(authService AuthorizationService) echo.MiddlewareFunc
|
||||
|
||||
// Optional authentication
|
||||
OptionalAuthMiddleware(authService AuthorizationService) echo.MiddlewareFunc
|
||||
```
|
||||
|
||||
### Context Helpers
|
||||
|
||||
```go
|
||||
// Extract user information from context
|
||||
userID, email, role, companyID, authenticated := GetUserFromContext(c)
|
||||
|
||||
// Extract token claims
|
||||
claims := GetTokenClaimsFromContext(c)
|
||||
|
||||
// Check authentication status
|
||||
if IsAuthenticated(c) {
|
||||
// User is authenticated
|
||||
}
|
||||
```
|
||||
|
||||
## Token Structure
|
||||
|
||||
### Access Token Claims
|
||||
|
||||
```json
|
||||
{
|
||||
"user_id": "uuid-string",
|
||||
"email": "user@example.com",
|
||||
"role": "admin",
|
||||
"company_id": "company-uuid",
|
||||
"token_type": "access",
|
||||
"iss": "tender-management",
|
||||
"sub": "uuid-string",
|
||||
"aud": ["tender-management-api"],
|
||||
"exp": 1640995200,
|
||||
"nbf": 1640994300,
|
||||
"iat": 1640994300
|
||||
}
|
||||
```
|
||||
|
||||
### Refresh Token Claims
|
||||
|
||||
```json
|
||||
{
|
||||
"user_id": "uuid-string",
|
||||
"email": "user@example.com",
|
||||
"role": "admin",
|
||||
"company_id": "company-uuid",
|
||||
"token_type": "refresh",
|
||||
"iss": "tender-management",
|
||||
"sub": "uuid-string",
|
||||
"aud": ["tender-management-api"],
|
||||
"exp": 1641600000,
|
||||
"nbf": 1640994300,
|
||||
"iat": 1640994300
|
||||
}
|
||||
```
|
||||
|
||||
## Security Features
|
||||
|
||||
### Token Security
|
||||
|
||||
- **Separate Secrets**: Access and refresh tokens use different signing secrets
|
||||
- **Short Expiry**: Access tokens expire quickly (default: 15 minutes)
|
||||
- **Long Expiry**: Refresh tokens last longer (default: 7 days)
|
||||
- **Type Validation**: Tokens are validated against their intended type
|
||||
- **Blacklist Support**: Tokens can be invalidated and blacklisted
|
||||
|
||||
### Best Practices
|
||||
|
||||
- **Environment Variables**: Never hardcode secrets in code
|
||||
- **Strong Secrets**: Use cryptographically strong random secrets
|
||||
- **HTTPS Only**: Always use HTTPS in production
|
||||
- **Token Rotation**: Implement refresh token rotation for security
|
||||
- **Audit Logging**: Log all authentication and authorization events
|
||||
|
||||
## Configuration Validation
|
||||
|
||||
The package automatically validates configuration on startup:
|
||||
|
||||
```go
|
||||
config := authorization.LoadConfigFromEnv()
|
||||
if err := config.ValidateConfig(); err != nil {
|
||||
log.Fatal("Invalid authorization configuration:", err)
|
||||
}
|
||||
```
|
||||
|
||||
Validation checks:
|
||||
- Non-empty secrets
|
||||
- Valid expiry durations
|
||||
- Access token expiry < refresh token expiry
|
||||
- Non-empty issuer and audience
|
||||
|
||||
## Error Handling
|
||||
|
||||
All functions return descriptive errors:
|
||||
|
||||
```go
|
||||
result, err := authService.ValidateAccessToken(token)
|
||||
if err != nil {
|
||||
// Handle validation error
|
||||
return err
|
||||
}
|
||||
|
||||
if !result.Valid {
|
||||
if result.Expired {
|
||||
return response.Unauthorized(c, "Token has expired")
|
||||
}
|
||||
return response.Unauthorized(c, result.Error)
|
||||
}
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- **Redis Integration**: Full token blacklist implementation
|
||||
- **Rate Limiting**: Token generation rate limiting
|
||||
- **Audit Trail**: Comprehensive authentication audit logging
|
||||
- **Multi-Tenant**: Enhanced company-level security
|
||||
- **OAuth2 Support**: Integration with external OAuth providers
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `github.com/golang-jwt/jwt/v5` - JWT implementation
|
||||
- `github.com/labstack/echo/v4` - HTTP framework
|
||||
- `tm/pkg/logger` - Internal logging package
|
||||
- `tm/pkg/response` - Internal response package
|
||||
|
||||
## License
|
||||
|
||||
This package is part of the Tender Management system and follows the same license terms.
|
||||
@@ -0,0 +1,620 @@
|
||||
package authorization
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/redis"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// TokenType represents the type of token
|
||||
type TokenType string
|
||||
|
||||
const (
|
||||
AccessToken TokenType = "access"
|
||||
RefreshToken TokenType = "refresh"
|
||||
)
|
||||
|
||||
// TokenClaims represents the JWT claims structure
|
||||
type TokenClaims struct {
|
||||
UserID string `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
CompanyID string `json:"company_id,omitempty"`
|
||||
TokenType TokenType `json:"token_type"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// TokenPair represents a pair of access and refresh tokens
|
||||
type TokenPair struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
// TokenValidationResult represents the result of token validation
|
||||
type TokenValidationResult struct {
|
||||
Valid bool
|
||||
Claims *TokenClaims
|
||||
Error string
|
||||
Expired bool
|
||||
}
|
||||
|
||||
// AuthorizationService defines the interface for authorization operations
|
||||
type AuthorizationService interface {
|
||||
// GenerateTokenPair generates both access and refresh tokens
|
||||
GenerateTokenPair(userID, email, role, companyID string) (*TokenPair, error)
|
||||
|
||||
// GenerateAccessToken generates only an access token
|
||||
GenerateAccessToken(userID, email, role, companyID string) (string, error)
|
||||
|
||||
// GenerateRefreshToken generates only a refresh token
|
||||
GenerateRefreshToken(userID, email, role, companyID string) (string, error)
|
||||
|
||||
// ValidateToken validates a token and returns the claims
|
||||
ValidateToken(tokenString string) (*TokenValidationResult, error)
|
||||
|
||||
// ValidateAccessToken validates specifically an access token
|
||||
ValidateAccessToken(tokenString string) (*TokenValidationResult, error)
|
||||
|
||||
// ValidateRefreshToken validates specifically a refresh token
|
||||
ValidateRefreshToken(tokenString string) (*TokenValidationResult, error)
|
||||
|
||||
// RefreshAccessToken generates a new access token using a valid refresh token
|
||||
RefreshAccessToken(refreshToken string) (string, error)
|
||||
|
||||
// InvalidateToken adds a token to the blacklist (if using Redis)
|
||||
InvalidateToken(tokenString string) error
|
||||
|
||||
// IsTokenBlacklisted checks if a token is blacklisted
|
||||
IsTokenBlacklisted(tokenString string) (bool, error)
|
||||
|
||||
// ClearExpiredTokens removes expired tokens from the blacklist
|
||||
ClearExpiredTokens() error
|
||||
|
||||
// GetBlacklistStats returns statistics about the blacklist
|
||||
GetBlacklistStats(ctx context.Context) (map[string]interface{}, error)
|
||||
}
|
||||
|
||||
// AuthorizationConfig holds the configuration for the authorization service
|
||||
type AuthorizationConfig struct {
|
||||
AccessTokenSecret string `json:"access_token_secret"`
|
||||
RefreshTokenSecret string `json:"refresh_token_secret"`
|
||||
AccessTokenExpiry time.Duration `json:"access_token_expiry"`
|
||||
RefreshTokenExpiry time.Duration `json:"refresh_token_expiry"`
|
||||
Issuer string `json:"issuer"`
|
||||
Audience string `json:"audience"`
|
||||
}
|
||||
|
||||
// DefaultConfig returns a default configuration
|
||||
func DefaultConfig() *AuthorizationConfig {
|
||||
return &AuthorizationConfig{
|
||||
AccessTokenSecret: "your-access-token-secret-key-here",
|
||||
RefreshTokenSecret: "your-refresh-token-secret-key-here",
|
||||
AccessTokenExpiry: 15 * time.Minute, // 15 minutes
|
||||
RefreshTokenExpiry: 7 * 24 * time.Hour, // 7 days
|
||||
Issuer: "tender-management",
|
||||
Audience: "tender-management-api",
|
||||
}
|
||||
}
|
||||
|
||||
// Service implements the AuthorizationService interface
|
||||
type Service struct {
|
||||
config *AuthorizationConfig
|
||||
logger logger.Logger
|
||||
redis redis.Client
|
||||
}
|
||||
|
||||
// NewAuthorizationService creates a new authorization service
|
||||
func NewAuthorizationService(config *AuthorizationConfig, logger logger.Logger, redis redis.Client) AuthorizationService {
|
||||
if config == nil {
|
||||
config = DefaultConfig()
|
||||
}
|
||||
|
||||
return &Service{
|
||||
config: config,
|
||||
logger: logger,
|
||||
redis: redis,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateTokenPair generates both access and refresh tokens
|
||||
func (s *Service) GenerateTokenPair(userID, email, role, companyID string) (*TokenPair, error) {
|
||||
s.logger.Info("Generating token pair", map[string]interface{}{
|
||||
"user_id": userID,
|
||||
"email": email,
|
||||
"role": role,
|
||||
})
|
||||
|
||||
accessToken, err := s.GenerateAccessToken(userID, email, role, companyID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to generate access token", map[string]interface{}{
|
||||
"user_id": userID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to generate access token: %w", err)
|
||||
}
|
||||
|
||||
refreshToken, err := s.GenerateRefreshToken(userID, email, role, companyID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to generate refresh token", map[string]interface{}{
|
||||
"user_id": userID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to generate refresh token: %w", err)
|
||||
}
|
||||
|
||||
tokenPair := &TokenPair{
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: int64(s.config.AccessTokenExpiry.Seconds()),
|
||||
}
|
||||
|
||||
s.logger.Info("Token pair generated successfully", map[string]interface{}{
|
||||
"user_id": userID,
|
||||
"expires_in": tokenPair.ExpiresIn,
|
||||
})
|
||||
|
||||
return tokenPair, nil
|
||||
}
|
||||
|
||||
// GenerateAccessToken generates only an access token
|
||||
func (s *Service) GenerateAccessToken(userID, email, role, companyID string) (string, error) {
|
||||
now := time.Now()
|
||||
expiresAt := now.Add(s.config.AccessTokenExpiry)
|
||||
|
||||
claims := &TokenClaims{
|
||||
UserID: userID,
|
||||
Email: email,
|
||||
Role: role,
|
||||
CompanyID: companyID,
|
||||
TokenType: AccessToken,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: s.config.Issuer,
|
||||
Subject: userID,
|
||||
Audience: []string{s.config.Audience},
|
||||
ExpiresAt: jwt.NewNumericDate(expiresAt),
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString([]byte(s.config.AccessTokenSecret))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign access token: %w", err)
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
// GenerateRefreshToken generates only a refresh token
|
||||
func (s *Service) GenerateRefreshToken(userID, email, role, companyID string) (string, error) {
|
||||
now := time.Now()
|
||||
expiresAt := now.Add(s.config.RefreshTokenExpiry)
|
||||
|
||||
claims := &TokenClaims{
|
||||
UserID: userID,
|
||||
Email: email,
|
||||
Role: role,
|
||||
CompanyID: companyID,
|
||||
TokenType: RefreshToken,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: s.config.Issuer,
|
||||
Subject: userID,
|
||||
Audience: []string{s.config.Audience},
|
||||
ExpiresAt: jwt.NewNumericDate(expiresAt),
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString([]byte(s.config.RefreshTokenSecret))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign refresh token: %w", err)
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
// ValidateToken validates a token and returns the claims
|
||||
func (s *Service) ValidateToken(tokenString string) (*TokenValidationResult, error) {
|
||||
// First check if token is blacklisted
|
||||
isBlacklisted, err := s.IsTokenBlacklisted(tokenString)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to check token blacklist", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
// Continue with validation even if blacklist check fails
|
||||
}
|
||||
|
||||
if isBlacklisted {
|
||||
return &TokenValidationResult{
|
||||
Valid: false,
|
||||
Claims: nil,
|
||||
Error: "token is blacklisted",
|
||||
Expired: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Parse the token
|
||||
token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
// Validate the signing method
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
|
||||
// Determine which secret to use based on token type
|
||||
claims, ok := token.Claims.(*TokenClaims)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid token claims")
|
||||
}
|
||||
|
||||
switch claims.TokenType {
|
||||
case AccessToken:
|
||||
return []byte(s.config.AccessTokenSecret), nil
|
||||
case RefreshToken:
|
||||
return []byte(s.config.RefreshTokenSecret), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid token type")
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if jwt.ErrTokenExpired.Error() == err.Error() {
|
||||
return &TokenValidationResult{
|
||||
Valid: false,
|
||||
Claims: nil,
|
||||
Error: "token expired",
|
||||
Expired: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &TokenValidationResult{
|
||||
Valid: false,
|
||||
Claims: nil,
|
||||
Error: fmt.Sprintf("token validation failed: %s", err.Error()),
|
||||
Expired: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
return &TokenValidationResult{
|
||||
Valid: false,
|
||||
Claims: nil,
|
||||
Error: "invalid token",
|
||||
Expired: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(*TokenClaims)
|
||||
if !ok {
|
||||
return &TokenValidationResult{
|
||||
Valid: false,
|
||||
Claims: nil,
|
||||
Error: "invalid token claims",
|
||||
Expired: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &TokenValidationResult{
|
||||
Valid: true,
|
||||
Claims: claims,
|
||||
Error: "",
|
||||
Expired: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ValidateAccessToken validates specifically an access token
|
||||
func (s *Service) ValidateAccessToken(tokenString string) (*TokenValidationResult, error) {
|
||||
result, err := s.ValidateToken(tokenString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !result.Valid {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Check if it's actually an access token
|
||||
if result.Claims.TokenType != AccessToken {
|
||||
return &TokenValidationResult{
|
||||
Valid: false,
|
||||
Claims: nil,
|
||||
Error: "token is not an access token",
|
||||
Expired: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ValidateRefreshToken validates specifically a refresh token
|
||||
func (s *Service) ValidateRefreshToken(tokenString string) (*TokenValidationResult, error) {
|
||||
result, err := s.ValidateToken(tokenString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !result.Valid {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Check if it's actually a refresh token
|
||||
if result.Claims.TokenType != RefreshToken {
|
||||
return &TokenValidationResult{
|
||||
Valid: false,
|
||||
Claims: nil,
|
||||
Error: "token is not a refresh token",
|
||||
Expired: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// RefreshAccessToken generates a new access token using a valid refresh token
|
||||
func (s *Service) RefreshAccessToken(refreshToken string) (string, error) {
|
||||
s.logger.Info("Refreshing access token", map[string]interface{}{
|
||||
"refresh_token_length": len(refreshToken),
|
||||
})
|
||||
|
||||
// Validate the refresh token
|
||||
result, err := s.ValidateRefreshToken(refreshToken)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to validate refresh token", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return "", fmt.Errorf("failed to validate refresh token: %w", err)
|
||||
}
|
||||
|
||||
if !result.Valid {
|
||||
s.logger.Warn("Invalid refresh token provided", map[string]interface{}{
|
||||
"error": result.Error,
|
||||
})
|
||||
return "", fmt.Errorf("invalid refresh token: %s", result.Error)
|
||||
}
|
||||
|
||||
// Generate new access token
|
||||
newAccessToken, err := s.GenerateAccessToken(
|
||||
result.Claims.UserID,
|
||||
result.Claims.Email,
|
||||
result.Claims.Role,
|
||||
result.Claims.CompanyID,
|
||||
)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to generate new access token", map[string]interface{}{
|
||||
"user_id": result.Claims.UserID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return "", fmt.Errorf("failed to generate new access token: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("Access token refreshed successfully", map[string]interface{}{
|
||||
"user_id": result.Claims.UserID,
|
||||
})
|
||||
|
||||
return newAccessToken, nil
|
||||
}
|
||||
|
||||
// generateTokenHash generates a SHA-256 hash of the token for storage
|
||||
func (s *Service) generateTokenHash(tokenString string) string {
|
||||
hash := sha256.Sum256([]byte(tokenString))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// InvalidateToken adds a token to the blacklist
|
||||
func (s *Service) InvalidateToken(tokenString string) error {
|
||||
if s.redis == nil {
|
||||
s.logger.Warn("Redis client not available, token blacklisting disabled", map[string]interface{}{
|
||||
"token_length": len(tokenString),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate hash of the token for storage
|
||||
tokenHash := s.generateTokenHash(tokenString)
|
||||
|
||||
// Parse token to get expiration time for blacklist TTL
|
||||
claims := &TokenClaims{}
|
||||
_, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
|
||||
// Determine which secret to use based on token type
|
||||
claims, ok := token.Claims.(*TokenClaims)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid token claims")
|
||||
}
|
||||
|
||||
switch claims.TokenType {
|
||||
case AccessToken:
|
||||
return []byte(s.config.AccessTokenSecret), nil
|
||||
case RefreshToken:
|
||||
return []byte(s.config.RefreshTokenSecret), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid token type")
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to parse token for blacklisting", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
// Still add to blacklist with default TTL even if parsing fails
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Use default TTL (24 hours) if we can't determine actual expiration
|
||||
defaultTTL := 24 * time.Hour
|
||||
err = s.redis.Set(ctx, "blacklist:"+tokenHash, "1", defaultTTL)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to add token to blacklist", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return fmt.Errorf("failed to add token to blacklist: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("Token added to blacklist with default TTL", map[string]interface{}{
|
||||
"token_hash": tokenHash,
|
||||
"ttl": defaultTTL.String(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Calculate TTL based on token expiration
|
||||
var ttl time.Duration
|
||||
if claims.ExpiresAt != nil {
|
||||
ttl = time.Until(claims.ExpiresAt.Time)
|
||||
// Add some buffer time to ensure token is blacklisted until it would have expired
|
||||
ttl += 5 * time.Minute
|
||||
} else {
|
||||
// Fallback to default TTL if no expiration
|
||||
ttl = 24 * time.Hour
|
||||
}
|
||||
|
||||
// Add token to blacklist with TTL
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err = s.redis.Set(ctx, "blacklist:"+tokenHash, "1", ttl)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to add token to blacklist", map[string]interface{}{
|
||||
"token_hash": tokenHash,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return fmt.Errorf("failed to add token to blacklist: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("Token successfully added to blacklist", map[string]interface{}{
|
||||
"token_hash": tokenHash,
|
||||
"ttl": ttl.String(),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsTokenBlacklisted checks if a token is blacklisted
|
||||
func (s *Service) IsTokenBlacklisted(tokenString string) (bool, error) {
|
||||
if s.redis == nil {
|
||||
// If Redis is not available, assume token is not blacklisted
|
||||
// This allows the system to continue functioning without Redis
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Generate hash of the token for lookup
|
||||
tokenHash := s.generateTokenHash(tokenString)
|
||||
|
||||
// Check if token exists in blacklist
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
exists, err := s.redis.Exists(ctx, "blacklist:"+tokenHash)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to check token blacklist", map[string]interface{}{
|
||||
"token_hash": tokenHash,
|
||||
"error": err.Error(),
|
||||
})
|
||||
// If we can't check the blacklist, assume token is not blacklisted
|
||||
// This prevents blocking legitimate requests due to Redis issues
|
||||
return false, nil
|
||||
}
|
||||
|
||||
isBlacklisted := exists > 0
|
||||
|
||||
if isBlacklisted {
|
||||
s.logger.Debug("Token found in blacklist", map[string]interface{}{
|
||||
"token_hash": tokenHash,
|
||||
})
|
||||
} else {
|
||||
s.logger.Debug("Token not found in blacklist", map[string]interface{}{
|
||||
"token_hash": tokenHash,
|
||||
})
|
||||
}
|
||||
|
||||
return isBlacklisted, nil
|
||||
}
|
||||
|
||||
// ClearExpiredTokens removes expired tokens from the blacklist
|
||||
// This is useful for cleanup operations
|
||||
func (s *Service) ClearExpiredTokens() error {
|
||||
if s.redis == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.logger.Info("Clearing expired tokens from blacklist", map[string]interface{}{})
|
||||
|
||||
// Note: Redis automatically expires keys based on TTL
|
||||
// This method is provided for manual cleanup if needed
|
||||
// In most cases, Redis TTL handling is sufficient
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetBlacklistStats returns statistics about the blacklist
|
||||
func (s *Service) GetBlacklistStats(ctx context.Context) (map[string]interface{}, error) {
|
||||
if s.redis == nil {
|
||||
return map[string]interface{}{
|
||||
"redis_available": false,
|
||||
"message": "Redis client not available",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// This is a placeholder for future implementation
|
||||
// Could return metrics like total blacklisted tokens, memory usage, etc.
|
||||
return map[string]interface{}{
|
||||
"redis_available": true,
|
||||
"message": "Blacklist statistics not yet implemented",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GenerateRandomSecret generates a random secret key for JWT signing
|
||||
func GenerateRandomSecret(length int) (string, error) {
|
||||
if length < 32 {
|
||||
length = 32 // Minimum recommended length
|
||||
}
|
||||
|
||||
bytes := make([]byte, length)
|
||||
_, err := rand.Read(bytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate random bytes: %w", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", bytes), nil
|
||||
}
|
||||
|
||||
// GenerateRSAKeyPair generates an RSA key pair for JWT signing
|
||||
func GenerateRSAKeyPair(bits int) (privateKeyPEM, publicKeyPEM string, err error) {
|
||||
if bits < 2048 {
|
||||
bits = 2048 // Minimum recommended bits
|
||||
}
|
||||
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, bits)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to generate RSA key: %w", err)
|
||||
}
|
||||
|
||||
// Encode private key
|
||||
privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey)
|
||||
privateKeyPEM = string(pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: privateKeyBytes,
|
||||
}))
|
||||
|
||||
// Encode public key
|
||||
publicKeyBytes := x509.MarshalPKCS1PublicKey(&privateKey.PublicKey)
|
||||
publicKeyPEM = string(pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PUBLIC KEY",
|
||||
Bytes: publicKeyBytes,
|
||||
}))
|
||||
|
||||
return privateKeyPEM, publicKeyPEM, nil
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package authorization
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
"tm/pkg/logger"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockRedisClient is a mock implementation of the Redis client interface
|
||||
type MockRedisClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockRedisClient) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
|
||||
args := m.Called(ctx, key, value, expiration)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockRedisClient) Get(ctx context.Context, key string) (string, error) {
|
||||
args := m.Called(ctx, key)
|
||||
return args.String(0), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockRedisClient) Del(ctx context.Context, keys ...string) error {
|
||||
args := m.Called(ctx, keys)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockRedisClient) Exists(ctx context.Context, keys ...string) (int64, error) {
|
||||
args := m.Called(ctx, keys)
|
||||
return args.Get(0).(int64), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockRedisClient) Close() error {
|
||||
args := m.Called()
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// MockLogger is a mock implementation of the logger interface
|
||||
type MockLogger struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockLogger) Debug(msg string, fields map[string]interface{}) {
|
||||
m.Called(msg, fields)
|
||||
}
|
||||
|
||||
func (m *MockLogger) Info(msg string, fields map[string]interface{}) {
|
||||
m.Called(msg, fields)
|
||||
}
|
||||
|
||||
func (m *MockLogger) Warn(msg string, fields map[string]interface{}) {
|
||||
m.Called(msg, fields)
|
||||
}
|
||||
|
||||
func (m *MockLogger) Error(msg string, fields map[string]interface{}) {
|
||||
m.Called(msg, fields)
|
||||
}
|
||||
|
||||
func (m *MockLogger) Fatal(msg string, fields map[string]interface{}) {
|
||||
m.Called(msg, fields)
|
||||
}
|
||||
|
||||
func (m *MockLogger) Sync() error {
|
||||
args := m.Called()
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockLogger) WithFields(fields map[string]interface{}) logger.Logger {
|
||||
args := m.Called(fields)
|
||||
return args.Get(0).(logger.Logger)
|
||||
}
|
||||
|
||||
func TestGenerateTokenHash(t *testing.T) {
|
||||
service := &Service{}
|
||||
|
||||
token1 := "test-token-1"
|
||||
token2 := "test-token-2"
|
||||
|
||||
hash1 := service.generateTokenHash(token1)
|
||||
hash2 := service.generateTokenHash(token2)
|
||||
|
||||
// Hash should be consistent for the same input
|
||||
assert.Equal(t, service.generateTokenHash(token1), hash1)
|
||||
|
||||
// Different tokens should have different hashes
|
||||
assert.NotEqual(t, hash1, hash2)
|
||||
|
||||
// Hash should be a valid hex string
|
||||
assert.Len(t, hash1, 64) // SHA-256 produces 32 bytes = 64 hex chars
|
||||
}
|
||||
|
||||
func TestInvalidateToken_WithRedis(t *testing.T) {
|
||||
mockRedis := new(MockRedisClient)
|
||||
mockLogger := new(MockLogger)
|
||||
|
||||
config := &AuthorizationConfig{
|
||||
AccessTokenSecret: "test-secret",
|
||||
RefreshTokenSecret: "test-refresh-secret",
|
||||
AccessTokenExpiry: 15 * time.Minute,
|
||||
RefreshTokenExpiry: 7 * 24 * time.Hour,
|
||||
Issuer: "test",
|
||||
Audience: "test-api",
|
||||
}
|
||||
|
||||
service := &Service{
|
||||
config: config,
|
||||
logger: mockLogger,
|
||||
redis: mockRedis,
|
||||
}
|
||||
|
||||
// Generate a valid token first
|
||||
token, err := service.GenerateAccessToken("user123", "test@example.com", "user", "company123")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Mock Redis Set call
|
||||
mockRedis.On("Set", mock.Anything, mock.Anything, "1", mock.Anything).Return(nil)
|
||||
|
||||
// Mock logger calls
|
||||
mockLogger.On("Info", mock.Anything, mock.Anything).Return()
|
||||
|
||||
// Test invalidation
|
||||
err = service.InvalidateToken(token)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify Redis was called
|
||||
mockRedis.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestInvalidateToken_WithoutRedis(t *testing.T) {
|
||||
mockLogger := new(MockLogger)
|
||||
|
||||
config := &AuthorizationConfig{
|
||||
AccessTokenSecret: "test-secret",
|
||||
RefreshTokenSecret: "test-refresh-secret",
|
||||
AccessTokenExpiry: 15 * time.Minute,
|
||||
RefreshTokenExpiry: 7 * 24 * time.Hour,
|
||||
Issuer: "test",
|
||||
Audience: "test-api",
|
||||
}
|
||||
|
||||
service := &Service{
|
||||
config: config,
|
||||
logger: mockLogger,
|
||||
redis: nil, // No Redis client
|
||||
}
|
||||
|
||||
// Mock logger calls
|
||||
mockLogger.On("Warn", mock.Anything, mock.Anything).Return()
|
||||
|
||||
// Test invalidation without Redis
|
||||
err := service.InvalidateToken("test-token")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify warning was logged
|
||||
mockLogger.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestIsTokenBlacklisted_WithRedis(t *testing.T) {
|
||||
mockRedis := new(MockRedisClient)
|
||||
mockLogger := new(MockLogger)
|
||||
|
||||
config := &AuthorizationConfig{
|
||||
AccessTokenSecret: "test-secret",
|
||||
RefreshTokenSecret: "test-refresh-secret",
|
||||
AccessTokenExpiry: 15 * time.Minute,
|
||||
RefreshTokenExpiry: 7 * 24 * time.Hour,
|
||||
Issuer: "test",
|
||||
Audience: "test-api",
|
||||
}
|
||||
|
||||
service := &Service{
|
||||
config: config,
|
||||
logger: mockLogger,
|
||||
redis: mockRedis,
|
||||
}
|
||||
|
||||
token := "test-token"
|
||||
tokenHash := service.generateTokenHash(token)
|
||||
|
||||
// Test case 1: Token is blacklisted
|
||||
mockRedis.On("Exists", mock.Anything, []string{"blacklist:" + tokenHash}).Return(int64(1), nil).Once()
|
||||
mockLogger.On("Debug", mock.Anything, mock.Anything).Return()
|
||||
|
||||
isBlacklisted, err := service.IsTokenBlacklisted(token)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, isBlacklisted)
|
||||
|
||||
// Test case 2: Token is not blacklisted
|
||||
mockRedis.On("Exists", mock.Anything, []string{"blacklist:" + tokenHash}).Return(int64(0), nil).Once()
|
||||
mockLogger.On("Debug", mock.Anything, mock.Anything).Return()
|
||||
|
||||
isBlacklisted, err = service.IsTokenBlacklisted(token)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, isBlacklisted)
|
||||
|
||||
mockRedis.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestIsTokenBlacklisted_WithoutRedis(t *testing.T) {
|
||||
mockLogger := new(MockLogger)
|
||||
|
||||
config := &AuthorizationConfig{
|
||||
AccessTokenSecret: "test-secret",
|
||||
RefreshTokenSecret: "test-refresh-secret",
|
||||
AccessTokenExpiry: 15 * time.Minute,
|
||||
RefreshTokenExpiry: 7 * 24 * time.Hour,
|
||||
Issuer: "test",
|
||||
Audience: "test-api",
|
||||
}
|
||||
|
||||
service := &Service{
|
||||
config: config,
|
||||
logger: mockLogger,
|
||||
redis: nil, // No Redis client
|
||||
}
|
||||
|
||||
// Test blacklist check without Redis
|
||||
isBlacklisted, err := service.IsTokenBlacklisted("test-token")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, isBlacklisted)
|
||||
}
|
||||
|
||||
func TestClearExpiredTokens(t *testing.T) {
|
||||
mockRedis := new(MockRedisClient)
|
||||
mockLogger := new(MockLogger)
|
||||
|
||||
service := &Service{
|
||||
logger: mockLogger,
|
||||
redis: mockRedis,
|
||||
}
|
||||
|
||||
// Mock logger call
|
||||
mockLogger.On("Info", mock.Anything, mock.Anything).Return()
|
||||
|
||||
// Test clearing expired tokens
|
||||
err := service.ClearExpiredTokens()
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockLogger.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestGetBlacklistStats(t *testing.T) {
|
||||
mockRedis := new(MockRedisClient)
|
||||
mockLogger := new(MockLogger)
|
||||
|
||||
service := &Service{
|
||||
logger: mockLogger,
|
||||
redis: mockRedis,
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with Redis available
|
||||
stats, err := service.GetBlacklistStats(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, true, stats["redis_available"])
|
||||
|
||||
// Test without Redis
|
||||
service.redis = nil
|
||||
stats, err = service.GetBlacklistStats(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, false, stats["redis_available"])
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package authorization
|
||||
|
||||
import "errors"
|
||||
|
||||
// Authorization errors
|
||||
var (
|
||||
ErrEmptyAccessTokenSecret = errors.New("access token secret cannot be empty")
|
||||
ErrEmptyRefreshTokenSecret = errors.New("refresh token secret cannot be empty")
|
||||
ErrInvalidAccessTokenExpiry = errors.New("access token expiry must be greater than 0")
|
||||
ErrInvalidRefreshTokenExpiry = errors.New("refresh token expiry must be greater than 0")
|
||||
ErrAccessTokenExpiryTooLong = errors.New("access token expiry cannot be longer than refresh token expiry")
|
||||
ErrEmptyIssuer = errors.New("JWT issuer cannot be empty")
|
||||
ErrEmptyAudience = errors.New("JWT audience cannot be empty")
|
||||
ErrInvalidToken = errors.New("invalid token")
|
||||
ErrTokenExpired = errors.New("token has expired")
|
||||
ErrTokenBlacklisted = errors.New("token is blacklisted")
|
||||
ErrInvalidTokenType = errors.New("invalid token type")
|
||||
ErrInvalidSigningMethod = errors.New("invalid signing method")
|
||||
ErrInvalidClaims = errors.New("invalid token claims")
|
||||
ErrTokenValidationFailed = errors.New("token validation failed")
|
||||
ErrInsufficientPermissions = errors.New("insufficient permissions")
|
||||
ErrCompanyAccessDenied = errors.New("access denied to this company")
|
||||
)
|
||||
Reference in New Issue
Block a user