Skip to content

Commit

Permalink
Support hmac_secret rotation in jwt_refresh feature
Browse files Browse the repository at this point in the history
This supports the use of the previous secret, but does not take
any actions to update it to the new secret (there isn't actions
you can take, as the token is submitted as a parameter, it's not
part of the session or a cookie).  However, as jwt refresh tokens
are only valid for 30 minutes by default, rotation to a new secret
will occur quickly.  The only exception is when
allow_refresh_with_expired_jwt_access_token? is set, in which case
the user would need to decide how long they want to keep the old
secret around to support old expired access tokens.
  • Loading branch information
jeremyevans committed Sep 29, 2023
1 parent c42f6a2 commit dee3b8d
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 4 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
=== master

* Support hmac_secret rotation in jwt_refresh feature (jeremyevans) (#365)

* Support hmac_secret rotation in single_session feature (jeremyevans) (#365)

* Support hmac_secret rotation in remember feature (jeremyevans) (#365)
Expand Down
6 changes: 4 additions & 2 deletions lib/rodauth/features/jwt_refresh.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def _account_from_refresh_token(token)
unless key &&
(id.to_s == session_value.to_s) &&
(actual = get_active_refresh_token(id, token_id)) &&
timing_safe_eql?(key, convert_token_key(actual)) &&
(timing_safe_eql?(key, convert_token_key(actual)) || (hmac_secret_rotation? && timing_safe_eql?(key, compute_old_hmac(actual)))) &&
jwt_refresh_token_match?(key)
return
end
Expand Down Expand Up @@ -150,7 +150,9 @@ def jwt_refresh_token_match?(key)

# If allowing with expired jwt access token, check the expired session contains
# hmac matching submitted and active refresh token.
timing_safe_eql?(compute_hmac(session[jwt_refresh_token_data_session_key].to_s + key), session[jwt_refresh_token_hmac_session_key].to_s)
s = session[jwt_refresh_token_hmac_session_key].to_s
h = session[jwt_refresh_token_data_session_key].to_s + key
timing_safe_eql?(compute_hmac(h), s) || (hmac_secret_rotation? && timing_safe_eql?(compute_old_hmac(h), s))
end

def get_active_refresh_token(account_id, token_id)
Expand Down
95 changes: 93 additions & 2 deletions spec/jwt_refresh_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -178,11 +178,17 @@

[false, true].each do |hs|
it "generates and refreshes Refresh Tokens #{'with hmac_secret' if hs}" do
initial_secret = secret = SecureRandom.random_bytes(32) if hs
if hs
initial_secret = secret = SecureRandom.random_bytes(32)
old_secret = nil
end
rt = nil
rodauth do
enable :login, :logout, :jwt_refresh
hmac_secret{secret} if hs
if hs
hmac_secret{secret}
hmac_old_secret{old_secret}
end
jwt_secret '1'
skip_status_checks? hs
after_refresh_token{rt = json_response['refresh_token']}
Expand Down Expand Up @@ -281,6 +287,31 @@
@authorization = res.last['access_token']
res = json_request("/")
res.must_equal [200, {'hello'=>'world'}]
seventh_refresh_token = jwt_refresh_login.last['refresh_token']

# Refresh secret works when rotating
old_secret = secret
secret = SecureRandom.random_bytes(32)
res = json_request("/jwt-refresh", :refresh_token=>seventh_refresh_token)
jwt_refresh_validate(res)

# And still gives us a valid access token
@authorization = res.last['access_token']
res = json_request("/")
res.must_equal [200, {'hello'=>'world'}]
eighth_refresh_token = jwt_refresh_login.last['refresh_token']

# Refresh secret works after rotating
old_secret = nil
res = json_request("/jwt-refresh", :refresh_token=>eighth_refresh_token)
jwt_refresh_validate(res)

# Refresh secret doesn't work if neither secret matches
secret = SecureRandom.random_bytes(32)
old_secret = SecureRandom.random_bytes(32)
res = json_request("/jwt-refresh", :refresh_token=>eighth_refresh_token)
res.first.must_equal 400
res.must_equal [400, {'error'=>'invalid JWT refresh token'}]
end
end
end
Expand Down Expand Up @@ -430,6 +461,66 @@
json_request('/').must_equal [200, {"authenticated_by"=>["password"]}]
end

it "should allow refreshing token when providing expired access token when rotating hmac secret" do
period = -2
secret = SecureRandom.random_bytes(32)
old_secret = nil
rodauth do
enable :login, :logout, :jwt_refresh, :close_account
hmac_secret{secret}
hmac_old_secret{old_secret}
jwt_secret '1'
jwt_access_token_period{period}
allow_refresh_with_expired_jwt_access_token? true
end
roda(:jwt) do |r|
r.rodauth
rodauth.require_authentication
response['Content-Type'] = 'application/json'
{'authenticated_by' => rodauth.authenticated_by}.to_json
end

res = jwt_refresh_login
refresh_token = res.last['refresh_token']
period = 1800

old_secret = secret
secret = SecureRandom.random_bytes(32)
res = json_request("/jwt-refresh", :refresh_token=>refresh_token)
jwt_refresh_validate(res)

json_request('/').must_equal [200, {"authenticated_by"=>["password"]}]
end

it "should not allow refreshing token when providing expired access token when rotating hmac secret with invalid old secret" do
period = -2
secret = SecureRandom.random_bytes(32)
old_secret = nil
rodauth do
enable :login, :logout, :jwt_refresh, :close_account
hmac_secret{secret}
hmac_old_secret{old_secret}
jwt_secret '1'
jwt_access_token_period{period}
allow_refresh_with_expired_jwt_access_token? true
end
roda(:jwt) do |r|
r.rodauth
rodauth.require_authentication
response['Content-Type'] = 'application/json'
{'authenticated_by' => rodauth.authenticated_by}.to_json
end

res = jwt_refresh_login
refresh_token = res.last['refresh_token']
period = 1800

old_secret = SecureRandom.random_bytes(32)
secret = SecureRandom.random_bytes(32)
res = json_request("/jwt-refresh", :refresh_token=>refresh_token)
res.must_equal [400, {"error"=>"invalid JWT refresh token"}]
end

it "should allow refreshing token when providing expired access token if configured and prefix is not correct" do
period = -2
rodauth do
Expand Down

0 comments on commit dee3b8d

Please sign in to comment.