Signed does not mean authorized

Free preview 3 min to read

A signed stream name (two courses ago) can't be tampered with. A client can't invent a new one; it can only present one the server generated. That fact answers one question and leaves another open. Proving a name is genuine says nothing about whether this visitor, right now, should be allowed to hear it.

One key for the whole application

Turbo.signed_stream_verifier, from this app's installed turbo-rails-2.0.23:

def signed_stream_verifier
  @signed_stream_verifier ||= ActiveSupport::MessageVerifier.new(signed_stream_verifier_key, digest: "SHA256", serializer: JSON)
end

And where the key comes from, turbo/engine.rb, unedited:

initializer "turbo.signed_stream_verifier_key" do
  config.after_initialize do
    Turbo.signed_stream_verifier_key = config.turbo.signed_stream_verifier_key ||
      Rails.application.key_generator.generate_key("turbo/signed_stream_verifier_key")
  end
end

One verifier and one key, derived once from this app's secret_key_base through Rails.application.key_generator, shared by every stream name this app signs. The public roadmap's "catalog_roadmap" and a hypothetical private stream would both be verified by that same key. The key proves this server generated the name. It has no idea who's asking right now, because a single application-wide key was never designed to encode that.

What "verified" checks, once more

Turbo::StreamsChannel#subscribed, read in the "Turbo Streams: WebSockets" course, is the entire authorization check that ships with Turbo:

def subscribed
  if stream_name = verified_stream_name_from_params
    stream_from stream_name
  else
    reject
  end
end

"Verified" means one thing here: the signature matches the payload, under this app's key. It doesn't mean "the current Action Cable connection belongs to the user this stream was meant for". Nothing in this method reads current_user, checks a session or compares an id. Anyone holding a validly signed stream name, however they got it, subscribes successfully. Turbo was never designed to answer "should this person see this", only "is this name real".

Why this app's one stream gets away with it

"catalog_roadmap", the last two courses' example, is fine because there's nothing to protect. The roadmap is public course data, visible to signed-in and anonymous visitors alike, by design. Signing still matters there (a tampered name could subscribe to something unintended), but authorization was never a question for a stream everyone may see. Once a stream is meant for one person and nobody else, that gap, signed but not authorized, becomes the whole problem this course sets out to close.

Key takeaways

  • This app's Turbo.signed_stream_verifier uses one application-wide key, derived from secret_key_base. Every stream name this app signs, public or private, shares that key.
  • Turbo::StreamsChannel#subscribed, read again, checks the signature and nothing else: no current_user, no session check, no per-user logic.
  • "Verified" proves a name wasn't tampered with. It proves nothing about whether the connection presenting it belongs to the person the stream was meant for.
  • This app's one stream, "catalog_roadmap", is safe without extra authorization only because it's public. The gap in this lesson matters once a stream isn't meant for everyone.
Lesson 1 of 7 · free

Keep going with lesson 2

Signed does not mean secret

Signing gives integrity without confidentiality: plain Base64.decode64 recovers a stream name's payload without any key.

A free account also gives you the quizzes, your progress and notes, and the discussion.

Type to start searching...