feat: standardize field names and add flexible role_id handling for JWT compatibility
- Rename user_id → users_id across all models, handlers, services, and tests
- Add custom RoleIDs type supporting string/int/array unmarshaling (e.g., "1", 1, [1])
- Implement flexible JSON unmarshaling for JWT Claims to handle field name variants
- Support both user_id/users_id and email/email_address field names
- Enable role_id as string ("1"), int (1), or array ([1,2])
- Update AuthorizationContext to handle role_id type flexibility
- Add comprehensive logging to repository, service, and handler layers
- Entry/exit logs with full context
- Success (✓) and failure (✗) indicators
- Step-by-step authorization flow tracking
- Add containsRole helper for multi-role membership checks
- Fix database queries: user_id → users_id, id → permissions_id
- Update all tests to use models.RoleIDs{} syntax
- Change GetRole middleware return type: string → []int
- Maintain backward compatibility with legacy JWT tokens
This change improves integration with external services (MIS) that may send
role_id in different formats and standardizes field naming conventions
throughout the authorization microservice.
This commit is contained in:
+8
-7
@@ -21,7 +21,7 @@ import (
|
||||
|
||||
const (
|
||||
claimsKey models.ContextKey = "claims"
|
||||
userIDKey models.ContextKey = "user_id"
|
||||
userIDKey models.ContextKey = "users_id"
|
||||
roleIDKey models.ContextKey = "role_id"
|
||||
)
|
||||
|
||||
@@ -168,7 +168,7 @@ func parseAndValidateToken(tokenString string) (*models.Claims, error) {
|
||||
return nil, fmt.Errorf("invalid claims")
|
||||
}
|
||||
|
||||
log.Printf("Token verified successfully for user: (UserID: %s)", claims.UserID)
|
||||
log.Printf("Token verified successfully for user: (UserID: %s)", claims.UsersID)
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
@@ -239,8 +239,9 @@ func JWTAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||
// buildContext efficiently builds context with claims (reduces allocations)
|
||||
func buildContext(parent context.Context, claims *models.Claims) context.Context {
|
||||
ctx := context.WithValue(parent, claimsKey, claims)
|
||||
ctx = context.WithValue(ctx, userIDKey, claims.UserID)
|
||||
ctx = context.WithValue(ctx, roleIDKey, claims.RoleID)
|
||||
ctx = context.WithValue(ctx, userIDKey, claims.UsersID)
|
||||
// Store plain []int in context for roles to keep middleware interfaces simple
|
||||
ctx = context.WithValue(ctx, roleIDKey, []int(claims.RoleID))
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -256,8 +257,8 @@ func GetUserID(r *http.Request) (string, bool) {
|
||||
return userID, ok
|
||||
}
|
||||
|
||||
// GetRole retrieves the role from the request context
|
||||
func GetRole(r *http.Request) (string, bool) {
|
||||
role, ok := r.Context().Value(roleIDKey).(string)
|
||||
// GetRole retrieves the roles from the request context
|
||||
func GetRole(r *http.Request) ([]int, bool) {
|
||||
role, ok := r.Context().Value(roleIDKey).([]int)
|
||||
return role, ok
|
||||
}
|
||||
|
||||
+27
-25
@@ -167,15 +167,15 @@ func TestParseAndValidateToken(t *testing.T) {
|
||||
|
||||
func TestBuildContext(t *testing.T) {
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: "admin",
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{3},
|
||||
}
|
||||
|
||||
parent := context.Background()
|
||||
ctx := buildContext(parent, claims)
|
||||
|
||||
// Check claims
|
||||
if val, ok := ctx.Value(claimsKey).(*models.Claims); !ok || val.UserID != "user123" {
|
||||
if val, ok := ctx.Value(claimsKey).(*models.Claims); !ok || val.UsersID != "user123" {
|
||||
t.Error("Claims not properly set in context")
|
||||
}
|
||||
|
||||
@@ -185,15 +185,15 @@ func TestBuildContext(t *testing.T) {
|
||||
}
|
||||
|
||||
// Check role
|
||||
if val, ok := ctx.Value(roleIDKey).(string); !ok || val != "admin" {
|
||||
if val, ok := ctx.Value(roleIDKey).([]int); !ok || len(val) == 0 || val[0] != 3 {
|
||||
t.Error("Role not properly set in context")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetClaims(t *testing.T) {
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: "admin",
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{3},
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
@@ -204,8 +204,8 @@ func TestGetClaims(t *testing.T) {
|
||||
if !ok {
|
||||
t.Error("Expected claims to be found")
|
||||
}
|
||||
if retrievedClaims.UserID != "user123" {
|
||||
t.Errorf("Expected UserID 'user123', got '%s'", retrievedClaims.UserID)
|
||||
if retrievedClaims.UsersID != "user123" {
|
||||
t.Errorf("Expected UserID 'user123', got '%s'", retrievedClaims.UsersID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,15 +225,17 @@ func TestGetUserID(t *testing.T) {
|
||||
|
||||
func TestGetRole(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
ctx := context.WithValue(req.Context(), roleIDKey, "admin")
|
||||
ctx := context.WithValue(req.Context(), roleIDKey, []int{3})
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
role, ok := GetRole(req)
|
||||
if !ok {
|
||||
t.Error("Expected role to be found")
|
||||
}
|
||||
if role != "admin" {
|
||||
t.Errorf("Expected 'admin', got '%s'", role)
|
||||
if len(role) == 0 {
|
||||
t.Errorf("Expected at least one role, got '%v'", role)
|
||||
} else if role[0] != 3 {
|
||||
t.Errorf("Expected first role to be 3, got '%v'", role[0])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,13 +337,13 @@ func TestParseAndValidateTokenMalformedTokens(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBuildContextWithDifferentRoles(t *testing.T) {
|
||||
roles := []string{"admin", "user", "guest", "superadmin", "", "role-with-dash"}
|
||||
roles := []int{3, 4, 5, 6, 7, 8}
|
||||
|
||||
for _, role := range roles {
|
||||
t.Run("Role: "+role, func(t *testing.T) {
|
||||
t.Run("Role: "+string(rune(role)), func(t *testing.T) {
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: role,
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{role},
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
@@ -352,8 +354,8 @@ func TestBuildContextWithDifferentRoles(t *testing.T) {
|
||||
if !ok {
|
||||
t.Error("Claims not found in context")
|
||||
}
|
||||
if retrievedClaims.RoleID != role {
|
||||
t.Errorf("Role = %q, want %q", retrievedClaims.RoleID, role)
|
||||
if len(retrievedClaims.RoleID) == 0 || retrievedClaims.RoleID[0] != role {
|
||||
t.Errorf("Role = %v, want %v", retrievedClaims.RoleID, role)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -404,8 +406,8 @@ func TestGetRoleWithNoClaims(t *testing.T) {
|
||||
if ok {
|
||||
t.Error("Expected ok=false when no claims")
|
||||
}
|
||||
if role != "" {
|
||||
t.Errorf("Expected empty string, got %q", role)
|
||||
if role != nil && len(role) != 0 {
|
||||
t.Errorf("Expected no roles, got %v", role)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,7 +446,7 @@ func TestJWTAuthTokenWithMissingClaims(t *testing.T) {
|
||||
{
|
||||
"Missing UserID",
|
||||
&models.Claims{
|
||||
RoleID: "admin",
|
||||
RoleID: models.RoleIDs{3},
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
|
||||
},
|
||||
@@ -453,7 +455,7 @@ func TestJWTAuthTokenWithMissingClaims(t *testing.T) {
|
||||
{
|
||||
"Missing Role",
|
||||
&models.Claims{
|
||||
UserID: "user123",
|
||||
UsersID: "user123",
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
|
||||
},
|
||||
@@ -494,8 +496,8 @@ func TestJWTAuthConcurrentRequests(t *testing.T) {
|
||||
t.Skip("Requires RSA certificate setup - integration test")
|
||||
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: "admin",
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{3},
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
|
||||
},
|
||||
@@ -539,8 +541,8 @@ func TestJWTAuthTokenSignedWithWrongKey(t *testing.T) {
|
||||
|
||||
// Create token with wrong key
|
||||
claims := &models.Claims{
|
||||
UserID: "user123",
|
||||
RoleID: "admin",
|
||||
UsersID: "user123",
|
||||
RoleID: models.RoleIDs{3},
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@ func RateLimiterMiddleware(config models.RateLimitConfig) func(http.HandlerFunc)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract user identifier (prefer user_id from JWT, fallback to IP)
|
||||
// Extract user identifier (prefer users_id from JWT, fallback to IP)
|
||||
var identifier string
|
||||
if userID, ok := GetUserID(r); ok {
|
||||
identifier = "user:" + userID
|
||||
|
||||
Reference in New Issue
Block a user