Class: SignalWire::Security::SessionManager

Inherits:
Object
  • Object
show all
Defined in:
lib/signalwire/security/session_manager.rb

Overview

Stateless HMAC-SHA256 session manager for secure SWAIG tool tokens.

Tokens are self-contained: all information needed for validation is encoded inside the token itself. No server-side session state is stored.

mgr = SessionManager.new(token_expiry_secs: 900)
token = mgr.create_token("lookup_order", "call-abc-123")
mgr.validate_token("lookup_order", token, "call-abc-123") # => true

Instance Method Summary collapse

Constructor Details

#initialize(token_expiry_secs: 3600, secret_key: nil) ⇒ SessionManager

Returns a new instance of SessionManager.

Parameters:

  • token_expiry_secs (Integer) (defaults to: 3600)

    seconds until tokens expire (minimum 1)

  • secret_key (String, nil) (defaults to: nil)

    hex-encoded secret; generated if omitted



26
27
28
29
# File 'lib/signalwire/security/session_manager.rb', line 26

def initialize(token_expiry_secs: 3600, secret_key: nil)
  @token_expiry_secs = [token_expiry_secs, 1].max
  @secret_key = secret_key || SecureRandom.hex(32)
end

Instance Method Details

#create_token(function_name, call_id) ⇒ String

Create a secure, self-contained token for a function call.

Token format (before Base64):

call_id.function_name.expiry_timestamp.nonce.hmac_hex

Parameters:

  • function_name (String)
  • call_id (String)

Returns:

  • (String)

    URL-safe Base64-encoded token



39
40
41
42
43
44
45
46
47
48
# File 'lib/signalwire/security/session_manager.rb', line 39

def create_token(function_name, call_id)
  expiry = (Time.now.to_i + @token_expiry_secs).to_s
  nonce  = SecureRandom.hex(8)

  message   = "#{call_id}:#{function_name}:#{expiry}:#{nonce}"
  signature = compute_hmac(message)

  token_raw = "#{call_id}.#{function_name}.#{expiry}.#{nonce}.#{signature}"
  Base64.urlsafe_encode64(token_raw, padding: false)
end

#validate_token(function_name, token, call_id) ⇒ Boolean

Validate a function-call token.

Checks:

  1. Correct Base64 / structure (5 dot-separated parts)

  2. HMAC signature (timing-safe comparison)

  3. Function name matches

  4. Call ID matches

  5. Token not expired

Parameters:

  • function_name (String)
  • token (String)

    the token to validate

  • call_id (String)

Returns:

  • (Boolean)


63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/signalwire/security/session_manager.rb', line 63

def validate_token(function_name, token, call_id)
  return false if token.nil? || token.empty?
  return false if call_id.nil? || call_id.empty?

  decoded = Base64.urlsafe_decode64(token)
  parts   = decoded.split(".")
  return false unless parts.length == 5

  token_call_id, token_function, token_expiry, token_nonce, token_signature = parts

  # Verify function name
  return false unless token_function == function_name

  # Verify call ID
  return false unless token_call_id == call_id

  # Check expiry
  expiry = Integer(token_expiry)
  return false if expiry < Time.now.to_i

  # Recompute HMAC and compare with timing-safe comparison
  message           = "#{token_call_id}:#{token_function}:#{token_expiry}:#{token_nonce}"
  expected_signature = compute_hmac(message)

  secure_compare(token_signature, expected_signature)
rescue ArgumentError, TypeError
  # Bad Base64, bad integer, etc.
  false
end