Support hashed remoteStorage access tokens
CI / test (pull_request) Successful in 23s

Add a hashed_access_tokens config option (env RS_HASHED_ACCESS_TOKENS) that hashes the presented bearer token and looks up the SHA256 digest first, falling back to the plaintext key during the transition. When disabled (the default), the lookup is unchanged, so deployments not using hashed tokens pay no extra Redis round-trips.

Enable the flag here first, then migrate akkounts to re-key the Redis authorizations (kosmos/akkounts#259); the plaintext fallback keeps existing tokens working throughout.

Refs kosmos/akkounts#259
This commit is contained in:
basti committed 2026-11-04 09:34:04 +04:00
1 parent 0cf5926b65
commit 007e12d12e
4 files changed
+71 -1

No files matched your search

+13
View File
@@ -50,6 +50,19 @@ Running the app:
_TODO document options_
### Hashed access tokens
By default Liquor Cabinet looks up remoteStorage authorizations by the plaintext
bearer token (`authorizations:<user>:<token>`). Set `RS_HASHED_ACCESS_TOKENS=true`
to look up the SHA256 digest first, falling back to the plaintext key when no
digest matches.
This is a coordinated rollout with akkounts, which owns the Redis keys: enable
the flag here first (the plaintext fallback keeps existing tokens working), then
migrate akkounts so it re-keys the authorizations to digests. Keep the fallback
enabled until the re-key has been verified; only then can the plaintext lookup
be retired.
## Contributing
We love pull requests. If you want to submit a patch:
+1
View File
@@ -1,5 +1,6 @@
development: &defaults
maintenance: false
hashed_access_tokens: <%= ENV["RS_HASHED_ACCESS_TOKENS"] == "true" %>
redis:
host: <%= ENV["REDIS_HOST"] || "localhost" %>
port: <%= ENV["REDIS_PORT"] || 6379 %>
+11 -1
View File
@@ -5,6 +5,7 @@ require "active_support/core_ext/time/conversions"
require "active_support/core_ext/numeric/time"
require "active_support/core_ext/hash"
require "redis"
require "digest"
require "digest/md5"
module RemoteStorage
@@ -26,7 +27,7 @@ module RemoteStorage
server.halt 401, "Unauthorized" if token.nil? || token.empty?
authorizations = redis.smembers("authorizations:#{user}:#{token}")
authorizations = authorizations_for(user, token)
permission = directory_permission(authorizations, directory)
server.halt 401, "Unauthorized" unless permission
@@ -233,6 +234,15 @@ module RemoteStorage
end
end
def authorizations_for(user, token)
if settings.respond_to?(:hashed_access_tokens) && settings.hashed_access_tokens
digest = Digest::SHA256.hexdigest(token)
permissions = redis.smembers("authorizations:#{user}:#{digest}")
return permissions unless permissions.empty?
end
redis.smembers("authorizations:#{user}:#{token}")
end
def directory_permission(authorizations, directory)
authorizations = authorizations.map do |auth|
auth.index(":") ? auth.split(":") : [auth, "rw"]
+46
View File
@@ -704,4 +704,50 @@ JFIFddDuckyA␍⎺␉␊␍
end
end
end
describe "authorization with hashed access tokens" do
before do
redis.del "authorizations:phil:#{Digest::SHA256.hexdigest("amarillo")}"
redis.del "authorizations:phil:amarillo"
purge_redis
header "Authorization", "Bearer amarillo"
end
after do
app.set :hashed_access_tokens, false
end
context "when hashed access tokens are enabled" do
before do
app.set :hashed_access_tokens, true
end
it "authorizes using the digest key" do
redis.sadd "authorizations:phil:#{Digest::SHA256.hexdigest("amarillo")}", [":rw"]
put "/phil/food/aguacate", "si"
_(last_response.status).must_equal 201
end
it "falls back to the plaintext key during the rollout" do
redis.sadd "authorizations:phil:amarillo", [":rw"]
put "/phil/food/aguacate", "si"
_(last_response.status).must_equal 201
end
end
context "when hashed access tokens are disabled" do
it "does not authorize a digest-only key" do
redis.sadd "authorizations:phil:#{Digest::SHA256.hexdigest("amarillo")}", [":rw"]
put "/phil/food/aguacate", "si"
_(last_response.status).must_equal 401
_(last_response.body).must_equal "Unauthorized"
end
end
end
end