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
-
.decode(token) ⇒ Object
Decode and verify a JWT.
-
.encode(payload, expires_in: DurableStreams.token_expiry) ⇒ Object
Encode a payload as an ES256 JWT.
-
.generate_key_pair ⇒ Object
Generate an ES256 key pair.
- .reset_signing_key ⇒ Object
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) = { algorithm: ALG, verify_iss: true, iss: DurableStreams.token_issuer } ::JWT.decode(token, verify_key, true, ).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_pair ⇒ Object
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_key ⇒ Object
73 74 75 76 |
# File 'lib/durable_streams/rails/gatekeeper.rb', line 73 def reset_signing_key @signing_key = nil @verify_key = nil end |