Module: DurableStreams::Rails::Gatekeeper

Defined in:
lib/durable_streams/rails/gatekeeper.rb

Overview

ES256 JWT signing for Durable Streams authentication.

Replaces the symmetric HMAC approach (ActiveSupport::MessageVerifier) with asymmetric ES256 (ECDSA P-256). Rails holds the private key and signs tokens. Caddy verifies with the public key via ggicci/caddy-jwt — no callback to Rails.

Descended from the deleted app-level Gatekeeper model (commit e4f9c53), adapted for the gem.

Configuration

DurableStreams.signing_key     = Rails.application.credentials.dig(:durable_streams, :signing_key)
DurableStreams.signing_kid     = "es256-20260330"
DurableStreams.token_issuer    = "https://exchange.tokimonki.com"
DurableStreams.token_expiry    = 120  # seconds

Key generation

rake durable_streams:generate_keys

Constant Summary collapse

ALG =
"ES256"

Class Method Summary collapse

Class Method Details

.decode(token) ⇒ Object

Decode and verify a JWT. Used in testing — production verification happens in Caddy. The algorithm is hardcoded to ES256 to prevent algorithm confusion attacks. Issuer is verified to prevent cross-environment token reuse.



55
56
57
58
59
60
61
62
# File 'lib/durable_streams/rails/gatekeeper.rb', line 55

def decode(token)
  options = {
    algorithm: ALG,
    verify_iss: true,
    iss: DurableStreams.token_issuer
  }
  ::JWT.decode(token, verify_key, true, options).first
end

.encode(payload, expires_in: DurableStreams.token_expiry) ⇒ Object

Encode a payload as an ES256 JWT.

Standard claims (iss, iat, exp) are merged automatically. Pass expires_in to override the default expiry.

DurableStreams::Rails::Gatekeeper.encode("stream" => "rooms/1/messages", "permissions" => ["read"])
DurableStreams::Rails::Gatekeeper.encode({ "server" => true }, expires_in: 120)


36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/durable_streams/rails/gatekeeper.rb', line 36

def encode(payload, expires_in: DurableStreams.token_expiry)
  now = Time.now.to_i

  claims = payload.merge(
    "iss" => DurableStreams.token_issuer,
    "aud" => DurableStreams.client_url,
    "iat" => now,
    "exp" => now + expires_in.to_i
  )

  headers = {}
  headers["kid"] = DurableStreams.signing_kid if DurableStreams.signing_kid

  ::JWT.encode(claims, signing_key, ALG, headers)
end

.generate_key_pairObject

Generate an ES256 key pair. Returns [private_pem, public_pem].

private_pem, public_pem = DurableStreams::Rails::Gatekeeper.generate_key_pair


68
69
70
71
# File 'lib/durable_streams/rails/gatekeeper.rb', line 68

def generate_key_pair
  key = OpenSSL::PKey::EC.generate("prime256v1")
  [ key.to_pem, key.public_to_pem ]
end

.reset_signing_keyObject



73
74
75
76
# File 'lib/durable_streams/rails/gatekeeper.rb', line 73

def reset_signing_key
  @signing_key = nil
  @verify_key = nil
end