Class: RackJwtAegis::JwtValidator

Inherits:
Object
  • Object
show all
Defined in:
lib/rack_jwt_aegis/jwt_validator.rb

Overview

JWT token validation and payload verification

Handles JWT token decoding, signature verification, and payload validation including claims verification and type checking based on configuration.

Examples:

Basic usage

config = Configuration.new(jwt_secret: 'your-secret')
validator = JwtValidator.new(config)
payload = validator.validate(jwt_token)

With multi-tenant validation

config = Configuration.new(
  jwt_secret: 'your-secret',
  validate_tenant_id: true,
  validate_subdomain: true,
  validate_pathname_slug: true
)
validator = JwtValidator.new(config)
payload = validator.validate(jwt_token) # Will validate tenant claims

Author:

  • Ken Camajalan Demanawa

Since:

  • 0.1.0

Instance Method Summary collapse

Constructor Details

#initialize(config) ⇒ JwtValidator

Initialize the JWT validator

Parameters:

Since:

  • 0.1.0



32
33
34
# File 'lib/rack_jwt_aegis/jwt_validator.rb', line 32

def initialize(config)
  @config = config
end

Instance Method Details

#validate(token) ⇒ Hash

Validate and decode a JWT token

Parameters:

  • token (String)

    the JWT token to validate

Returns:

  • (Hash)

    the decoded JWT payload

Raises:

Since:

  • 0.1.0



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/rack_jwt_aegis/jwt_validator.rb', line 42

def validate(token)
  # Decode JWT with verification
  payload, _header = JWT.decode(
    token,
    @config.jwt_secret,
    true, # verify signature
    {
      algorithm: @config.jwt_algorithm,
      verify_expiration: true,
      verify_not_before: true,
      verify_iat: true,
      verify_aud: false, # Not validating audience by default
      verify_iss: false, # Not validating issuer by default
      verify_sub: false, # Not validating subject by default
    },
  )

  # Validate payload structure
  validate_payload_structure(payload)

  payload
rescue JWT::ExpiredSignature
  raise AuthenticationError, 'JWT token has expired'
rescue JWT::ImmatureSignature
  raise AuthenticationError, 'JWT token not yet valid'
rescue JWT::InvalidIatError
  raise AuthenticationError, 'JWT token issued in the future'
rescue JWT::VerificationError
  raise AuthenticationError, 'JWT signature verification failed'
rescue JWT::DecodeError => e
  raise AuthenticationError, "Invalid JWT token: #{e.message}"
rescue StandardError => e
  raise AuthenticationError, "JWT validation error: #{e.message}"
end

#validate_claim_types(payload) ⇒ Object (private)

Validate the data types of specific claims in the payload

Parameters:

  • payload (Hash)

    the JWT payload to validate

Raises:

Since:

  • 0.1.0



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/rack_jwt_aegis/jwt_validator.rb', line 116

def validate_claim_types(payload)
  user_id = payload[@config.payload_key(:user_id).to_s]
  # User ID should be numeric or string
  if user_id.to_s.empty? || (!user_id.is_a?(Numeric) && !user_id.is_a?(String))
    raise AuthenticationError, 'Invalid user_id format in JWT payload'
  end

  # Tenant ID should be numeric or string (if present)
  if @config.validate_tenant_id?
    tenant_id = payload[@config.payload_key(:tenant_id).to_s]
    if tenant_id.to_s.empty? || (!tenant_id.is_a?(Numeric) && !tenant_id.is_a?(String))
      raise AuthenticationError, 'Invalid tenant_id format in JWT payload'
    end
  end

  # Company group domain should be string (if present)
  if @config.validate_subdomain?
    subdomain = payload[@config.payload_key(:subdomain).to_s]
    if subdomain.to_s.empty? || !subdomain.is_a?(String)
      raise AuthenticationError, 'Invalid subdomain format in JWT payload'
    end
  end

  # Company slugs should be array (if present)
  return unless @config.validate_pathname_slug?

  pathname_slugs = payload[@config.payload_key(:pathname_slugs).to_s]
  return unless pathname_slugs && !pathname_slugs.is_a?(Array)

  raise AuthenticationError, 'Invalid pathname_slugs format in JWT payload - must be array'
end

#validate_payload_structure(payload) ⇒ Object (private)

Validate the structure and content of the JWT payload

Parameters:

  • payload (Object)

    the decoded payload from JWT

Raises:

Since:

  • 0.1.0



83
84
85
86
87
88
89
90
91
92
# File 'lib/rack_jwt_aegis/jwt_validator.rb', line 83

def validate_payload_structure(payload)
  # Ensure payload is a hash
  raise AuthenticationError, 'Invalid JWT payload structure' unless payload.is_a?(Hash)

  # Validate required claims based on enabled features
  validate_required_claims(payload)

  # Validate claim types
  validate_claim_types(payload)
end

#validate_required_claims(payload) ⇒ Object (private)

Validate that all required claims are present in the payload

Parameters:

  • payload (Hash)

    the JWT payload to validate

Raises:

Since:

  • 0.1.0



98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/rack_jwt_aegis/jwt_validator.rb', line 98

def validate_required_claims(payload)
  # Always require user identification
  required_claims = [@config.payload_key(:user_id)]
  required_claims << @config.payload_key(:subdomain) if @config.validate_subdomain?
  required_claims << @config.payload_key(:tenant_id) if @config.validate_tenant_id?
  required_claims << @config.payload_key(:pathname_slugs) if @config.validate_pathname_slug?
  required_claims << @config.payload_key(:role_ids) if @config.rbac_enabled?

  missing_claims = required_claims.select { |claim| payload[claim.to_s].to_s.empty? }
  return if missing_claims.empty?

  raise AuthenticationError, "JWT payload missing required claims: #{missing_claims.join(', ')}"
end