Module: Turbocable::Auth
- Defined in:
- lib/turbocable/auth.rb
Overview
JWT minting and public-key publishing for the TurboCable gateway.
The gateway validates every WebSocket connection with an RS256 JWT. The gem is responsible for:
-
Minting short-lived tokens via
Auth.issue_token. -
Publishing the corresponding public key to the NATS KV bucket the gateway watches via
Auth.publish_public_key!.
Typical boot sequence
Turbocable.configure do |c|
c.jwt_private_key = File.read("private.pem")
c.jwt_public_key = File.read("public.pem")
c.jwt_issuer = "my-rails-app"
end
# Once at boot (or after every key rotation):
Turbocable::Auth.publish_public_key!
# Per request / per user:
token = Turbocable::Auth.issue_token(
sub: current_user.id.to_s,
allowed_streams: ["chat_room_*"],
ttl: 3600
)
Allowed-stream patterns
The server supports three glob forms for allowed_streams:
-
“*” — the subscriber may access any stream.
-
“prefix_*” — any stream whose name starts with
prefix_. The prefix must be a non-empty string matching /A\z/and the trailing *+ must be the only wildcard character. -
Exact name — a single stream name matching +/A
\z/.
Anything else (embedded dots, mid-string wildcards, multiple wildcards, whitespace) is rejected by issue_token at mint time with AuthError.
Key rotation runbook
See docs/auth.md for the full runbook, including the warning about TURBOCABLE_JWT_PUBLIC_KEY_PATH silently shadowing the KV entry.
Class Method Summary collapse
-
.issue_token(sub:, allowed_streams:, ttl:, **extra_claims) ⇒ String
Mints a short-lived RS256 JWT for a WebSocket subscriber.
-
.publish_public_key! ⇒ Integer
Publishes the configured RSA public key PEM to the NATS KV bucket that the gateway watches for hot-reload.
-
.valid_stream_pattern?(pattern) ⇒ Boolean
Returns
trueifpatternis a validallowed_streamsentry for the server’s glob grammar. -
.verify_token(token) ⇒ Array<(Hash, Hash)>
Decodes and verifies a JWT signed with the configured public key.
Class Method Details
.issue_token(sub:, allowed_streams:, ttl:, **extra_claims) ⇒ String
Mints a short-lived RS256 JWT for a WebSocket subscriber.
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/turbocable/auth.rb', line 64 def self.issue_token(sub:, allowed_streams:, ttl:, **extra_claims) config = Turbocable.config pem = config.jwt_private_key raise ConfigurationError, "jwt_private_key is required to mint tokens" if pem.nil? || pem.empty? rsa_key = load_rsa_private_key!(pem) validate_allowed_streams!(allowed_streams) now = Time.now.to_i payload = { sub: sub, allowed_streams: allowed_streams, iat: now, exp: now + ttl } payload[:iss] = config.jwt_issuer if config.jwt_issuer payload.merge!(extra_claims) JWT.encode(payload, rsa_key, "RS256") end |
.publish_public_key! ⇒ Integer
Publishes the configured RSA public key PEM to the NATS KV bucket that the gateway watches for hot-reload.
Creates the TC_PUBKEYS bucket if it does not yet exist. The server watches the bucket but never creates it.
Warning: if the server operator has set TURBOCABLE_JWT_PUBLIC_KEY_PATH to a file, the server will prioritise that file over the KV entry and KV rotations will be silently ignored. This method emits a :warn log when it detects this condition (by probing GET /pubkey on the server). See docs/auth.md for the rotation runbook.
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/turbocable/auth.rb', line 102 def self.publish_public_key! config = Turbocable.config pem = config.jwt_public_key raise ConfigurationError, "jwt_public_key is required to publish the public key" if pem.nil? || pem.empty? # Guard against accidentally publishing a private key if pem.include?("PRIVATE KEY") raise AuthError, "jwt_public_key appears to contain a private key — refusing to publish. " \ "Set jwt_public_key to the *public* half of your RSA key pair." end rsa_pub = load_rsa_public_key!(pem) canonical_pem = rsa_pub.public_key.to_pem maybe_warn_file_shadow!(config, canonical_pem) kv = Turbocable.client.send(:connection).key_value(config.jwt_kv_bucket) revision = kv.put(config.jwt_kv_key, canonical_pem) config.logger.info do "[Turbocable::Auth] Published public key to #{config.jwt_kv_bucket}/#{config.jwt_kv_key} " \ "(revision #{revision})" end revision end |
.valid_stream_pattern?(pattern) ⇒ Boolean
Returns true if pattern is a valid allowed_streams entry for the server’s glob grammar. Exposed for callers that want to pre-validate patterns without minting a full token.
157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/turbocable/auth.rb', line 157 def self.valid_stream_pattern?(pattern) return true if pattern == "*" if pattern.end_with?("*") prefix = pattern[0..-2] return false if prefix.empty? return prefix.match?(/\A[A-Za-z0-9_:-]+\z/) end pattern.match?(/\A[A-Za-z0-9_:-]+\z/) end |
.verify_token(token) ⇒ Array<(Hash, Hash)>
Decodes and verifies a JWT signed with the configured public key.
*Intended for test suites only* — the gateway verifies tokens itself. Do not use this in production request paths.
141 142 143 144 145 146 147 148 149 |
# File 'lib/turbocable/auth.rb', line 141 def self.verify_token(token) config = Turbocable.config pem = config.jwt_public_key raise ConfigurationError, "jwt_public_key is required to verify tokens" if pem.nil? || pem.empty? rsa_pub = load_rsa_public_key!(pem) JWT.decode(token, rsa_pub.public_key, true, algorithms: ["RS256"]) end |