feat(auth): Complete Auth Service implementation and fix Consul health checks
- Add VerifyPassword RPC to Identity Service - Added to proto file and generated code - Implemented in Identity Service gRPC server - Added to Identity Service client interface and gRPC client - Complete RefreshToken implementation - Store refresh tokens in database using RefreshToken entity - Validate refresh tokens with expiration checking - Revoke refresh tokens on logout and token rotation - Integrate Authz Service for role retrieval - Added AuthzServiceClient to Auth Service - Get user roles during login and token refresh - Gracefully handle Authz Service failures - Require JWT secret in configuration - Removed default secret fallback - Service fails to start if JWT secret is not configured - Fix Consul health checks for Docker - Services now register with Docker service names (e.g., audit-service) - Allows Consul (in Docker) to reach services via Docker DNS - Health checks use gRPC service names instead of localhost This completes all TODOs in auth_service_fx.go and fixes the Consul health check failures in Docker environments.
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
authv1 "git.dcentral.systems/toolz/goplt/api/proto/generated/auth/v1"
|
||||
"git.dcentral.systems/toolz/goplt/internal/ent/refreshtoken"
|
||||
"git.dcentral.systems/toolz/goplt/internal/infra/database"
|
||||
"git.dcentral.systems/toolz/goplt/pkg/config"
|
||||
"git.dcentral.systems/toolz/goplt/pkg/logger"
|
||||
@@ -37,6 +38,7 @@ type authService struct {
|
||||
client *database.Client
|
||||
logger logger.Logger
|
||||
identityClient services.IdentityServiceClient
|
||||
authzClient services.AuthzServiceClient
|
||||
jwtSecret []byte
|
||||
accessTokenExpiry time.Duration
|
||||
refreshTokenExpiry time.Duration
|
||||
@@ -79,18 +81,25 @@ func (s *authService) generateAccessToken(userID, email string, roles []string)
|
||||
}
|
||||
|
||||
// generateRefreshToken generates a refresh token and stores it in the database.
|
||||
// Note: This is a simplified version - RefreshToken entity needs to be generated first
|
||||
func (s *authService) generateRefreshToken(ctx context.Context, userID string) (string, error) {
|
||||
token, err := generateRefreshToken()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// TODO: Store refresh token in database using RefreshToken entity once generated
|
||||
// For now, we'll just return the token
|
||||
// tokenHash := hashToken(token)
|
||||
// expiresAt := time.Now().Add(s.refreshTokenExpiry)
|
||||
// _, err = s.client.RefreshToken.Create()...
|
||||
tokenHash := hashToken(token)
|
||||
expiresAt := time.Now().Add(s.refreshTokenExpiry)
|
||||
|
||||
// Store refresh token in database
|
||||
_, err = s.client.RefreshToken.Create().
|
||||
SetID(fmt.Sprintf("%d-%d", time.Now().Unix(), time.Now().UnixNano()%1000000)).
|
||||
SetUserID(userID).
|
||||
SetTokenHash(tokenHash).
|
||||
SetExpiresAt(expiresAt).
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to store refresh token: %w", err)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
@@ -128,41 +137,67 @@ func (s *authService) validateAccessToken(tokenString string) (*jwt.Token, jwt.M
|
||||
}
|
||||
|
||||
// validateRefreshToken validates a refresh token.
|
||||
// Note: This is a simplified version - RefreshToken entity needs to be generated first
|
||||
func (s *authService) validateRefreshToken(ctx context.Context, tokenString string) (string, error) {
|
||||
// TODO: Use RefreshToken entity once generated
|
||||
// tokenHash := hashToken(tokenString)
|
||||
// rt, err := s.client.RefreshToken.Query()...
|
||||
// return rt.UserID, nil
|
||||
tokenHash := hashToken(tokenString)
|
||||
|
||||
// For now, return error to indicate this needs proper implementation
|
||||
return "", fmt.Errorf("refresh token validation not yet implemented - RefreshToken entity needs to be generated")
|
||||
// Find refresh token by hash
|
||||
rt, err := s.client.RefreshToken.Query().
|
||||
Where(refreshtoken.TokenHash(tokenHash)).
|
||||
Only(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid refresh token")
|
||||
}
|
||||
|
||||
// Check if token has expired
|
||||
if rt.ExpiresAt.Before(time.Now()) {
|
||||
// Delete expired token
|
||||
_ = s.client.RefreshToken.DeleteOneID(rt.ID).Exec(ctx)
|
||||
return "", fmt.Errorf("refresh token expired")
|
||||
}
|
||||
|
||||
return rt.UserID, nil
|
||||
}
|
||||
|
||||
// revokeRefreshToken revokes a refresh token.
|
||||
// Note: This is a simplified version - RefreshToken entity needs to be generated first
|
||||
func (s *authService) revokeRefreshToken(ctx context.Context, tokenString string) error {
|
||||
// TODO: Implement once RefreshToken entity is generated
|
||||
// tokenHash := hashToken(tokenString)
|
||||
// rt, err := s.client.RefreshToken.Query()...
|
||||
// return s.client.RefreshToken.DeleteOneID(rt.ID).Exec(ctx)
|
||||
return nil // Placeholder
|
||||
tokenHash := hashToken(tokenString)
|
||||
|
||||
// Find and delete refresh token
|
||||
rt, err := s.client.RefreshToken.Query().
|
||||
Where(refreshtoken.TokenHash(tokenHash)).
|
||||
Only(ctx)
|
||||
if err != nil {
|
||||
// Token not found, consider it already revoked
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.client.RefreshToken.DeleteOneID(rt.ID).Exec(ctx)
|
||||
}
|
||||
|
||||
// login authenticates a user and returns tokens.
|
||||
func (s *authService) login(ctx context.Context, email, password string) (*authv1.LoginResponse, error) {
|
||||
// Verify credentials with Identity Service
|
||||
user, err := s.identityClient.GetUserByEmail(ctx, email)
|
||||
user, err := s.identityClient.VerifyPassword(ctx, email, password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid credentials")
|
||||
}
|
||||
|
||||
// Note: In a real implementation, we'd call VerifyPassword on Identity Service
|
||||
// For now, we'll assume Identity Service validates the password
|
||||
// This is a simplified version - the Identity Service should expose VerifyPassword
|
||||
|
||||
// Get user roles (simplified - would come from Authz Service)
|
||||
roles := []string{} // TODO: Get from Authz Service
|
||||
// Get user roles from Authz Service
|
||||
roles := []string{}
|
||||
if s.authzClient != nil {
|
||||
userRoles, err := s.authzClient.GetUserRoles(ctx, user.ID)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to get user roles",
|
||||
zap.String("user_id", user.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
// Continue without roles rather than failing login
|
||||
} else {
|
||||
for _, role := range userRoles {
|
||||
roles = append(roles, role.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
accessToken, expiresIn, err := s.generateAccessToken(user.ID, user.Email, roles)
|
||||
@@ -197,8 +232,22 @@ func (s *authService) refreshToken(ctx context.Context, refreshTokenString strin
|
||||
return nil, fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
// Get user roles (simplified)
|
||||
roles := []string{} // TODO: Get from Authz Service
|
||||
// Get user roles from Authz Service
|
||||
roles := []string{}
|
||||
if s.authzClient != nil {
|
||||
userRoles, err := s.authzClient.GetUserRoles(ctx, user.ID)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to get user roles",
|
||||
zap.String("user_id", user.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
// Continue without roles rather than failing refresh
|
||||
} else {
|
||||
for _, role := range userRoles {
|
||||
roles = append(roles, role.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate new tokens
|
||||
accessToken, expiresIn, err := s.generateAccessToken(user.ID, user.Email, roles)
|
||||
@@ -306,17 +355,19 @@ func provideAuthService() fx.Option {
|
||||
client *database.Client,
|
||||
log logger.Logger,
|
||||
identityClient services.IdentityServiceClient,
|
||||
authzClient services.AuthzServiceClient,
|
||||
cfg config.ConfigProvider,
|
||||
) (*authService, error) {
|
||||
jwtSecret := cfg.GetString("auth.jwt_secret")
|
||||
if jwtSecret == "" {
|
||||
jwtSecret = "default-secret-change-in-production" // TODO: Generate or require
|
||||
return nil, fmt.Errorf("auth.jwt_secret is required in configuration")
|
||||
}
|
||||
|
||||
return &authService{
|
||||
client: client,
|
||||
logger: log,
|
||||
identityClient: identityClient,
|
||||
authzClient: authzClient,
|
||||
jwtSecret: []byte(jwtSecret),
|
||||
accessTokenExpiry: accessTokenLifetime,
|
||||
refreshTokenExpiry: refreshTokenLifetime,
|
||||
|
||||
Reference in New Issue
Block a user