Module: Verikloak::BFF::HeaderGuardSanitizer

Defined in:
lib/verikloak/bff/header_guard.rb

Overview

Internal helpers that sanitize tokens and log payloads for HeaderGuard.

Constant Summary collapse

LOG_CONTROL_CHARS =
Constants::LOG_CONTROL_CHARS
MAX_LOG_FIELD_LENGTH =
Constants::MAX_LOG_FIELD_LENGTH

Class Method Summary collapse

Class Method Details

.emit_to_logger(logger, sanitized, kind) ⇒ void

This method returns an undefined value.

Parameters:

  • logger (Logger, nil)
  • sanitized (Hash)
  • kind (Symbol)


150
151
152
153
154
155
# File 'lib/verikloak/bff/header_guard.rb', line 150

def emit_to_logger(logger, sanitized, kind)
  return unless logger

  msg = sanitized.map { |k, v| v.nil? || v.to_s.empty? ? nil : "#{k}=#{v}" }.compact.join(' ')
  logger.public_send(kind == :ok ? :info : :warn, msg)
end

.invoke_log_hook(config, sanitized) ⇒ void

This method returns an undefined value.

Parameters:



138
139
140
141
142
143
144
# File 'lib/verikloak/bff/header_guard.rb', line 138

def invoke_log_hook(config, sanitized)
  return unless config.log_with.respond_to?(:call)

  config.log_with.call(sanitized)
rescue StandardError => e
  warn("[verikloak-bff] log_with hook failed: #{e.class}: #{e.message}") if $DEBUG
end

.log_event(env, config, kind, **attrs) ⇒ void

This method returns an undefined value.

Emit a structured log line if a logger is present.

Parameters:

  • env (Hash)
  • config (Configuration)
  • kind (Symbol)

    :ok, :mismatch, :claims_mismatch, :error

  • attrs (Hash)


126
127
128
129
130
131
132
133
# File 'lib/verikloak/bff/header_guard.rb', line 126

def log_event(env, config, kind, **attrs)
  payload = { event: 'bff.header_guard', kind: kind, rid: request_id(env) }.merge(attrs).compact
  sanitized = sanitize_payload(payload)
  invoke_log_hook(config, sanitized)
  emit_to_logger(config.logger || env['rack.logger'], sanitized, kind)
rescue StandardError => e
  warn("[verikloak-bff] log_event failed: #{e.class}: #{e.message}") if $DEBUG
end

.request_id(env) ⇒ String?

Extract request id for logging from common headers.

Parameters:

  • env (Hash)

Returns:

  • (String, nil)


115
116
117
# File 'lib/verikloak/bff/header_guard.rb', line 115

def request_id(env)
  env['HTTP_X_REQUEST_ID'] || env['action_dispatch.request_id']
end

.respond_with_error(env, config, error) ⇒ Array(Integer, Hash, Array<String>)

Build a minimal RFC6750-style error response. Delegates to ErrorResponse for consistent formatting across gems.

Parameters:

Returns:

  • (Array(Integer, Hash, Array<String>))


164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/verikloak/bff/header_guard.rb', line 164

def respond_with_error(env, config, error)
  log_event(env, config, :error, code: error.code)
  require 'verikloak/error_response'
  status, headers, body = Verikloak::ErrorResponse.build(
    code: error.code, message: error.message, status: error.http_status
  )

  # RFC 6750 ยง3.1: include WWW-Authenticate on 403 for client diagnostics
  if status == 403 && !headers.key?('WWW-Authenticate')
    sanitize = Verikloak::ErrorResponse.method(:sanitize_header_value)
    headers['WWW-Authenticate'] = format(
      'Bearer realm="%<realm>s", error="%<code>s", error_description="%<msg>s"',
      realm: sanitize.call('verikloak-bff'),
      code: sanitize.call(error.code),
      msg: sanitize.call(error.message)
    )
  end

  [status, headers, body]
end

.sanitize_log_field(value) ⇒ Object?

Sanitize an individual value destined for logs, pruning empty results.

Parameters:

  • value (Object)

Returns:

  • (Object, nil)

    sanitized value or nil when the result is empty



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/verikloak/bff/header_guard.rb', line 69

def sanitize_log_field(value)
  case value
  when nil
    nil
  when String
    sanitized = sanitize_string(value)
    sanitized.empty? ? nil : sanitized
  when Array
    sanitized = value.map { |item| item.is_a?(String) ? sanitize_string(item) : item }
    sanitized.reject! { |item| item.nil? || (item.is_a?(String) && item.empty?) }
    sanitized.empty? ? nil : sanitized
  else
    value
  end
end

.sanitize_payload(payload) ⇒ Hash

Remove unsafe characters from a structured logging payload.

Parameters:

  • payload (Hash)

Returns:

  • (Hash)

    sanitized payload suitable for logging



61
62
63
# File 'lib/verikloak/bff/header_guard.rb', line 61

def sanitize_payload(payload)
  payload.transform_values { |value| sanitize_log_field(value) }.compact
end

.sanitize_string(value) ⇒ String

Remove control characters, invalid UTF-8, and truncate to MAX_LOG_FIELD_LENGTH.

Parameters:

  • value (#to_s)

Returns:

  • (String)


89
90
91
92
93
# File 'lib/verikloak/bff/header_guard.rb', line 89

def sanitize_string(value)
  sanitized = value.to_s.encode('UTF-8', invalid: :replace, undef: :replace, replace: '').gsub(LOG_CONTROL_CHARS,
                                                                                               '')
  sanitized.length > MAX_LOG_FIELD_LENGTH ? "#{sanitized[0, MAX_LOG_FIELD_LENGTH]}..." : sanitized
end

.token_source(prefer_forwarded, auth_token, fwd_token) ⇒ String

Describe the source of the chosen token for logging purposes.

Parameters:

  • prefer_forwarded (Boolean)
  • auth_token (String, nil)
  • fwd_token (String, nil)

Returns:

  • (String)

    "authorization" or "forwarded"



101
102
103
104
105
106
107
108
109
# File 'lib/verikloak/bff/header_guard.rb', line 101

def token_source(prefer_forwarded, auth_token, fwd_token)
  return 'forwarded' if prefer_forwarded && fwd_token

  if auth_token && fwd_token
    'authorization'
  else
    (auth_token ? 'authorization' : 'forwarded')
  end
end

.token_tags_from_decoded(payload, header) ⇒ Hash{Symbol=>Object}

Build sanitized log tags from pre-decoded JWT payload and header. Avoids redundant decoding when the caller already has decoded data.

Parameters:

  • payload (Hash)
  • header (Hash)

Returns:

  • (Hash{Symbol=>Object})


41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/verikloak/bff/header_guard.rb', line 41

def token_tags_from_decoded(payload, header)
  return {} unless payload.is_a?(Hash)

  aud = payload['aud']
  aud = aud.join(' ') if aud.is_a?(Array)
  {
    sub: sanitize_log_field(payload['sub']&.to_s),
    iss: sanitize_log_field(payload['iss']&.to_s),
    aud: sanitize_log_field(aud&.to_s),
    kid: sanitize_log_field(header&.dig('kid')&.to_s)
  }.compact
rescue StandardError => e
  warn("[verikloak-bff] token_tags_from_decoded failed: #{e.class}: #{e.message}") if $DEBUG
  {}
end