This commit is contained in:
2025-08-26 19:16:41 -04:00
parent 7ca61eb712
commit 6725529b01
113 changed files with 0 additions and 337 deletions

View File

@ -0,0 +1,289 @@
package services
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"time"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
"go.uber.org/zap"
"github.com/kms/api-key-service/internal/audit"
"github.com/kms/api-key-service/internal/domain"
"github.com/kms/api-key-service/internal/repository"
)
// applicationService implements the ApplicationService interface
type applicationService struct {
appRepo repository.ApplicationRepository
auditRepo repository.AuditRepository
auditLogger audit.AuditLogger
logger *zap.Logger
validator *validator.Validate
}
// NewApplicationService creates a new application service
func NewApplicationService(appRepo repository.ApplicationRepository, auditRepo repository.AuditRepository, logger *zap.Logger) ApplicationService {
// Create audit logger with audit package's repository interface
auditRepoImpl := &auditRepositoryAdapter{repo: auditRepo}
auditLogger := audit.NewAuditLogger(nil, logger, auditRepoImpl) // config can be nil for now
return &applicationService{
appRepo: appRepo,
auditRepo: auditRepo,
auditLogger: auditLogger,
logger: logger,
validator: validator.New(),
}
}
// auditRepositoryAdapter adapts repository.AuditRepository to audit.AuditRepository
type auditRepositoryAdapter struct {
repo repository.AuditRepository
}
func (a *auditRepositoryAdapter) Create(ctx context.Context, event *audit.AuditEvent) error {
return a.repo.Create(ctx, event)
}
func (a *auditRepositoryAdapter) Query(ctx context.Context, filter *audit.AuditFilter) ([]*audit.AuditEvent, error) {
return a.repo.Query(ctx, filter)
}
func (a *auditRepositoryAdapter) GetStats(ctx context.Context, filter *audit.AuditStatsFilter) (*audit.AuditStats, error) {
return a.repo.GetStats(ctx, filter)
}
func (a *auditRepositoryAdapter) DeleteOldEvents(ctx context.Context, olderThan time.Time) (int, error) {
return a.repo.DeleteOldEvents(ctx, olderThan)
}
func (a *auditRepositoryAdapter) GetByID(ctx context.Context, eventID uuid.UUID) (*audit.AuditEvent, error) {
return a.repo.GetByID(ctx, eventID)
}
// Create creates a new application
func (s *applicationService) Create(ctx context.Context, req *domain.CreateApplicationRequest, userID string) (*domain.Application, error) {
s.logger.Info("Creating application", zap.String("app_id", req.AppID), zap.String("user_id", userID))
// Input validation using validator
if err := s.validator.Struct(req); err != nil {
s.logger.Warn("Application creation request validation failed",
zap.String("app_id", req.AppID),
zap.String("user_id", userID),
zap.Error(err))
return nil, fmt.Errorf("validation failed: %w", err)
}
// Manual validation for Duration fields
if req.TokenRenewalDuration.Duration <= 0 {
return nil, fmt.Errorf("token_renewal_duration must be greater than 0")
}
if req.MaxTokenDuration.Duration <= 0 {
return nil, fmt.Errorf("max_token_duration must be greater than 0")
}
// Basic permission validation - check if user can create applications
// In a real system, this would check against user roles/permissions
if userID == "" {
return nil, fmt.Errorf("user authentication required")
}
// Additional business logic validation
if req.TokenRenewalDuration.Duration > req.MaxTokenDuration.Duration {
return nil, fmt.Errorf("token renewal duration cannot be greater than max token duration")
}
app := &domain.Application{
AppID: req.AppID,
AppLink: req.AppLink,
Type: req.Type,
CallbackURL: req.CallbackURL,
HMACKey: generateHMACKey(), // Uses crypto/rand for secure key generation
TokenPrefix: req.TokenPrefix,
TokenRenewalDuration: req.TokenRenewalDuration,
MaxTokenDuration: req.MaxTokenDuration,
Owner: req.Owner,
}
if err := s.appRepo.Create(ctx, app); err != nil {
s.logger.Error("Failed to create application", zap.Error(err), zap.String("app_id", req.AppID))
// Log audit event for failed creation
s.auditLogger.LogEvent(ctx, audit.NewAuditEventBuilder(audit.EventTypeAppCreated).
WithSeverity(audit.SeverityError).
WithStatus(audit.StatusFailure).
WithActor(userID, "user", "").
WithResource(req.AppID, "application").
WithAction("create").
WithDescription(fmt.Sprintf("Failed to create application %s", req.AppID)).
WithDetails(map[string]interface{}{
"error": err.Error(),
"app_id": req.AppID,
"user_id": userID,
}).
Build())
return nil, fmt.Errorf("failed to create application: %w", err)
}
// Log successful creation
s.auditLogger.LogEvent(ctx, audit.NewAuditEventBuilder(audit.EventTypeAppCreated).
WithSeverity(audit.SeverityInfo).
WithStatus(audit.StatusSuccess).
WithActor(userID, "user", "").
WithResource(app.AppID, "application").
WithAction("create").
WithDescription(fmt.Sprintf("Created application %s", app.AppID)).
WithDetails(map[string]interface{}{
"app_id": app.AppID,
"app_link": app.AppLink,
"type": app.Type,
"user_id": userID,
"owner_name": app.Owner.Name,
"owner_type": app.Owner.Type,
}).
Build())
s.logger.Info("Application created successfully", zap.String("app_id", app.AppID))
return app, nil
}
// GetByID retrieves an application by its ID
func (s *applicationService) GetByID(ctx context.Context, appID string) (*domain.Application, error) {
s.logger.Debug("Getting application by ID", zap.String("app_id", appID))
app, err := s.appRepo.GetByID(ctx, appID)
if err != nil {
s.logger.Error("Failed to get application", zap.Error(err), zap.String("app_id", appID))
return nil, fmt.Errorf("failed to get application: %w", err)
}
return app, nil
}
// List retrieves applications with pagination
func (s *applicationService) List(ctx context.Context, limit, offset int) ([]*domain.Application, error) {
s.logger.Debug("Listing applications", zap.Int("limit", limit), zap.Int("offset", offset))
if limit <= 0 {
limit = 50 // Default limit
}
if limit > 100 {
limit = 100 // Max limit
}
apps, err := s.appRepo.List(ctx, limit, offset)
if err != nil {
s.logger.Error("Failed to list applications", zap.Error(err))
return nil, fmt.Errorf("failed to list applications: %w", err)
}
s.logger.Debug("Listed applications", zap.Int("count", len(apps)))
return apps, nil
}
// Update updates an existing application
func (s *applicationService) Update(ctx context.Context, appID string, updates *domain.UpdateApplicationRequest, userID string) (*domain.Application, error) {
s.logger.Info("Updating application", zap.String("app_id", appID), zap.String("user_id", userID))
// Input validation using validator
if err := s.validator.Struct(updates); err != nil {
s.logger.Warn("Application update request validation failed",
zap.String("app_id", appID),
zap.String("user_id", userID),
zap.Error(err))
return nil, fmt.Errorf("validation failed: %w", err)
}
// Basic permission validation - check if user can update applications
// In a real system, this would check against user roles/permissions and application ownership
if userID == "" {
return nil, fmt.Errorf("user authentication required")
}
// Manual validation for Duration fields
if updates.TokenRenewalDuration != nil && updates.TokenRenewalDuration.Duration <= 0 {
return nil, fmt.Errorf("token_renewal_duration must be greater than 0")
}
if updates.MaxTokenDuration != nil && updates.MaxTokenDuration.Duration <= 0 {
return nil, fmt.Errorf("max_token_duration must be greater than 0")
}
// Additional business logic validation
if updates.TokenRenewalDuration != nil && updates.MaxTokenDuration != nil {
if updates.TokenRenewalDuration.Duration > updates.MaxTokenDuration.Duration {
return nil, fmt.Errorf("token renewal duration cannot be greater than max token duration")
}
}
app, err := s.appRepo.Update(ctx, appID, updates)
if err != nil {
s.logger.Error("Failed to update application", zap.Error(err), zap.String("app_id", appID))
return nil, fmt.Errorf("failed to update application: %w", err)
}
s.logger.Info("Application updated successfully", zap.String("app_id", appID))
return app, nil
}
// Delete deletes an application
func (s *applicationService) Delete(ctx context.Context, appID string, userID string) error {
s.logger.Info("Deleting application", zap.String("app_id", appID), zap.String("user_id", userID))
// Basic permission validation - check if user can delete applications
// In a real system, this would check against user roles/permissions and application ownership
if userID == "" {
return fmt.Errorf("user authentication required")
}
// Input validation - check appID format
if appID == "" {
return fmt.Errorf("application ID is required")
}
// Check if application exists before attempting deletion
_, err := s.appRepo.GetByID(ctx, appID)
if err != nil {
s.logger.Warn("Application not found for deletion",
zap.String("app_id", appID),
zap.String("user_id", userID))
return fmt.Errorf("application not found: %w", err)
}
// Check for existing tokens and handle appropriately
// In a production system, we would implement one of these strategies:
// 1. Prevent deletion if active tokens exist (safe approach)
// 2. Cascade delete all associated tokens and permissions (clean approach)
// 3. Mark application as deleted but keep tokens active until they expire
// For now, log a warning about potential orphaned tokens
s.logger.Warn("Application deletion will proceed without checking for existing tokens",
zap.String("app_id", appID),
zap.String("recommendation", "implement token cleanup or prevention logic"))
if err := s.appRepo.Delete(ctx, appID); err != nil {
s.logger.Error("Failed to delete application", zap.Error(err), zap.String("app_id", appID))
return fmt.Errorf("failed to delete application: %w", err)
}
s.logger.Info("Application deleted successfully", zap.String("app_id", appID))
return nil
}
// generateHMACKey generates a secure HMAC key
func generateHMACKey() string {
// Generate 32 bytes (256 bits) of cryptographically secure random data
key := make([]byte, 32)
_, err := rand.Read(key)
if err != nil {
// If we can't generate random bytes, this is a critical security issue
panic(fmt.Sprintf("Failed to generate cryptographic key: %v", err))
}
// Return as hex-encoded string for storage
return hex.EncodeToString(key)
}

View File

@ -0,0 +1,305 @@
package services
import (
"context"
"fmt"
"strings"
"time"
"go.uber.org/zap"
"github.com/kms/api-key-service/internal/auth"
"github.com/kms/api-key-service/internal/config"
"github.com/kms/api-key-service/internal/domain"
"github.com/kms/api-key-service/internal/errors"
"github.com/kms/api-key-service/internal/repository"
)
// authenticationService implements the AuthenticationService interface
type authenticationService struct {
config config.ConfigProvider
logger *zap.Logger
jwtManager *auth.JWTManager
permissionRepo repository.PermissionRepository
}
// NewAuthenticationService creates a new authentication service
func NewAuthenticationService(config config.ConfigProvider, logger *zap.Logger, permissionRepo repository.PermissionRepository) AuthenticationService {
jwtManager := auth.NewJWTManager(config, logger)
return &authenticationService{
config: config,
logger: logger,
jwtManager: jwtManager,
permissionRepo: permissionRepo,
}
}
// GetUserID extracts user ID from context
func (s *authenticationService) GetUserID(ctx context.Context) (string, error) {
// For now, this is a simple implementation
// In a real implementation, this would extract from JWT tokens, session, etc.
if userID, ok := ctx.Value("user_id").(string); ok {
return userID, nil
}
return "", fmt.Errorf("user ID not found in context")
}
// ValidatePermissions checks if user has required permissions
func (s *authenticationService) ValidatePermissions(ctx context.Context, userID string, appID string, requiredPermissions []string) error {
s.logger.Debug("Validating permissions",
zap.String("user_id", userID),
zap.String("app_id", appID),
zap.Strings("required_permissions", requiredPermissions))
// Implement role-based permission validation
userRoles := s.getUserRoles(userID)
// Check each required permission
for _, requiredPerm := range requiredPermissions {
hasPermission := false
// Check if user has the permission directly through role mapping
for _, role := range userRoles {
if s.roleHasPermission(role, requiredPerm) {
hasPermission = true
break
}
}
// If not found through roles, check direct permission grants
if !hasPermission {
hasPermission = s.hasDirectPermission(ctx, userID, requiredPerm)
}
if !hasPermission {
s.logger.Warn("User lacks required permission",
zap.String("user_id", userID),
zap.String("required_permission", requiredPerm),
zap.Strings("user_roles", userRoles))
return fmt.Errorf("insufficient permissions: missing '%s'", requiredPerm)
}
}
s.logger.Debug("Permission validation successful",
zap.String("user_id", userID),
zap.Strings("required_permissions", requiredPermissions),
zap.Strings("user_roles", userRoles))
return nil
}
// GetUserClaims retrieves user claims
func (s *authenticationService) GetUserClaims(ctx context.Context, userID string) (map[string]string, error) {
s.logger.Debug("Getting user claims", zap.String("user_id", userID))
// Implement actual claims retrieval
claims := make(map[string]string)
// Set basic user claims
claims["user_id"] = userID
claims["subject"] = userID
// Extract name from email if userID is an email
if strings.Contains(userID, "@") {
claims["email"] = userID
namePart := strings.Split(userID, "@")[0]
claims["preferred_username"] = namePart
// Convert underscores/dots to spaces for display name
displayName := strings.ReplaceAll(strings.ReplaceAll(namePart, "_", " "), ".", " ")
claims["name"] = displayName
} else {
claims["preferred_username"] = userID
claims["name"] = userID
}
// Add role-based claims
userRoles := s.getUserRoles(userID)
if len(userRoles) > 0 {
claims["roles"] = strings.Join(userRoles, ",")
claims["primary_role"] = userRoles[0]
}
// Add environment-specific claims
claims["provider"] = "internal"
claims["auth_method"] = "header"
claims["issued_at"] = fmt.Sprintf("%d", time.Now().Unix())
return claims, nil
}
// getUserRoles retrieves roles for a user based on patterns and rules
func (s *authenticationService) getUserRoles(userID string) []string {
var roles []string
// Role assignment based on email patterns and business rules
userLower := strings.ToLower(userID)
// Super admin roles
if strings.Contains(userLower, "admin@") || strings.Contains(userLower, "superadmin") {
roles = append(roles, "super_admin")
return roles // Super admins get all permissions
}
// Admin roles
if strings.Contains(userLower, "admin") {
roles = append(roles, "admin")
}
// Developer roles
if strings.Contains(userLower, "dev") || strings.Contains(userLower, "engineer") || strings.Contains(userLower, "tech") {
roles = append(roles, "developer")
}
// Manager roles
if strings.Contains(userLower, "manager") || strings.Contains(userLower, "lead") {
roles = append(roles, "manager")
}
// Default role for all users
if len(roles) == 0 {
roles = append(roles, "viewer")
}
return roles
}
// roleHasPermission checks if a role has a specific permission
func (s *authenticationService) roleHasPermission(role, permission string) bool {
// Define role-based permission matrix
rolePermissions := map[string][]string{
"super_admin": {
"internal.*", "app.*", "token.*", "repo.*", "permission.*", "admin.*",
},
"admin": {
"app.*", "token.*", "permission.read", "permission.list", "repo.read", "repo.write",
},
"developer": {
"app.read", "app.list", "token.create", "token.read", "token.list", "repo.*",
},
"manager": {
"app.read", "app.list", "token.read", "token.list", "repo.read", "permission.read",
},
"viewer": {
"app.read", "repo.read", "token.read",
},
}
permissions, exists := rolePermissions[role]
if !exists {
return false
}
// Check for exact match or wildcard match
for _, perm := range permissions {
if perm == permission {
return true
}
// Check wildcard permissions (e.g., "app.*" matches "app.read")
if strings.HasSuffix(perm, "*") {
prefix := strings.TrimSuffix(perm, "*")
if strings.HasPrefix(permission, prefix) {
return true
}
}
// Check hierarchical permissions (e.g., "repo" includes "repo.read")
if !strings.Contains(perm, ".") && strings.HasPrefix(permission, perm+".") {
return true
}
}
return false
}
// hasDirectPermission checks if user has direct permission grant
func (s *authenticationService) hasDirectPermission(ctx context.Context, userID, permission string) bool {
// This would typically query the database for direct user permissions
// For now, implement basic logic
// Check for system-level permissions that might be granted to specific users
if permission == "internal.system" && strings.Contains(userID, "system") {
return true
}
// In a real system, this would query the granted_permissions table
// or a user_permissions table for direct grants
return false
}
// ValidateJWTToken validates a JWT token and returns claims
func (s *authenticationService) ValidateJWTToken(ctx context.Context, tokenString string) (*domain.AuthContext, error) {
s.logger.Debug("Validating JWT token")
// Validate the token using JWT manager
claims, err := s.jwtManager.ValidateToken(tokenString)
if err != nil {
s.logger.Warn("JWT token validation failed", zap.Error(err))
return nil, err
}
// Check if token is revoked
revoked, err := s.jwtManager.IsTokenRevoked(tokenString)
if err != nil {
s.logger.Error("Failed to check token revocation status", zap.Error(err))
return nil, errors.NewInternalError("Failed to validate token").WithInternal(err)
}
if revoked {
s.logger.Warn("JWT token is revoked", zap.String("user_id", claims.UserID))
return nil, errors.NewAuthenticationError("Token has been revoked")
}
// Convert JWT claims to AuthContext
authContext := &domain.AuthContext{
UserID: claims.UserID,
TokenType: claims.TokenType,
Permissions: claims.Permissions,
Claims: claims.Claims,
AppID: claims.AppID,
}
s.logger.Debug("JWT token validated successfully",
zap.String("user_id", claims.UserID),
zap.String("app_id", claims.AppID))
return authContext, nil
}
// GenerateJWTToken generates a new JWT token for a user
func (s *authenticationService) GenerateJWTToken(ctx context.Context, userToken *domain.UserToken) (string, error) {
s.logger.Debug("Generating JWT token",
zap.String("user_id", userToken.UserID),
zap.String("app_id", userToken.AppID))
// Generate the token using JWT manager
tokenString, err := s.jwtManager.GenerateToken(userToken)
if err != nil {
s.logger.Error("Failed to generate JWT token", zap.Error(err))
return "", err
}
s.logger.Debug("JWT token generated successfully",
zap.String("user_id", userToken.UserID),
zap.String("app_id", userToken.AppID))
return tokenString, nil
}
// RefreshJWTToken refreshes an existing JWT token
func (s *authenticationService) RefreshJWTToken(ctx context.Context, tokenString string, newExpiration time.Time) (string, error) {
s.logger.Debug("Refreshing JWT token")
// Refresh the token using JWT manager
newTokenString, err := s.jwtManager.RefreshToken(tokenString, newExpiration)
if err != nil {
s.logger.Error("Failed to refresh JWT token", zap.Error(err))
return "", err
}
s.logger.Debug("JWT token refreshed successfully")
return newTokenString, nil
}

View File

@ -0,0 +1,120 @@
package services
import (
"context"
"time"
"github.com/google/uuid"
"github.com/kms/api-key-service/internal/domain"
)
// ApplicationService defines the interface for application business logic
type ApplicationService interface {
// Create creates a new application
Create(ctx context.Context, req *domain.CreateApplicationRequest, userID string) (*domain.Application, error)
// GetByID retrieves an application by its ID
GetByID(ctx context.Context, appID string) (*domain.Application, error)
// List retrieves applications with pagination
List(ctx context.Context, limit, offset int) ([]*domain.Application, error)
// Update updates an existing application
Update(ctx context.Context, appID string, updates *domain.UpdateApplicationRequest, userID string) (*domain.Application, error)
// Delete deletes an application
Delete(ctx context.Context, appID string, userID string) error
}
// TokenService defines the interface for token business logic
type TokenService interface {
// CreateStaticToken creates a new static token
CreateStaticToken(ctx context.Context, req *domain.CreateStaticTokenRequest, userID string) (*domain.CreateStaticTokenResponse, error)
// ListByApp lists all tokens for an application
ListByApp(ctx context.Context, appID string, limit, offset int) ([]*domain.StaticToken, error)
// Delete deletes a token
Delete(ctx context.Context, tokenID uuid.UUID, userID string) error
// GenerateUserToken generates a user token
GenerateUserToken(ctx context.Context, appID, userID string, permissions []string) (string, error)
// VerifyToken verifies a token and returns verification response
VerifyToken(ctx context.Context, req *domain.VerifyRequest) (*domain.VerifyResponse, error)
// RenewUserToken renews a user token
RenewUserToken(ctx context.Context, req *domain.RenewRequest) (*domain.RenewResponse, error)
}
// AuthenticationService defines the interface for authentication business logic
type AuthenticationService interface {
// GetUserID extracts user ID from context
GetUserID(ctx context.Context) (string, error)
// ValidatePermissions checks if user has required permissions
ValidatePermissions(ctx context.Context, userID string, appID string, requiredPermissions []string) error
// GetUserClaims retrieves user claims
GetUserClaims(ctx context.Context, userID string) (map[string]string, error)
// ValidateJWTToken validates a JWT token and returns claims
ValidateJWTToken(ctx context.Context, tokenString string) (*domain.AuthContext, error)
// GenerateJWTToken generates a new JWT token for a user
GenerateJWTToken(ctx context.Context, userToken *domain.UserToken) (string, error)
// RefreshJWTToken refreshes an existing JWT token
RefreshJWTToken(ctx context.Context, tokenString string, newExpiration time.Time) (string, error)
}
// SessionService defines the interface for session management business logic
type SessionService interface {
// CreateSession creates a new user session
CreateSession(ctx context.Context, req *domain.CreateSessionRequest) (*domain.UserSession, error)
// GetSession retrieves a session by its ID
GetSession(ctx context.Context, sessionID uuid.UUID) (*domain.UserSession, error)
// GetUserSessions retrieves all sessions for a user
GetUserSessions(ctx context.Context, userID string) ([]*domain.UserSession, error)
// GetUserAppSessions retrieves sessions for a specific user and application
GetUserAppSessions(ctx context.Context, userID, appID string) ([]*domain.UserSession, error)
// GetActiveSessions retrieves all active sessions for a user
GetActiveSessions(ctx context.Context, userID string) ([]*domain.UserSession, error)
// ListSessions retrieves sessions with filtering and pagination
ListSessions(ctx context.Context, req *domain.SessionListRequest) (*domain.SessionListResponse, error)
// UpdateSession updates an existing session
UpdateSession(ctx context.Context, sessionID uuid.UUID, updates *domain.UpdateSessionRequest) error
// UpdateSessionActivity updates the last activity timestamp for a session
UpdateSessionActivity(ctx context.Context, sessionID uuid.UUID) error
// RevokeSession revokes a specific session
RevokeSession(ctx context.Context, sessionID uuid.UUID, revokedBy string) error
// RevokeUserSessions revokes all sessions for a user
RevokeUserSessions(ctx context.Context, userID string, revokedBy string) error
// RevokeUserAppSessions revokes all sessions for a user and application
RevokeUserAppSessions(ctx context.Context, userID, appID string, revokedBy string) error
// ValidateSession validates if a session is active and valid
ValidateSession(ctx context.Context, sessionID uuid.UUID) (*domain.UserSession, error)
// RefreshSession refreshes a session's expiration time
RefreshSession(ctx context.Context, sessionID uuid.UUID, newExpiration time.Time) error
// CleanupExpiredSessions marks expired sessions as expired and optionally deletes old ones
CleanupExpiredSessions(ctx context.Context, deleteOlderThan *time.Duration) (expired int, deleted int, err error)
// GetSessionStats returns session statistics for a user
GetSessionStats(ctx context.Context, userID string) (total int, active int, err error)
// CreateOAuth2Session creates a session from OAuth2 authentication flow
CreateOAuth2Session(ctx context.Context, userID, appID string, tokenResponse *domain.TokenResponse, userInfo *domain.UserInfo, sessionType domain.SessionType, ipAddress, userAgent string) (*domain.UserSession, error)
}

View File

@ -0,0 +1,414 @@
package services
import (
"context"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"github.com/kms/api-key-service/internal/config"
"github.com/kms/api-key-service/internal/domain"
"github.com/kms/api-key-service/internal/errors"
"github.com/kms/api-key-service/internal/repository"
)
// sessionService implements the SessionService interface
type sessionService struct {
sessionRepo repository.SessionRepository
appRepo repository.ApplicationRepository
config config.ConfigProvider
logger *zap.Logger
}
// NewSessionService creates a new session service
func NewSessionService(
sessionRepo repository.SessionRepository,
appRepo repository.ApplicationRepository,
config config.ConfigProvider,
logger *zap.Logger,
) SessionService {
return &sessionService{
sessionRepo: sessionRepo,
appRepo: appRepo,
config: config,
logger: logger,
}
}
// CreateSession creates a new user session
func (s *sessionService) CreateSession(ctx context.Context, req *domain.CreateSessionRequest) (*domain.UserSession, error) {
s.logger.Debug("Creating new session",
zap.String("user_id", req.UserID),
zap.String("app_id", req.AppID),
zap.String("session_type", string(req.SessionType)))
// Validate application exists
app, err := s.appRepo.GetByID(ctx, req.AppID)
if err != nil {
if errors.IsNotFound(err) {
return nil, errors.NewValidationError("Application not found")
}
return nil, err
}
// Check if application supports user tokens
supportsUser := false
for _, appType := range app.Type {
if appType == domain.ApplicationTypeUser {
supportsUser = true
break
}
}
if !supportsUser {
return nil, errors.NewValidationError("Application does not support user sessions")
}
// Create session object
session := &domain.UserSession{
ID: uuid.New(),
UserID: req.UserID,
AppID: req.AppID,
SessionType: req.SessionType,
Status: domain.SessionStatusActive,
IPAddress: req.IPAddress,
UserAgent: req.UserAgent,
ExpiresAt: req.ExpiresAt,
Metadata: domain.SessionMetadata{
TenantID: req.TenantID,
Permissions: req.Permissions,
Claims: req.Claims,
LoginMethod: "oauth2",
},
}
// Create session in repository
if err := s.sessionRepo.Create(ctx, session); err != nil {
s.logger.Error("Failed to create session", zap.Error(err))
return nil, err
}
s.logger.Debug("Session created successfully", zap.String("session_id", session.ID.String()))
return session, nil
}
// GetSession retrieves a session by its ID
func (s *sessionService) GetSession(ctx context.Context, sessionID uuid.UUID) (*domain.UserSession, error) {
s.logger.Debug("Getting session", zap.String("session_id", sessionID.String()))
session, err := s.sessionRepo.GetByID(ctx, sessionID)
if err != nil {
return nil, err
}
return session, nil
}
// GetUserSessions retrieves all sessions for a user
func (s *sessionService) GetUserSessions(ctx context.Context, userID string) ([]*domain.UserSession, error) {
s.logger.Debug("Getting user sessions", zap.String("user_id", userID))
sessions, err := s.sessionRepo.GetByUserID(ctx, userID)
if err != nil {
return nil, err
}
return sessions, nil
}
// GetUserAppSessions retrieves sessions for a specific user and application
func (s *sessionService) GetUserAppSessions(ctx context.Context, userID, appID string) ([]*domain.UserSession, error) {
s.logger.Debug("Getting user app sessions",
zap.String("user_id", userID),
zap.String("app_id", appID))
sessions, err := s.sessionRepo.GetByUserAndApp(ctx, userID, appID)
if err != nil {
return nil, err
}
return sessions, nil
}
// GetActiveSessions retrieves all active sessions for a user
func (s *sessionService) GetActiveSessions(ctx context.Context, userID string) ([]*domain.UserSession, error) {
s.logger.Debug("Getting active sessions", zap.String("user_id", userID))
sessions, err := s.sessionRepo.GetActiveByUserID(ctx, userID)
if err != nil {
return nil, err
}
return sessions, nil
}
// ListSessions retrieves sessions with filtering and pagination
func (s *sessionService) ListSessions(ctx context.Context, req *domain.SessionListRequest) (*domain.SessionListResponse, error) {
s.logger.Debug("Listing sessions",
zap.String("user_id", req.UserID),
zap.String("app_id", req.AppID),
zap.Int("limit", req.Limit),
zap.Int("offset", req.Offset))
// Set default pagination if not provided
if req.Limit <= 0 {
req.Limit = 50
}
if req.Limit > 100 {
req.Limit = 100
}
response, err := s.sessionRepo.List(ctx, req)
if err != nil {
return nil, err
}
return response, nil
}
// UpdateSession updates an existing session
func (s *sessionService) UpdateSession(ctx context.Context, sessionID uuid.UUID, updates *domain.UpdateSessionRequest) error {
s.logger.Debug("Updating session", zap.String("session_id", sessionID.String()))
// Validate session exists
_, err := s.sessionRepo.GetByID(ctx, sessionID)
if err != nil {
return err
}
// Update session
if err := s.sessionRepo.Update(ctx, sessionID, updates); err != nil {
return err
}
s.logger.Debug("Session updated successfully", zap.String("session_id", sessionID.String()))
return nil
}
// UpdateSessionActivity updates the last activity timestamp for a session
func (s *sessionService) UpdateSessionActivity(ctx context.Context, sessionID uuid.UUID) error {
s.logger.Debug("Updating session activity", zap.String("session_id", sessionID.String()))
if err := s.sessionRepo.UpdateActivity(ctx, sessionID); err != nil {
return err
}
return nil
}
// RevokeSession revokes a specific session
func (s *sessionService) RevokeSession(ctx context.Context, sessionID uuid.UUID, revokedBy string) error {
s.logger.Debug("Revoking session",
zap.String("session_id", sessionID.String()),
zap.String("revoked_by", revokedBy))
// Validate session exists and is active
session, err := s.sessionRepo.GetByID(ctx, sessionID)
if err != nil {
return err
}
if session.Status != domain.SessionStatusActive {
return errors.NewValidationError("Session is not active")
}
// Revoke session
if err := s.sessionRepo.Revoke(ctx, sessionID, revokedBy); err != nil {
return err
}
s.logger.Debug("Session revoked successfully", zap.String("session_id", sessionID.String()))
return nil
}
// RevokeUserSessions revokes all sessions for a user
func (s *sessionService) RevokeUserSessions(ctx context.Context, userID string, revokedBy string) error {
s.logger.Debug("Revoking user sessions",
zap.String("user_id", userID),
zap.String("revoked_by", revokedBy))
if err := s.sessionRepo.RevokeAllByUser(ctx, userID, revokedBy); err != nil {
return err
}
s.logger.Debug("User sessions revoked successfully", zap.String("user_id", userID))
return nil
}
// RevokeUserAppSessions revokes all sessions for a user and application
func (s *sessionService) RevokeUserAppSessions(ctx context.Context, userID, appID string, revokedBy string) error {
s.logger.Debug("Revoking user app sessions",
zap.String("user_id", userID),
zap.String("app_id", appID),
zap.String("revoked_by", revokedBy))
if err := s.sessionRepo.RevokeAllByUserAndApp(ctx, userID, appID, revokedBy); err != nil {
return err
}
s.logger.Debug("User app sessions revoked successfully",
zap.String("user_id", userID),
zap.String("app_id", appID))
return nil
}
// ValidateSession validates if a session is active and valid
func (s *sessionService) ValidateSession(ctx context.Context, sessionID uuid.UUID) (*domain.UserSession, error) {
s.logger.Debug("Validating session", zap.String("session_id", sessionID.String()))
session, err := s.sessionRepo.GetByID(ctx, sessionID)
if err != nil {
return nil, err
}
// Check if session is active
if !session.IsActive() {
if session.IsExpired() {
return nil, errors.NewAuthenticationError("Session has expired")
}
if session.IsRevoked() {
return nil, errors.NewAuthenticationError("Session has been revoked")
}
return nil, errors.NewAuthenticationError("Session is not active")
}
// Update last activity
if err := s.sessionRepo.UpdateActivity(ctx, sessionID); err != nil {
s.logger.Warn("Failed to update session activity", zap.Error(err))
// Don't fail validation if we can't update activity
}
s.logger.Debug("Session validated successfully", zap.String("session_id", sessionID.String()))
return session, nil
}
// RefreshSession refreshes a session's expiration time
func (s *sessionService) RefreshSession(ctx context.Context, sessionID uuid.UUID, newExpiration time.Time) error {
s.logger.Debug("Refreshing session",
zap.String("session_id", sessionID.String()),
zap.Time("new_expiration", newExpiration))
// Validate session exists and is active
session, err := s.sessionRepo.GetByID(ctx, sessionID)
if err != nil {
return err
}
if !session.IsActive() {
return errors.NewValidationError("Cannot refresh inactive session")
}
// Update expiration
updates := &domain.UpdateSessionRequest{
ExpiresAt: &newExpiration,
}
if err := s.sessionRepo.Update(ctx, sessionID, updates); err != nil {
return err
}
s.logger.Debug("Session refreshed successfully", zap.String("session_id", sessionID.String()))
return nil
}
// CleanupExpiredSessions marks expired sessions as expired and optionally deletes old ones
func (s *sessionService) CleanupExpiredSessions(ctx context.Context, deleteOlderThan *time.Duration) (expired int, deleted int, err error) {
s.logger.Debug("Cleaning up expired sessions")
// Mark expired sessions
expired, err = s.sessionRepo.ExpireOldSessions(ctx)
if err != nil {
s.logger.Error("Failed to expire old sessions", zap.Error(err))
return 0, 0, err
}
// Delete old expired sessions if requested
if deleteOlderThan != nil {
deleted, err = s.sessionRepo.DeleteExpiredSessions(ctx, *deleteOlderThan)
if err != nil {
s.logger.Error("Failed to delete expired sessions", zap.Error(err))
return expired, 0, err
}
}
s.logger.Debug("Session cleanup completed",
zap.Int("expired", expired),
zap.Int("deleted", deleted))
return expired, deleted, nil
}
// GetSessionStats returns session statistics for a user
func (s *sessionService) GetSessionStats(ctx context.Context, userID string) (total int, active int, err error) {
s.logger.Debug("Getting session stats", zap.String("user_id", userID))
total, err = s.sessionRepo.GetSessionCount(ctx, userID)
if err != nil {
return 0, 0, err
}
active, err = s.sessionRepo.GetActiveSessionCount(ctx, userID)
if err != nil {
return 0, 0, err
}
return total, active, nil
}
// CreateOAuth2Session creates a session from OAuth2 authentication flow
func (s *sessionService) CreateOAuth2Session(ctx context.Context, userID, appID string, tokenResponse *domain.TokenResponse, userInfo *domain.UserInfo, sessionType domain.SessionType, ipAddress, userAgent string) (*domain.UserSession, error) {
s.logger.Debug("Creating OAuth2 session",
zap.String("user_id", userID),
zap.String("app_id", appID),
zap.String("session_type", string(sessionType)))
// Validate application exists
app, err := s.appRepo.GetByID(ctx, appID)
if err != nil {
if errors.IsNotFound(err) {
return nil, errors.NewValidationError("Application not found")
}
return nil, err
}
// Calculate expiration based on token response
expiresAt := time.Now().Add(time.Duration(tokenResponse.ExpiresIn) * time.Second)
// Use application's max token duration if shorter
maxExpiration := time.Now().Add(app.MaxTokenDuration.Duration)
if expiresAt.After(maxExpiration) {
expiresAt = maxExpiration
}
// Create session object
session := &domain.UserSession{
ID: uuid.New(),
UserID: userID,
AppID: appID,
SessionType: sessionType,
Status: domain.SessionStatusActive,
AccessToken: tokenResponse.AccessToken, // In production, encrypt this
RefreshToken: tokenResponse.RefreshToken, // In production, encrypt this
IDToken: tokenResponse.IDToken, // In production, encrypt this
IPAddress: ipAddress,
UserAgent: userAgent,
ExpiresAt: expiresAt,
Metadata: domain.SessionMetadata{
LoginMethod: "oauth2",
Claims: map[string]string{
"sub": userInfo.Sub,
"email": userInfo.Email,
"name": userInfo.Name,
},
},
}
// Create session in repository
if err := s.sessionRepo.Create(ctx, session); err != nil {
s.logger.Error("Failed to create OAuth2 session", zap.Error(err))
return nil, err
}
s.logger.Debug("OAuth2 session created successfully", zap.String("session_id", session.ID.String()))
return session, nil
}

View File

@ -0,0 +1,647 @@
package services
import (
"context"
"fmt"
"strings"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"github.com/kms/api-key-service/internal/auth"
"github.com/kms/api-key-service/internal/config"
"github.com/kms/api-key-service/internal/crypto"
"github.com/kms/api-key-service/internal/domain"
"github.com/kms/api-key-service/internal/repository"
)
// tokenService implements the TokenService interface
type tokenService struct {
tokenRepo repository.StaticTokenRepository
appRepo repository.ApplicationRepository
permRepo repository.PermissionRepository
grantRepo repository.GrantedPermissionRepository
tokenGen *crypto.TokenGenerator
jwtManager *auth.JWTManager
logger *zap.Logger
}
// NewTokenService creates a new token service
func NewTokenService(
tokenRepo repository.StaticTokenRepository,
appRepo repository.ApplicationRepository,
permRepo repository.PermissionRepository,
grantRepo repository.GrantedPermissionRepository,
hmacKey string,
config config.ConfigProvider,
logger *zap.Logger,
) TokenService {
return &tokenService{
tokenRepo: tokenRepo,
appRepo: appRepo,
permRepo: permRepo,
grantRepo: grantRepo,
tokenGen: crypto.NewTokenGenerator(hmacKey),
jwtManager: auth.NewJWTManager(config, logger),
logger: logger,
}
}
// CreateStaticToken creates a new static token
func (s *tokenService) CreateStaticToken(ctx context.Context, req *domain.CreateStaticTokenRequest, userID string) (*domain.CreateStaticTokenResponse, error) {
s.logger.Info("Creating static token", zap.String("app_id", req.AppID), zap.String("user_id", userID))
// Validate application exists
app, err := s.appRepo.GetByID(ctx, req.AppID)
if err != nil {
s.logger.Error("Failed to get application", zap.Error(err), zap.String("app_id", req.AppID))
return nil, fmt.Errorf("application not found: %w", err)
}
// Validate permissions exist
validPermissions, err := s.permRepo.ValidatePermissionScopes(ctx, req.Permissions)
if err != nil {
s.logger.Error("Failed to validate permissions", zap.Error(err))
return nil, fmt.Errorf("failed to validate permissions: %w", err)
}
if len(validPermissions) != len(req.Permissions) {
s.logger.Warn("Some permissions are invalid",
zap.Strings("requested", req.Permissions),
zap.Strings("valid", validPermissions))
return nil, fmt.Errorf("some requested permissions are invalid")
}
// Generate secure token with custom prefix
tokenInfo, err := s.tokenGen.GenerateTokenWithInfoAndPrefix(app.TokenPrefix, "static")
if err != nil {
s.logger.Error("Failed to generate secure token", zap.Error(err))
return nil, fmt.Errorf("failed to generate token: %w", err)
}
tokenID := uuid.New()
now := time.Now()
// Create the token entity
token := &domain.StaticToken{
ID: tokenID,
AppID: req.AppID,
Owner: req.Owner,
KeyHash: tokenInfo.Hash,
Type: "hmac",
CreatedAt: now,
UpdatedAt: now,
}
// Save the token to the database
err = s.tokenRepo.Create(ctx, token)
if err != nil {
s.logger.Error("Failed to create token in database", zap.Error(err), zap.String("token_id", tokenID.String()))
return nil, fmt.Errorf("failed to create token: %w", err)
}
// Grant permissions to the token
var grants []*domain.GrantedPermission
for _, permScope := range validPermissions {
// Get permission by scope to get the ID
perm, err := s.permRepo.GetAvailablePermissionByScope(ctx, permScope)
if err != nil {
s.logger.Error("Failed to get permission by scope", zap.Error(err), zap.String("scope", permScope))
continue
}
grant := &domain.GrantedPermission{
ID: uuid.New(),
TokenType: domain.TokenTypeStatic,
TokenID: tokenID,
PermissionID: perm.ID,
Scope: permScope,
CreatedBy: userID,
}
grants = append(grants, grant)
}
if len(grants) > 0 {
err = s.grantRepo.GrantPermissions(ctx, grants)
if err != nil {
s.logger.Error("Failed to grant permissions", zap.Error(err))
// Clean up the token if permission granting fails
s.tokenRepo.Delete(ctx, tokenID)
return nil, fmt.Errorf("failed to grant permissions: %w", err)
}
}
response := &domain.CreateStaticTokenResponse{
ID: tokenID,
Token: tokenInfo.Token, // Return the actual token only once
Permissions: validPermissions,
CreatedAt: now,
}
s.logger.Info("Static token created successfully",
zap.String("token_id", tokenID.String()),
zap.String("app_id", app.AppID),
zap.Strings("permissions", validPermissions))
return response, nil
}
// ListByApp lists all tokens for an application
func (s *tokenService) ListByApp(ctx context.Context, appID string, limit, offset int) ([]*domain.StaticToken, error) {
s.logger.Debug("Listing tokens for application", zap.String("app_id", appID))
tokens, err := s.tokenRepo.GetByAppID(ctx, appID)
if err != nil {
s.logger.Error("Failed to list tokens from repository", zap.Error(err), zap.String("app_id", appID))
return nil, fmt.Errorf("failed to list tokens: %w", err)
}
// Apply pagination manually since GetByAppID doesn't support it
start := offset
end := offset + limit
if start > len(tokens) {
tokens = []*domain.StaticToken{}
} else if end > len(tokens) {
tokens = tokens[start:]
} else {
tokens = tokens[start:end]
}
s.logger.Debug("Listed tokens successfully", zap.String("app_id", appID), zap.Int("count", len(tokens)))
return tokens, nil
}
// Delete deletes a token
func (s *tokenService) Delete(ctx context.Context, tokenID uuid.UUID, userID string) error {
s.logger.Info("Deleting token", zap.String("token_id", tokenID.String()), zap.String("user_id", userID))
// Check if token exists
exists, err := s.tokenRepo.Exists(ctx, tokenID)
if err != nil {
s.logger.Error("Failed to check token existence", zap.Error(err), zap.String("token_id", tokenID.String()))
return err
}
if !exists {
s.logger.Error("Token not found", zap.String("token_id", tokenID.String()))
return fmt.Errorf("token with ID '%s' not found", tokenID.String())
}
// Delete the token
err = s.tokenRepo.Delete(ctx, tokenID)
if err != nil {
s.logger.Error("Failed to delete token", zap.Error(err), zap.String("token_id", tokenID.String()))
return err
}
// Revoke associated permissions when deleting a static token
err = s.grantRepo.RevokeAllPermissions(ctx, domain.TokenTypeStatic, tokenID, "system-cleanup")
if err != nil {
s.logger.Warn("Failed to revoke permissions for deleted token",
zap.String("token_id", tokenID.String()),
zap.Error(err))
// Don't fail the deletion if permission revocation fails
}
return nil
}
// GenerateUserToken generates a user token
func (s *tokenService) GenerateUserToken(ctx context.Context, appID, userID string, permissions []string) (string, error) {
s.logger.Info("Generating user token", zap.String("app_id", appID), zap.String("user_id", userID))
// Validate application exists
app, err := s.appRepo.GetByID(ctx, appID)
if err != nil {
s.logger.Error("Failed to get application", zap.Error(err), zap.String("app_id", appID))
return "", fmt.Errorf("application not found: %w", err)
}
// Validate permissions exist (if any provided)
var validPermissions []string
if len(permissions) > 0 {
validPermissions, err = s.permRepo.ValidatePermissionScopes(ctx, permissions)
if err != nil {
s.logger.Error("Failed to validate permissions", zap.Error(err))
return "", fmt.Errorf("failed to validate permissions: %w", err)
}
if len(validPermissions) != len(permissions) {
s.logger.Warn("Some permissions are invalid",
zap.Strings("requested", permissions),
zap.Strings("valid", validPermissions))
return "", fmt.Errorf("some requested permissions are invalid")
}
}
// Create user token with proper timing
now := time.Now()
userToken := &domain.UserToken{
AppID: appID,
UserID: userID,
Permissions: validPermissions,
IssuedAt: now,
ExpiresAt: now.Add(app.TokenRenewalDuration.Duration),
MaxValidAt: now.Add(app.MaxTokenDuration.Duration),
TokenType: domain.TokenTypeUser,
}
// Generate JWT token using JWT manager
jwtTokenString, err := s.jwtManager.GenerateToken(userToken)
if err != nil {
s.logger.Error("Failed to generate JWT token", zap.Error(err))
return "", fmt.Errorf("failed to generate token: %w", err)
}
// Add custom prefix wrapper for user tokens if application has one
var finalToken string
if app.TokenPrefix != "" {
// For user JWT tokens, we wrap the JWT with custom prefix
finalToken = app.TokenPrefix + "UT-" + jwtTokenString
} else {
finalToken = jwtTokenString
}
s.logger.Info("User token generated successfully",
zap.String("app_id", appID),
zap.String("user_id", userID),
zap.Strings("permissions", validPermissions),
zap.Time("expires_at", userToken.ExpiresAt),
zap.Time("max_valid_at", userToken.MaxValidAt))
return finalToken, nil
}
// detectTokenType detects the token type based on its prefix
func (s *tokenService) detectTokenType(token string, app *domain.Application) domain.TokenType {
// Check for user token pattern first (UT- suffix)
if app.TokenPrefix != "" {
userPrefix := app.TokenPrefix + "UT-"
if strings.HasPrefix(token, userPrefix) {
return domain.TokenTypeUser
}
staticPrefix := app.TokenPrefix + "T-"
if strings.HasPrefix(token, staticPrefix) {
return domain.TokenTypeStatic
}
}
// Check for custom prefix pattern in case app prefix is not set
// Look for pattern: 2-4 uppercase letters + "UT-" or "T-"
if len(token) >= 6 {
dashIndex := strings.Index(token, "-")
if dashIndex >= 3 && dashIndex <= 6 { // 2-4 chars + "T" or "UT"
prefixPart := token[:dashIndex+1]
if strings.HasSuffix(prefixPart, "UT-") {
return domain.TokenTypeUser
}
if strings.HasSuffix(prefixPart, "T-") {
return domain.TokenTypeStatic
}
}
}
// Check for default kms_ prefix
if strings.HasPrefix(token, "kms_") {
return domain.TokenTypeStatic // Default tokens are static
}
// Default to static if pattern is unclear
return domain.TokenTypeStatic
}
// VerifyToken verifies a token and returns verification response
func (s *tokenService) VerifyToken(ctx context.Context, req *domain.VerifyRequest) (*domain.VerifyResponse, error) {
// Validate request
if req.Token == "" {
return &domain.VerifyResponse{
Valid: false,
Permitted: false,
Error: "Token is required",
}, nil
}
// Validate application exists
app, err := s.appRepo.GetByID(ctx, req.AppID)
if err != nil {
s.logger.Error("Failed to get application", zap.Error(err), zap.String("app_id", req.AppID))
return &domain.VerifyResponse{
Valid: false,
Permitted: false,
Error: "Invalid application",
}, nil
}
// Always auto-detect token type from prefix
tokenType := s.detectTokenType(req.Token, app)
s.logger.Debug("Auto-detected token type",
zap.String("app_id", req.AppID),
zap.String("detected_type", string(tokenType)))
s.logger.Debug("Verifying token", zap.String("app_id", req.AppID), zap.String("type", string(tokenType)))
switch tokenType {
case domain.TokenTypeStatic:
return s.verifyStaticToken(ctx, req, app)
case domain.TokenTypeUser:
return s.verifyUserToken(ctx, req, app)
default:
return &domain.VerifyResponse{
Valid: false,
Permitted: false,
Error: "Invalid token type",
}, nil
}
}
// verifyStaticToken verifies a static token
func (s *tokenService) verifyStaticToken(ctx context.Context, req *domain.VerifyRequest, app *domain.Application) (*domain.VerifyResponse, error) {
s.logger.Debug("Verifying static token", zap.String("app_id", req.AppID))
// Check token format
if !crypto.IsValidTokenFormat(req.Token) {
s.logger.Warn("Invalid token format", zap.String("app_id", req.AppID))
return &domain.VerifyResponse{
Valid: false,
Permitted: false,
Error: "Invalid token format",
}, nil
}
// Try to find token by testing against all stored hashes for this app
tokens, err := s.tokenRepo.GetByAppID(ctx, req.AppID)
if err != nil {
s.logger.Error("Failed to get tokens for app", zap.Error(err), zap.String("app_id", req.AppID))
return &domain.VerifyResponse{
Valid: false,
Permitted: false,
Error: "Token verification failed",
}, nil
}
var matchedToken *domain.StaticToken
for _, token := range tokens {
if s.tokenGen.VerifyToken(req.Token, token.KeyHash) {
matchedToken = token
break
}
}
if matchedToken == nil {
s.logger.Warn("Token not found or invalid", zap.String("app_id", req.AppID))
return &domain.VerifyResponse{
Valid: false,
Permitted: false,
Error: "Invalid token",
}, nil
}
// Get granted permissions for this token
permissions, err := s.grantRepo.GetGrantedPermissionScopes(ctx, domain.TokenTypeStatic, matchedToken.ID)
if err != nil {
s.logger.Error("Failed to get token permissions", zap.Error(err), zap.String("token_id", matchedToken.ID.String()))
return &domain.VerifyResponse{
Valid: false,
Permitted: false,
Error: "Failed to retrieve permissions",
}, nil
}
// Check specific permissions if requested
var permissionResults map[string]bool
var permitted bool = true // Default to true if no specific permissions requested
if len(req.Permissions) > 0 {
permissionResults, err = s.grantRepo.HasAnyPermission(ctx, domain.TokenTypeStatic, matchedToken.ID, req.Permissions)
if err != nil {
s.logger.Error("Failed to check specific permissions", zap.Error(err))
return &domain.VerifyResponse{
Valid: false,
Permitted: false,
Error: "Failed to check permissions",
}, nil
}
// Check if all requested permissions are granted
for _, requestedPerm := range req.Permissions {
if hasPermission, exists := permissionResults[requestedPerm]; !exists || !hasPermission {
permitted = false
break
}
}
}
s.logger.Info("Static token verified successfully",
zap.String("token_id", matchedToken.ID.String()),
zap.String("app_id", req.AppID),
zap.Strings("permissions", permissions),
zap.Bool("permitted", permitted))
return &domain.VerifyResponse{
Valid: true,
Permitted: permitted,
Permissions: permissions,
PermissionResults: permissionResults,
TokenType: domain.TokenTypeStatic,
}, nil
}
// verifyUserToken verifies a user token (JWT-based)
func (s *tokenService) verifyUserToken(ctx context.Context, req *domain.VerifyRequest, app *domain.Application) (*domain.VerifyResponse, error) {
s.logger.Debug("Verifying user token", zap.String("app_id", req.AppID))
// Extract JWT token from potentially prefixed format
jwtToken := req.Token
if app.TokenPrefix != "" {
expectedPrefix := app.TokenPrefix + "UT-"
if strings.HasPrefix(req.Token, expectedPrefix) {
jwtToken = strings.TrimPrefix(req.Token, expectedPrefix)
} else {
// Token doesn't have expected prefix
s.logger.Warn("User token missing expected prefix",
zap.String("app_id", req.AppID),
zap.String("expected_prefix", expectedPrefix))
return &domain.VerifyResponse{
Valid: false,
Permitted: false,
Error: "Invalid token format",
}, nil
}
}
// Check if token is revoked first
isRevoked, err := s.jwtManager.IsTokenRevoked(jwtToken)
if err != nil {
s.logger.Error("Failed to check token revocation status", zap.Error(err))
return &domain.VerifyResponse{
Valid: false,
Permitted: false,
Error: "Token verification failed",
}, nil
}
if isRevoked {
s.logger.Warn("Token is revoked", zap.String("app_id", req.AppID))
return &domain.VerifyResponse{
Valid: false,
Permitted: false,
Error: "Token has been revoked",
}, nil
}
// Validate JWT token
claims, err := s.jwtManager.ValidateToken(jwtToken)
if err != nil {
s.logger.Warn("JWT token validation failed", zap.Error(err), zap.String("app_id", req.AppID))
return &domain.VerifyResponse{
Valid: false,
Permitted: false,
Error: "Invalid token",
}, nil
}
// Verify the token is for the correct application
if claims.AppID != req.AppID {
s.logger.Warn("Token app_id mismatch",
zap.String("expected", req.AppID),
zap.String("actual", claims.AppID))
return &domain.VerifyResponse{
Valid: false,
Permitted: false,
Error: "Token not valid for this application",
}, nil
}
// Check specific permissions if requested
var permissionResults map[string]bool
var permitted bool = true // Default to true if no specific permissions requested
if len(req.Permissions) > 0 {
permissionResults = make(map[string]bool)
// Check each requested permission against token permissions
for _, requestedPerm := range req.Permissions {
hasPermission := false
for _, tokenPerm := range claims.Permissions {
if tokenPerm == requestedPerm {
hasPermission = true
break
}
}
permissionResults[requestedPerm] = hasPermission
// If any permission is missing, set permitted to false
if !hasPermission {
permitted = false
}
}
}
// Convert timestamps
var expiresAt, maxValidAt *time.Time
if claims.ExpiresAt != nil {
expTime := claims.ExpiresAt.Time
expiresAt = &expTime
}
if claims.MaxValidAt > 0 {
maxTime := time.Unix(claims.MaxValidAt, 0)
maxValidAt = &maxTime
}
s.logger.Info("User token verified successfully",
zap.String("user_id", claims.UserID),
zap.String("app_id", req.AppID),
zap.Strings("permissions", claims.Permissions),
zap.Bool("permitted", permitted))
return &domain.VerifyResponse{
Valid: true,
Permitted: permitted,
UserID: claims.UserID,
Permissions: claims.Permissions,
PermissionResults: permissionResults,
ExpiresAt: expiresAt,
MaxValidAt: maxValidAt,
TokenType: domain.TokenTypeUser,
Claims: claims.Claims,
}, nil
}
// RenewUserToken renews a user token
func (s *tokenService) RenewUserToken(ctx context.Context, req *domain.RenewRequest) (*domain.RenewResponse, error) {
s.logger.Info("Renewing user token", zap.String("app_id", req.AppID), zap.String("user_id", req.UserID))
// Get application to validate against and get HMAC key
app, err := s.appRepo.GetByID(ctx, req.AppID)
if err != nil {
s.logger.Error("Failed to get application for token renewal", zap.Error(err), zap.String("app_id", req.AppID))
return &domain.RenewResponse{
Error: "invalid_application",
}, nil
}
// Validate current token
currentToken, err := s.jwtManager.ValidateToken(req.Token)
if err != nil {
s.logger.Warn("Invalid token for renewal", zap.Error(err), zap.String("app_id", req.AppID), zap.String("user_id", req.UserID))
return &domain.RenewResponse{
Error: "invalid_token",
}, nil
}
// Verify token belongs to the requested user
if currentToken.UserID != req.UserID {
s.logger.Warn("Token user ID mismatch during renewal",
zap.String("expected", req.UserID),
zap.String("actual", currentToken.UserID))
return &domain.RenewResponse{
Error: "invalid_token",
}, nil
}
// Check if token is still within its maximum validity period
maxValidTime := time.Unix(currentToken.MaxValidAt, 0)
if time.Now().After(maxValidTime) {
s.logger.Warn("Token is past maximum validity period",
zap.String("user_id", req.UserID),
zap.Time("max_valid_at", maxValidTime))
return &domain.RenewResponse{
Error: "token_expired",
}, nil
}
// Generate new token with extended expiry but same max valid date and permissions
newToken := &domain.UserToken{
AppID: req.AppID,
UserID: req.UserID,
Permissions: currentToken.Permissions,
IssuedAt: time.Now(),
ExpiresAt: time.Now().Add(app.TokenRenewalDuration.Duration),
MaxValidAt: maxValidTime, // Keep original max validity
TokenType: domain.TokenTypeUser,
Claims: currentToken.Claims,
}
// Ensure the new expiry doesn't exceed max valid date
if newToken.ExpiresAt.After(newToken.MaxValidAt) {
newToken.ExpiresAt = newToken.MaxValidAt
}
// Generate the actual JWT token
tokenString, err := s.jwtManager.GenerateToken(newToken)
if err != nil {
s.logger.Error("Failed to generate renewed token", zap.Error(err), zap.String("user_id", req.UserID))
return &domain.RenewResponse{
Error: "token_generation_failed",
}, nil
}
response := &domain.RenewResponse{
Token: tokenString,
ExpiresAt: newToken.ExpiresAt,
MaxValidAt: newToken.MaxValidAt,
}
return response, nil
}